浏览代码

align svg exports with canvas exports

Ryan Di 9 月之前
父节点
当前提交
e49db1dd3c

+ 0 - 3
packages/excalidraw/components/PasteChartDialog.tsx

@@ -50,9 +50,6 @@ const ChartPreviewBtn = (props: {
           },
           files: null,
         },
-        config: {
-          skipInliningFonts: true,
-        },
       });
       svg.querySelector(".style-fonts")?.remove();
       previewNode.replaceChildren();

+ 0 - 3
packages/excalidraw/components/PublishLibrary.tsx

@@ -140,9 +140,6 @@ const SingleLibraryItem = ({
           },
           files: null,
         },
-        config: {
-          skipInliningFonts: true,
-        },
       });
       node.innerHTML = svg.outerHTML;
     })();

+ 0 - 4
packages/excalidraw/hooks/useLibraryItemSvg.ts

@@ -19,10 +19,6 @@ const exportLibraryItemToSvg = async (elements: LibraryItem["elements"]) => {
       },
       files: null,
     },
-    config: {
-      renderEmbeddables: false,
-      skipInliningFonts: true,
-    },
   });
 };
 

+ 69 - 105
packages/excalidraw/scene/export.ts

@@ -29,14 +29,14 @@ import {
   getInitializedImageElements,
   updateImageCache,
 } from "../element/image";
-import { restore, restoreAppState } from "../data/restore";
+import { restoreAppState } from "../data/restore";
 import {
   getElementsOverlappingFrame,
   getFrameLikeElements,
   getFrameLikeTitle,
   getRootElements,
 } from "../frame";
-import { getNonDeletedElements, newTextElement } from "../element";
+import { newTextElement } from "../element";
 import { type Mutable } from "../utility-types";
 import { newElementWith } from "../element/mutateElement";
 import { isFrameLikeElement } from "../element/typeChecks";
@@ -164,13 +164,13 @@ type ExportToCanvasAppState = Partial<
   Omit<AppState, "offsetTop" | "offsetLeft">
 >;
 
-export type ExportToCanvasData = {
+export type ExportSceneData = {
   elements: readonly NonDeletedExcalidrawElement[];
   appState?: ExportToCanvasAppState;
   files: BinaryFiles | null;
 };
 
-export type ExportToCanvasConfig = {
+export type ExportSceneConfig = {
   theme?: Theme;
   /**
    * Canvas background. Valid values are:
@@ -339,8 +339,8 @@ const configExportDimension = async ({
   data,
   config,
 }: {
-  data: ExportToCanvasData;
-  config?: ExportToCanvasConfig;
+  data: ExportSceneData;
+  config?: ExportSceneConfig;
 }) => {
   // clone
   const cfg = Object.assign({}, config);
@@ -626,8 +626,8 @@ export const exportToCanvas = async ({
   data,
   config,
 }: {
-  data: ExportToCanvasData;
-  config?: ExportToCanvasConfig;
+  data: ExportSceneData;
+  config?: ExportSceneConfig;
 }) => {
   const {
     config: cfg,
@@ -706,7 +706,7 @@ export const exportToCanvas = async ({
 };
 
 type ExportToSvgConfig = Pick<
-  ExportToCanvasConfig,
+  ExportSceneConfig,
   "canvasBackgroundColor" | "padding" | "theme" | "exportingFrame"
 > & {
   /**
@@ -721,108 +721,75 @@ export const exportToSvg = async ({
   data,
   config,
 }: {
-  data: {
-    elements: readonly NonDeletedExcalidrawElement[];
-    appState: {
-      exportBackground: boolean;
-      exportScale?: number;
-      viewBackgroundColor: string;
-      exportWithDarkMode?: boolean;
-      exportEmbedScene?: boolean;
-      frameRendering?: AppState["frameRendering"];
-      gridModeEnabled?: boolean;
-    };
-    files: BinaryFiles | null;
-  };
-  config?: ExportToSvgConfig;
-}): Promise<SVGSVGElement> => {
-  // clone
-  const cfg = Object.assign({}, config);
+  data: ExportSceneData;
+  config?: ExportSceneConfig;
+}) => {
+  const {
+    config: cfg,
+    normalizedPadding,
+    exportWidth,
+    exportHeight,
+    exportScale,
+    x,
+    y,
+    elementsForRender,
+    appState,
+    frameRendering,
+  } = await configExportDimension({ data, config });
 
-  cfg.exportingFrame = cfg.exportingFrame ?? null;
+  const offsetX = -(x - normalizedPadding);
+  const offsetY = -(y - normalizedPadding);
 
-  const { elements: restoredElements } = restore(
-    { ...data, files: data.files || {} },
-    null,
-    null,
-  );
-  const elements = getNonDeletedElements(restoredElements);
+  const { elements } = data;
 
-  const frameRendering = getFrameRenderingConfig(
-    cfg?.exportingFrame ?? null,
-    data.appState.frameRendering ?? null,
+  // initialize SVG root
+  const svgRoot = document.createElementNS(SVG_NS, "svg");
+  svgRoot.setAttribute("version", "1.1");
+  svgRoot.setAttribute("xmlns", SVG_NS);
+  svgRoot.setAttribute(
+    "viewBox",
+    `0 0 ${exportWidth / exportScale} ${exportHeight / exportScale}`,
   );
+  svgRoot.setAttribute("width", `${exportWidth}`);
+  svgRoot.setAttribute("height", `${exportHeight}`);
+  if (cfg.theme === THEME.DARK) {
+    svgRoot.setAttribute("filter", THEME_FILTER);
+  }
 
-  let {
-    exportWithDarkMode = false,
-    viewBackgroundColor,
-    exportScale = 1,
-    exportEmbedScene,
-  } = data.appState;
-
-  let padding = cfg.padding ?? 0;
-
-  const elementsForRender = prepareElementsForRender({
-    elements,
-    exportingFrame: cfg.exportingFrame,
-    exportWithDarkMode,
-    frameRendering,
-  });
+  const fontFaces = cfg.loadFonts
+    ? await Fonts.generateFontFaceDeclarations(elements)
+    : [];
 
-  if (cfg.exportingFrame) {
-    padding = 0;
-  }
+  const delimiter = "\n      "; // 6 spaces
 
   let metadata = "";
 
   // we need to serialize the "original" elements before we put them through
   // the tempScene hack which duplicates and regenerates ids
-  if (exportEmbedScene) {
+  if (appState.exportEmbedScene) {
     try {
       metadata = (await import("../data/image")).encodeSvgMetadata({
         // when embedding scene, we want to embed the origionally supplied
         // elements which don't contain the temp frame labels.
         // But it also requires that the exportToSvg is being supplied with
         // only the elements that we're exporting, and no extra.
-        text: serializeAsJSON(
-          elements,
-          data.appState,
-          data.files || {},
-          "local",
-        ),
+        text: serializeAsJSON(elements, appState, data.files || {}, "local"),
       });
     } catch (error: any) {
       console.error(error);
     }
   }
 
-  let [minX, minY, width, height] = getCanvasSize(
-    cfg.exportingFrame
-      ? [cfg.exportingFrame]
-      : getRootElements(elementsForRender),
-  );
-
-  width += padding * 2;
-  height += padding * 2;
-
-  // initialize SVG root
-  const svgRoot = document.createElementNS(SVG_NS, "svg");
-  svgRoot.setAttribute("version", "1.1");
-  svgRoot.setAttribute("xmlns", SVG_NS);
-  svgRoot.setAttribute("viewBox", `0 0 ${width} ${height}`);
-  svgRoot.setAttribute("width", `${width * exportScale}`);
-  svgRoot.setAttribute("height", `${height * exportScale}`);
-  if (exportWithDarkMode) {
-    svgRoot.setAttribute("filter", THEME_FILTER);
+  let exportContentClipPath = "";
+  if (cfg.width != null && cfg.height != null) {
+    exportContentClipPath = `<clipPath id="content">
+      <rect x="${offsetX}" y="${offsetY}" width="${exportWidth}" height="${exportWidth}"></rect>
+    </clipPath>`;
   }
 
-  const offsetX = -minX + padding;
-  const offsetY = -minY + padding;
-
-  const frameElements = getFrameLikeElements(elements);
-
   let exportingFrameClipPath = "";
   const elementsMap = arrayToMap(elements);
+  const frameElements = getFrameLikeElements(elements);
   for (const frame of frameElements) {
     const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame, elementsMap);
     const cx = (x2 - x1) / 2 - (frame.x - x1);
@@ -844,36 +811,33 @@ export const exportToSvg = async ({
         </clipPath>`;
   }
 
-  const fontFaces = !cfg?.skipInliningFonts
-    ? await Fonts.generateFontFaceDeclarations(elements)
-    : [];
-
-  const delimiter = "\n      "; // 6 spaces
-
   svgRoot.innerHTML = `
   ${SVG_EXPORT_TAG}
   ${metadata}
   <defs>
-    <style class="style-fonts">${delimiter}${fontFaces.join(delimiter)}
-    </style>
+    <style class="style-fonts">${delimiter}${fontFaces.join(delimiter)}</style>
+    ${exportContentClipPath}
     ${exportingFrameClipPath}
   </defs>
   `;
 
   // render background rect
-  if (data.appState.exportBackground && viewBackgroundColor) {
+  if (appState.exportBackground && appState.viewBackgroundColor) {
     const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect");
     rect.setAttribute("x", "0");
     rect.setAttribute("y", "0");
-    rect.setAttribute("width", `${width}`);
-    rect.setAttribute("height", `${height}`);
-    rect.setAttribute("fill", viewBackgroundColor);
+    rect.setAttribute("width", `${exportWidth / exportScale}`);
+    rect.setAttribute("height", `${exportHeight / exportScale}`);
+    rect.setAttribute(
+      "fill",
+      cfg.canvasBackgroundColor || appState.viewBackgroundColor,
+    );
     svgRoot.appendChild(rect);
   }
 
   const rsvg = rough.svg(svgRoot);
 
-  const renderEmbeddables = cfg.renderEmbeddables ?? false;
+  // const renderEmbeddables = appState.embe ?? false;
 
   renderSceneToSvg(
     elementsForRender,
@@ -885,18 +849,18 @@ export const exportToSvg = async ({
       offsetX,
       offsetY,
       isExporting: true,
-      exportWithDarkMode,
-      renderEmbeddables,
+      exportWithDarkMode: cfg.theme === THEME.DARK,
+      renderEmbeddables: false,
       frameRendering,
-      canvasBackgroundColor: viewBackgroundColor,
-      embedsValidationStatus: renderEmbeddables
+      canvasBackgroundColor: appState.viewBackgroundColor,
+      embedsValidationStatus: false
         ? new Map(
             elementsForRender
               .filter((element) => isFrameLikeElement(element))
               .map((element) => [element.id, true]),
           )
         : new Map(),
-      reuseImages: cfg?.reuseImages ?? true,
+      reuseImages: true,
     },
   );
 
@@ -916,7 +880,7 @@ export const getCanvasSize = (
 
 export { MIME_TYPES };
 
-type ExportToBlobConfig = ExportToCanvasConfig & {
+type ExportToBlobConfig = ExportSceneConfig & {
   mimeType?: string;
   quality?: number;
 };
@@ -925,7 +889,7 @@ export const exportToBlob = async ({
   data,
   config,
 }: {
-  data: ExportToCanvasData;
+  data: ExportSceneData;
   config?: ExportToBlobConfig;
 }): Promise<Blob> => {
   let { mimeType = MIME_TYPES.png, quality } = config || {};
@@ -990,7 +954,7 @@ export const exportToClipboard = async ({
   data,
   config,
 }: {
-  data: ExportToCanvasData;
+  data: ExportSceneData;
 } & (
   | { type: "png"; config?: ExportToBlobConfig }
   | { type: "svg"; config?: ExportToSvgConfig }

+ 1 - 1
packages/excalidraw/tests/__snapshots__/MermaidToExcalidraw.test.tsx.snap

@@ -6,7 +6,7 @@ exports[`Test <MermaidToExcalidraw/> > should open mermaid popup when active too
  B --&gt; C{Let me think}
  C --&gt;|One| D[Laptop]
  C --&gt;|Two| E[iPhone]
- C --&gt;|Three| F[Car]</textarea><div class="ttd-dialog-panel-button-container invisible" style="display: flex; align-items: center;"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class=""></div></button></div></div><div class="ttd-dialog-panel"><div class="ttd-dialog-panel__header"><label>Preview</label></div><div class="ttd-dialog-output-wrapper"><div style="opacity: 1;" class="ttd-dialog-output-canvas-container"><canvas width="9" height="0" dir="ltr"></canvas></div></div><div class="ttd-dialog-panel-button-container" style="display: flex; align-items: center;"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class="">Insert<span><svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 20 20" class="" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><g stroke-width="1.25"><path d="M4.16602 10H15.8327"></path><path d="M12.5 13.3333L15.8333 10"></path><path d="M12.5 6.66666L15.8333 9.99999"></path></g></svg></span></div></button><div class="ttd-dialog-submit-shortcut"><div class="ttd-dialog-submit-shortcut__key">Ctrl</div><div class="ttd-dialog-submit-shortcut__key">Enter</div></div></div></div></div></div></div></div></div></div></div>"
+ C --&gt;|Three| F[Car]</textarea><div class="ttd-dialog-panel-button-container invisible" style="display: flex; align-items: center;"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class=""></div></button></div></div><div class="ttd-dialog-panel"><div class="ttd-dialog-panel__header"><label>Preview</label></div><div class="ttd-dialog-output-wrapper"><div style="opacity: 1;" class="ttd-dialog-output-canvas-container"><canvas width="300" height="0" dir="ltr"></canvas></div></div><div class="ttd-dialog-panel-button-container" style="display: flex; align-items: center;"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class="">Insert<span><svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 20 20" class="" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><g stroke-width="1.25"><path d="M4.16602 10H15.8327"></path><path d="M12.5 13.3333L15.8333 10"></path><path d="M12.5 6.66666L15.8333 9.99999"></path></g></svg></span></div></button><div class="ttd-dialog-submit-shortcut"><div class="ttd-dialog-submit-shortcut__key">Ctrl</div><div class="ttd-dialog-submit-shortcut__key">Enter</div></div></div></div></div></div></div></div></div></div></div>"
 `;
 
 exports[`Test <MermaidToExcalidraw/> > should show error in preview when mermaid library throws error 1`] = `

+ 2 - 2
packages/excalidraw/tests/__snapshots__/export.test.tsx.snap

@@ -6,8 +6,8 @@ exports[`export > exporting svg containing transformed images > svg export outpu
   
   <defs>
     <style class="style-fonts">
-      
-    </style>
+      </style>
+    
     
   </defs>
   <clipPath id="image-clipPath-id1" data-id="id1"><rect width="100" height="100" rx="25" ry="25"></rect></clipPath><g transform="translate(20.710678118654755 20.710678118654755) rotate(315 50 50)" clip-path="url(#image-clipPath-id1)" data-id="id1"><use href="#image-file_A" width="100" height="100" opacity="1"></use></g><clipPath id="image-clipPath-id2" data-id="id2"><rect width="50" height="50" rx="12.5" ry="12.5"></rect></clipPath><g transform="translate(120.71067811865476 20.710678118654755) rotate(45 25 25)" clip-path="url(#image-clipPath-id2)" data-id="id2"><use href="#image-file_A" width="50" height="50" opacity="1" transform="translate(25 25) scale(-1 1) translate(-25 -25)"></use></g><clipPath id="image-clipPath-id3" data-id="id3"><rect width="100" height="100" rx="25" ry="25"></rect></clipPath><g transform="translate(20.710678118654755 120.71067811865476) rotate(45 50 50)" clip-path="url(#image-clipPath-id3)" data-id="id3"><use href="#image-file_A" width="100" height="100" opacity="1" transform="translate(50 50) scale(1 -1) translate(-50 -50)"></use></g><clipPath id="image-clipPath-id4" data-id="id4"><rect width="50" height="50" rx="12.5" ry="12.5"></rect></clipPath><g transform="translate(120.71067811865476 120.71067811865476) rotate(315 25 25)" clip-path="url(#image-clipPath-id4)" data-id="id4"><use href="#image-file_A" width="50" height="50" opacity="1" transform="translate(25 25) scale(-1 -1) translate(-25 -25)"></use></g></svg>"

文件差异内容过多而无法显示
+ 0 - 4
packages/excalidraw/tests/scene/__snapshots__/export.test.ts.snap


+ 4 - 4
packages/excalidraw/tests/scene/export.test.ts

@@ -117,9 +117,7 @@ describe("exportToSvg", () => {
       },
     });
 
-    expect(svgElement.getAttribute("filter")).toMatchInlineSnapshot(
-      `"_themeFilter_1883f3"`,
-    );
+    expect(svgElement.getAttribute("filter")).toMatchInlineSnapshot(`null`);
   });
 
   it("with exportPadding", async () => {
@@ -149,10 +147,12 @@ describe("exportToSvg", () => {
         elements: ELEMENTS,
         appState: {
           ...DEFAULT_OPTIONS,
-          exportScale: SCALE,
         },
         files: null,
       },
+      config: {
+        scale: SCALE,
+      },
     });
 
     expect(svgElement).toHaveAttribute(

部分文件因为文件数量过多而无法显示