2
0
Эх сурвалжийг харах

unify exports in a single place

Ryan Di 7 сар өмнө
parent
commit
9da26fb7e0

+ 4 - 6
excalidraw-app/App.tsx

@@ -25,6 +25,7 @@ import {
   TTDDialogTrigger,
   StoreAction,
   reconcileElements,
+  exportToCanvas,
 } from "../packages/excalidraw";
 import {
   exportToBlob,
@@ -133,7 +134,6 @@ import { AIComponents } from "./components/AI";
 import { ExcalidrawPlusIframeExport } from "./ExcalidrawPlusIframeExport";
 import { fileSave } from "../packages/excalidraw/data/filesystem";
 import type { ExportToCanvasConfig } from "../packages/excalidraw/scene/export";
-import { exportToCanvas } from "../packages/utils";
 
 polyfill();
 
@@ -646,8 +646,6 @@ const ExcalidrawWrapper = () => {
         (el) => el.strokeStyle === "dashed" && !el.isDeleted,
       );
 
-      const zoom = appState.zoom.value;
-
       exportToCanvas({
         data: {
           elements: getNonDeletedElements(elements).filter(
@@ -1401,7 +1399,7 @@ const ExcalidrawWrapper = () => {
               maxWH{" "}
               <input
                 type="number"
-                max={600}
+                // max={600}
                 style={{ width: "3rem" }}
                 value={config.maxWidthOrHeight}
                 onChange={(event) =>
@@ -1409,7 +1407,7 @@ const ExcalidrawWrapper = () => {
                     ...s,
                     maxWidthOrHeight: !event.target.value.trim()
                       ? undefined
-                      : Math.min(parseInt(event.target.value as any), 600),
+                      : parseInt(event.target.value as any),
                   }))
                 }
               />
@@ -1440,7 +1438,7 @@ const ExcalidrawWrapper = () => {
             exportToBlob({
               data: {
                 elements: excalidrawAPI!.getSceneElements(),
-                files: null,
+                files: excalidrawAPI?.getFiles() || null,
               },
               config,
             }).then((blob) => {

+ 1 - 1
packages/excalidraw/components/ImageExportDialog.tsx

@@ -23,7 +23,6 @@ import { nativeFileSystemSupported } from "../data/filesystem";
 import type { NonDeletedExcalidrawElement } from "../element/types";
 import { t } from "../i18n";
 import { isSomeElementSelected } from "../scene";
-import { exportToCanvas } from "../../utils/export";
 
 import { copyIcon, downloadIcon, helpIcon } from "./icons";
 import { Dialog } from "./Dialog";
@@ -36,6 +35,7 @@ import { FilledButton } from "./FilledButton";
 import { cloneJSON } from "../utils";
 import { prepareElementsForExport } from "../data";
 import { useCopyStatus } from "../hooks/useCopiedIndicator";
+import { exportToCanvas } from "../scene/export";
 
 const supportsContextFilters =
   "filter" in document.createElement("canvas").getContext("2d")!;

+ 1 - 1
packages/excalidraw/components/PublishLibrary.tsx

@@ -7,7 +7,6 @@ import { t } from "../i18n";
 import Trans from "./Trans";
 
 import type { LibraryItems, LibraryItem, UIAppState } from "../types";
-import { exportToCanvas, exportToSvg } from "../../utils/export";
 import {
   COLOR_WHITE,
   EDITOR_LS_KEYS,
@@ -25,6 +24,7 @@ import { ToolButton } from "./ToolButton";
 import { EditorLocalStorage } from "../data/EditorLocalStorage";
 
 import "./PublishLibrary.scss";
+import { exportToCanvas, exportToSvg } from "../scene/export";
 
 interface PublishLibraryDataParams {
   authorName: string;

+ 1 - 1
packages/excalidraw/hooks/useLibraryItemSvg.ts

@@ -2,8 +2,8 @@ import { atom, useAtom } from "jotai";
 import { useEffect, useState } from "react";
 import { COLOR_PALETTE } from "../colors";
 import { jotaiScope } from "../jotai";
-import { exportToSvg } from "../../utils/export";
 import type { LibraryItem } from "../types";
+import { exportToSvg } from "../scene/export";
 
 export type SvgCache = Map<LibraryItem["id"], SVGSVGElement>;
 

+ 6 - 7
packages/excalidraw/index.tsx

@@ -225,12 +225,6 @@ export {
 
 export { reconcileElements } from "./data/reconcile";
 
-export {
-  exportToBlob,
-  exportToSvg,
-  exportToClipboard,
-} from "../utils/export";
-
 export { serializeAsJSON, serializeLibraryAsJSON } from "./data/json";
 export {
   loadFromBlob,
@@ -273,7 +267,12 @@ export { WelcomeScreen };
 export { LiveCollaborationTrigger };
 export { Stats } from "./components/Stats";
 
-export { exportToCanvas } from "./scene/export";
+export {
+  exportToCanvas,
+  exportToBlob,
+  exportToClipboard,
+  exportToSvg,
+} from "./scene/export";
 
 export { DefaultSidebar } from "./components/DefaultSidebar";
 export { TTDDialog } from "./components/TTDDialog/TTDDialog";

+ 1 - 1
packages/excalidraw/renderer/helpers.ts

@@ -1,4 +1,4 @@
-import type { StaticCanvasAppState, AppState } from "../types";
+import type { AppState } from "../types";
 
 import type { StaticCanvasRenderConfig } from "../scene/types";
 

+ 141 - 20
packages/excalidraw/scene/export.ts

@@ -21,6 +21,7 @@ import {
   SVG_NS,
   THEME,
   THEME_FILTER,
+  MIME_TYPES,
 } from "../constants";
 import { getDefaultAppState } from "../appState";
 import { serializeAsJSON } from "../data/json";
@@ -28,14 +29,14 @@ import {
   getInitializedImageElements,
   updateImageCache,
 } from "../element/image";
-import { restoreAppState } from "../data/restore";
+import { restore, restoreAppState } from "../data/restore";
 import {
   getElementsOverlappingFrame,
   getFrameLikeElements,
   getFrameLikeTitle,
   getRootElements,
 } from "../frame";
-import { newTextElement } from "../element";
+import { getNonDeletedElements, newTextElement } from "../element";
 import { type Mutable } from "../utility-types";
 import { newElementWith } from "../element/mutateElement";
 import { isFrameLikeElement } from "../element/typeChecks";
@@ -43,6 +44,12 @@ import type { RenderableElementsMap } from "./types";
 import { syncInvalidIndices } from "../fractionalIndex";
 import { renderStaticScene } from "../renderer/staticScene";
 import { Fonts } from "../fonts";
+import { encodePngMetadata } from "../data/image";
+import {
+  copyBlobToClipboardAsPng,
+  copyTextToSystemClipboard,
+  copyToClipboard,
+} from "../clipboard";
 
 const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
 
@@ -153,9 +160,13 @@ const prepareElementsForRender = ({
   return nextElements;
 };
 
+type ExportToCanvasAppState = Partial<
+  Omit<AppState, "offsetTop" | "offsetLeft">
+>;
+
 export type ExportToCanvasData = {
   elements: readonly NonDeletedExcalidrawElement[];
-  appState?: Partial<Omit<AppState, "offsetTop" | "offsetLeft">>;
+  appState?: ExportToCanvasAppState;
   files: BinaryFiles | null;
 };
 
@@ -632,6 +643,18 @@ export const exportToCanvas = async ({
   return canvas;
 };
 
+type ExportToSvgConfig = Pick<
+  ExportToCanvasConfig,
+  "canvasBackgroundColor" | "padding" | "theme" | "exportingFrame"
+> & {
+  /**
+   * if true, all embeddables passed in will be rendered when possible.
+   */
+  renderEmbeddables?: boolean;
+  skipInliningFonts?: true;
+  reuseImages?: boolean;
+};
+
 export const exportToSvg = async ({
   data,
   config,
@@ -640,31 +663,28 @@ export const exportToSvg = async ({
     elements: readonly NonDeletedExcalidrawElement[];
     appState: {
       exportBackground: boolean;
-      exportPadding?: number;
       exportScale?: number;
       viewBackgroundColor: string;
       exportWithDarkMode?: boolean;
       exportEmbedScene?: boolean;
       frameRendering?: AppState["frameRendering"];
+      gridModeEnabled?: boolean;
     };
     files: BinaryFiles | null;
   };
-  config?: {
-    /**
-     * if true, all embeddables passed in will be rendered when possible.
-     */
-    renderEmbeddables?: boolean;
-    exportingFrame?: ExcalidrawFrameLikeElement | null;
-    skipInliningFonts?: true;
-    reuseImages?: boolean;
-  };
+  config?: ExportToSvgConfig;
 }): Promise<SVGSVGElement> => {
   // clone
   const cfg = Object.assign({}, config);
 
   cfg.exportingFrame = cfg.exportingFrame ?? null;
 
-  const elements = data.elements;
+  const { elements: restoredElements } = restore(
+    { ...data, files: data.files || {} },
+    null,
+    null,
+  );
+  const elements = getNonDeletedElements(restoredElements);
 
   const frameRendering = getFrameRenderingConfig(
     cfg?.exportingFrame ?? null,
@@ -672,13 +692,14 @@ export const exportToSvg = async ({
   );
 
   let {
-    exportPadding = DEFAULT_EXPORT_PADDING,
     exportWithDarkMode = false,
     viewBackgroundColor,
     exportScale = 1,
     exportEmbedScene,
   } = data.appState;
 
+  let padding = cfg.padding ?? 0;
+
   const elementsForRender = prepareElementsForRender({
     elements,
     exportingFrame: cfg.exportingFrame,
@@ -687,7 +708,7 @@ export const exportToSvg = async ({
   });
 
   if (cfg.exportingFrame) {
-    exportPadding = 0;
+    padding = 0;
   }
 
   let metadata = "";
@@ -719,8 +740,8 @@ export const exportToSvg = async ({
       : getRootElements(elementsForRender),
   );
 
-  width += exportPadding * 2;
-  height += exportPadding * 2;
+  width += padding * 2;
+  height += padding * 2;
 
   // initialize SVG root
   const svgRoot = document.createElementNS(SVG_NS, "svg");
@@ -733,8 +754,8 @@ export const exportToSvg = async ({
     svgRoot.setAttribute("filter", THEME_FILTER);
   }
 
-  const offsetX = -minX + exportPadding;
-  const offsetY = -minY + exportPadding;
+  const offsetX = -minX + padding;
+  const offsetY = -minY + padding;
 
   const frameElements = getFrameLikeElements(elements);
 
@@ -830,3 +851,103 @@ export const getCanvasSize = (
 
   return [minX, minY, width, height];
 };
+
+export { MIME_TYPES };
+
+type ExportToBlobConfig = ExportToCanvasConfig & {
+  mimeType?: string;
+  quality?: number;
+};
+
+export const exportToBlob = async ({
+  data,
+  config,
+}: {
+  data: ExportToCanvasData;
+  config?: ExportToBlobConfig;
+}): Promise<Blob> => {
+  let { mimeType = MIME_TYPES.png, quality } = config || {};
+
+  if (mimeType === MIME_TYPES.png && typeof quality === "number") {
+    console.warn(`"quality" will be ignored for "${MIME_TYPES.png}" mimeType`);
+  }
+
+  // typo in MIME type (should be "jpeg")
+  if (mimeType === "image/jpg") {
+    mimeType = MIME_TYPES.jpg;
+  }
+
+  if (mimeType === MIME_TYPES.jpg && !config?.canvasBackgroundColor === false) {
+    console.warn(
+      `Defaulting "exportBackground" to "true" for "${MIME_TYPES.jpg}" mimeType`,
+    );
+    config = {
+      ...config,
+      canvasBackgroundColor: data.appState?.viewBackgroundColor || COLOR_WHITE,
+    };
+  }
+
+  const canvas = await exportToCanvas({ data, config });
+
+  quality = quality ? quality : /image\/jpe?g/.test(mimeType) ? 0.92 : 0.8;
+
+  return new Promise((resolve, reject) => {
+    canvas.toBlob(
+      async (blob) => {
+        if (!blob) {
+          return reject(new Error("couldn't export to blob"));
+        }
+        if (
+          blob &&
+          mimeType === MIME_TYPES.png &&
+          data.appState?.exportEmbedScene
+        ) {
+          blob = await encodePngMetadata({
+            blob,
+            metadata: serializeAsJSON(
+              // NOTE as long as we're using the Scene hack, we need to ensure
+              // we pass the original, uncloned elements when serializing
+              // so that we keep ids stable
+              data.elements,
+              data.appState,
+              data.files || {},
+              "local",
+            ),
+          });
+        }
+        resolve(blob);
+      },
+      mimeType,
+      quality,
+    );
+  });
+};
+
+export const exportToClipboard = async ({
+  type,
+  data,
+  config,
+}: {
+  data: ExportToCanvasData;
+} & (
+  | { type: "png"; config?: ExportToBlobConfig }
+  | { type: "svg"; config?: ExportToSvgConfig }
+  | { type: "json"; config?: never }
+)) => {
+  if (type === "svg") {
+    const svg = await exportToSvg({
+      data: {
+        ...data,
+        appState: restoreAppState(data.appState, null),
+      },
+      config,
+    });
+    await copyTextToSystemClipboard(svg.outerHTML);
+  } else if (type === "png") {
+    await copyBlobToClipboardAsPng(exportToBlob({ data, config }));
+  } else if (type === "json") {
+    await copyToClipboard(data.elements, data.files);
+  } else {
+    throw new Error("Invalid export type");
+  }
+};

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
packages/excalidraw/tests/__snapshots__/export.test.tsx.snap


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 6 - 6
packages/excalidraw/tests/scene/__snapshots__/export.test.ts.snap


+ 75 - 18
packages/excalidraw/tests/scene/export.test.ts

@@ -11,9 +11,15 @@ import {
   textFixture,
 } from "../fixtures/elementFixture";
 import { API } from "../helpers/api";
-import { exportToCanvas, exportToSvg } from "../../../utils";
 import { FONT_FAMILY, FRAME_STYLE } from "../../constants";
 import { prepareElementsForExport } from "../../data";
+import { diagramFactory } from "../fixtures/diagramFixture";
+import { vi } from "vitest";
+
+const DEFAULT_OPTIONS = {
+  exportBackground: false,
+  viewBackgroundColor: "#ffffff",
+};
 
 describe("exportToSvg", () => {
   const ELEMENT_HEIGHT = 100;
@@ -46,11 +52,6 @@ describe("exportToSvg", () => {
     },
   ] as NonDeletedExcalidrawElement[];
 
-  const DEFAULT_OPTIONS = {
-    exportBackground: false,
-    viewBackgroundColor: "#ffffff",
-  };
-
   it("with default arguments", async () => {
     const svgElement = await exportUtils.exportToSvg({
       data: { elements: ELEMENTS, appState: DEFAULT_OPTIONS, files: null },
@@ -127,7 +128,6 @@ describe("exportToSvg", () => {
         elements: ELEMENTS,
         appState: {
           ...DEFAULT_OPTIONS,
-          exportPadding: 0,
         },
         files: null,
       },
@@ -149,7 +149,6 @@ describe("exportToSvg", () => {
         elements: ELEMENTS,
         appState: {
           ...DEFAULT_OPTIONS,
-          exportPadding: 0,
           exportScale: SCALE,
         },
         files: null,
@@ -226,7 +225,7 @@ describe("exporting frames", () => {
         }),
       ];
 
-      const canvas = await exportToCanvas({
+      const canvas = await exportUtils.exportToCanvas({
         data: {
           elements,
           files: null,
@@ -259,7 +258,7 @@ describe("exporting frames", () => {
         }),
       ];
 
-      const canvas = await exportToCanvas({
+      const canvas = await exportUtils.exportToCanvas({
         data: {
           elements,
           files: null,
@@ -302,8 +301,12 @@ describe("exporting frames", () => {
         y: 0,
       });
 
-      const svg = await exportToSvg({
-        data: { elements: [rectOverlapping, frame, frameChild], files: null },
+      const svg = await exportUtils.exportToSvg({
+        data: {
+          elements: [rectOverlapping, frame, frameChild],
+          files: null,
+          appState: DEFAULT_OPTIONS,
+        },
         config: {
           padding: 0,
           exportingFrame: frame,
@@ -347,8 +350,12 @@ describe("exporting frames", () => {
         y: 0,
       });
 
-      const svg = await exportToSvg({
-        data: { elements: [frameChild, frame, elementOutside], files: null },
+      const svg = await exportUtils.exportToSvg({
+        data: {
+          elements: [frameChild, frame, elementOutside],
+          files: null,
+          appState: DEFAULT_OPTIONS,
+        },
         config: {
           padding: 0,
           exportingFrame: frame,
@@ -416,8 +423,12 @@ describe("exporting frames", () => {
         true,
       );
 
-      const svg = await exportToSvg({
-        data: { elements: exportedElements, files: null },
+      const svg = await exportUtils.exportToSvg({
+        data: {
+          elements: exportedElements,
+          files: null,
+          appState: DEFAULT_OPTIONS,
+        },
         config: {
           padding: 0,
           exportingFrame,
@@ -462,10 +473,11 @@ describe("exporting frames", () => {
         false,
       );
 
-      const svg = await exportToSvg({
+      const svg = await exportUtils.exportToSvg({
         data: {
           elements: exportedElements,
           files: null,
+          appState: DEFAULT_OPTIONS,
         },
         config: {
           padding: 0,
@@ -525,10 +537,11 @@ describe("exporting frames", () => {
         true,
       );
 
-      const svg = await exportToSvg({
+      const svg = await exportUtils.exportToSvg({
         data: {
           elements: exportedElements,
           files: null,
+          appState: DEFAULT_OPTIONS,
         },
         config: {
           padding: 0,
@@ -549,3 +562,47 @@ describe("exporting frames", () => {
     });
   });
 });
+
+describe("exportToBlob", async () => {
+  describe("mime type", () => {
+    it("should change image/jpg to image/jpeg", async () => {
+      const blob = await exportUtils.exportToBlob({
+        data: {
+          ...diagramFactory(),
+
+          appState: {
+            exportBackground: true,
+          },
+        },
+        config: {
+          getDimensions: (width, height) => ({ width, height, scale: 1 }),
+          // testing typo in MIME type (jpg → jpeg)
+          mimeType: "image/jpg",
+        },
+      });
+      expect(blob?.type).toBe(exportUtils.MIME_TYPES.jpg);
+    });
+    it("should default to image/png", async () => {
+      const blob = await exportUtils.exportToBlob({
+        data: diagramFactory(),
+      });
+      expect(blob?.type).toBe(exportUtils.MIME_TYPES.png);
+    });
+
+    it("should warn when using quality with image/png", async () => {
+      const consoleSpy = vi
+        .spyOn(console, "warn")
+        .mockImplementationOnce(() => void 0);
+      await exportUtils.exportToBlob({
+        data: diagramFactory(),
+        config: {
+          mimeType: exportUtils.MIME_TYPES.png,
+          quality: 1,
+        },
+      });
+      expect(consoleSpy).toHaveBeenCalledWith(
+        `"quality" will be ignored for "${exportUtils.MIME_TYPES.png}" mimeType`,
+      );
+    });
+  });
+});

+ 0 - 144
packages/utils/export.test.ts

@@ -1,144 +0,0 @@
-import * as utils from ".";
-import { diagramFactory } from "../excalidraw/tests/fixtures/diagramFixture";
-import { vi } from "vitest";
-import * as mockedSceneExportUtils from "../excalidraw/scene/export";
-
-import { MIME_TYPES } from "../excalidraw/constants";
-
-import { exportToCanvas } from "../excalidraw/scene/export";
-const exportToSvgSpy = vi.spyOn(mockedSceneExportUtils, "exportToSvg");
-
-describe("exportToCanvas", async () => {
-  it("with default arguments", async () => {
-    const canvas = await exportToCanvas({
-      data: diagramFactory({ elementOverrides: { width: 100, height: 100 } }),
-    });
-
-    expect(canvas.width).toBe(100);
-    expect(canvas.height).toBe(100);
-  });
-
-  it("when custom width and height", async () => {
-    const canvas = await exportToCanvas({
-      data: {
-        ...diagramFactory({ elementOverrides: { width: 100, height: 100 } }),
-      },
-      config: {
-        getDimensions: () => ({ width: 200, height: 200, scale: 1 }),
-      },
-    });
-
-    expect(canvas.width).toBe(200);
-    expect(canvas.height).toBe(200);
-  });
-});
-
-describe("exportToBlob", async () => {
-  describe("mime type", () => {
-    it("should change image/jpg to image/jpeg", async () => {
-      const blob = await utils.exportToBlob({
-        data: {
-          ...diagramFactory(),
-
-          appState: {
-            exportBackground: true,
-          },
-        },
-        config: {
-          getDimensions: (width, height) => ({ width, height, scale: 1 }),
-          // testing typo in MIME type (jpg → jpeg)
-          mimeType: "image/jpg",
-        },
-      });
-      expect(blob?.type).toBe(MIME_TYPES.jpg);
-    });
-    it("should default to image/png", async () => {
-      const blob = await utils.exportToBlob({
-        data: diagramFactory(),
-      });
-      expect(blob?.type).toBe(MIME_TYPES.png);
-    });
-
-    it("should warn when using quality with image/png", async () => {
-      const consoleSpy = vi
-        .spyOn(console, "warn")
-        .mockImplementationOnce(() => void 0);
-      await utils.exportToBlob({
-        data: diagramFactory(),
-        config: {
-          mimeType: MIME_TYPES.png,
-          quality: 1,
-        },
-      });
-      expect(consoleSpy).toHaveBeenCalledWith(
-        `"quality" will be ignored for "${MIME_TYPES.png}" mimeType`,
-      );
-    });
-  });
-});
-
-describe("exportToSvg", () => {
-  const passedElements = () => exportToSvgSpy.mock.calls[0][0].data.elements;
-  const passedOptions = () => exportToSvgSpy.mock.calls[0][0].data.appState;
-
-  afterEach(() => {
-    vi.clearAllMocks();
-  });
-
-  it("with default arguments", async () => {
-    await utils.exportToSvg({
-      data: diagramFactory({
-        overrides: { appState: void 0 },
-      }),
-    });
-
-    const passedOptionsWhenDefault = {
-      ...passedOptions(),
-      // To avoid varying snapshots
-      name: "name",
-    };
-    expect(passedElements().length).toBe(3);
-    expect(passedOptionsWhenDefault).toMatchSnapshot();
-  });
-
-  // FIXME the utils.exportToSvg no longer filters out deleted elements.
-  // It's already supposed to be passed non-deleted elements by we're not
-  // type-checking for it correctly.
-  it.skip("with deleted elements", async () => {
-    await utils.exportToSvg({
-      data: diagramFactory({
-        overrides: { appState: void 0 },
-        elementOverrides: { isDeleted: true },
-      }),
-    });
-
-    expect(passedElements().length).toBe(0);
-  });
-
-  it("with exportPadding", async () => {
-    await utils.exportToSvg({
-      data: diagramFactory({
-        overrides: { appState: { name: "diagram name" } },
-      }),
-      config: { padding: 0 },
-    });
-
-    expect(passedElements().length).toBe(3);
-    expect(passedOptions()).toEqual(
-      expect.objectContaining({ exportPadding: 0 }),
-    );
-  });
-
-  it("with exportEmbedScene", async () => {
-    await utils.exportToSvg({
-      data: diagramFactory({
-        overrides: {
-          appState: { name: "diagram name", exportEmbedScene: true },
-        },
-      }),
-    });
-
-    expect(passedElements().length).toBe(3);
-    expect(passedOptions().exportEmbedScene).toBe(true);
-  });
-});

+ 0 - 163
packages/utils/export.ts

@@ -1,163 +0,0 @@
-import {
-  exportToCanvas as _exportToCanvas,
-  type ExportToCanvasConfig,
-  type ExportToCanvasData,
-  exportToSvg as _exportToSvg,
-} from "../excalidraw/scene/export";
-import { restore } from "../excalidraw/data/restore";
-import { COLOR_WHITE, MIME_TYPES } from "../excalidraw/constants";
-import { encodePngMetadata } from "../excalidraw/data/image";
-import { serializeAsJSON } from "../excalidraw/data/json";
-import {
-  copyBlobToClipboardAsPng,
-  copyTextToSystemClipboard,
-  copyToClipboard,
-} from "../excalidraw/clipboard";
-import { getNonDeletedElements } from "../excalidraw";
-
-export { MIME_TYPES };
-
-type ExportToBlobConfig = ExportToCanvasConfig & {
-  mimeType?: string;
-  quality?: number;
-};
-
-type ExportToSvgConfig = Pick<
-  ExportToCanvasConfig,
-  "canvasBackgroundColor" | "padding" | "theme" | "exportingFrame"
-> & {
-  /**
-   * if true, all embeddables passed in will be rendered when possible.
-   */
-  renderEmbeddables?: boolean;
-  skipInliningFonts?: true;
-  reuseImages?: boolean;
-};
-
-export const exportToCanvas = async ({
-  data,
-  config,
-}: {
-  data: ExportToCanvasData;
-  config?: ExportToCanvasConfig;
-}) => {
-  return _exportToCanvas({
-    data,
-    config,
-  });
-};
-
-export const exportToBlob = async ({
-  data,
-  config,
-}: {
-  data: ExportToCanvasData;
-  config?: ExportToBlobConfig;
-}): Promise<Blob> => {
-  let { mimeType = MIME_TYPES.png, quality } = config || {};
-
-  if (mimeType === MIME_TYPES.png && typeof quality === "number") {
-    console.warn(`"quality" will be ignored for "${MIME_TYPES.png}" mimeType`);
-  }
-
-  // typo in MIME type (should be "jpeg")
-  if (mimeType === "image/jpg") {
-    mimeType = MIME_TYPES.jpg;
-  }
-
-  if (mimeType === MIME_TYPES.jpg && !config?.canvasBackgroundColor === false) {
-    console.warn(
-      `Defaulting "exportBackground" to "true" for "${MIME_TYPES.jpg}" mimeType`,
-    );
-    config = {
-      ...config,
-      canvasBackgroundColor: data.appState?.viewBackgroundColor || COLOR_WHITE,
-    };
-  }
-
-  const canvas = await _exportToCanvas({ data, config });
-
-  quality = quality ? quality : /image\/jpe?g/.test(mimeType) ? 0.92 : 0.8;
-
-  return new Promise((resolve, reject) => {
-    canvas.toBlob(
-      async (blob) => {
-        if (!blob) {
-          return reject(new Error("couldn't export to blob"));
-        }
-        if (
-          blob &&
-          mimeType === MIME_TYPES.png &&
-          data.appState?.exportEmbedScene
-        ) {
-          blob = await encodePngMetadata({
-            blob,
-            metadata: serializeAsJSON(
-              // NOTE as long as we're using the Scene hack, we need to ensure
-              // we pass the original, uncloned elements when serializing
-              // so that we keep ids stable
-              data.elements,
-              data.appState,
-              data.files || {},
-              "local",
-            ),
-          });
-        }
-        resolve(blob);
-      },
-      mimeType,
-      quality,
-    );
-  });
-};
-
-export const exportToSvg = async ({
-  data,
-  config,
-}: {
-  data: ExportToCanvasData;
-  config?: ExportToSvgConfig;
-}): Promise<SVGSVGElement> => {
-  const { elements: restoredElements, appState: restoredAppState } = restore(
-    { ...data, files: data.files || {} },
-    null,
-    null,
-  );
-
-  const appState = { ...restoredAppState, exportPadding: config?.padding };
-  const elements = getNonDeletedElements(restoredElements);
-  const files = data.files || {};
-
-  return _exportToSvg({
-    data: { elements, appState, files },
-    config: {
-      exportingFrame: config?.exportingFrame,
-      renderEmbeddables: config?.renderEmbeddables,
-      skipInliningFonts: config?.skipInliningFonts,
-      reuseImages: config?.reuseImages,
-    },
-  });
-};
-
-export const exportToClipboard = async ({
-  type,
-  data,
-  config,
-}: {
-  data: ExportToCanvasData;
-} & (
-  | { type: "png"; config?: ExportToBlobConfig }
-  | { type: "svg"; config?: ExportToSvgConfig }
-  | { type: "json"; config?: never }
-)) => {
-  if (type === "svg") {
-    const svg = await exportToSvg({ data, config });
-    await copyTextToSystemClipboard(svg.outerHTML);
-  } else if (type === "png") {
-    await copyBlobToClipboardAsPng(exportToBlob({ data, config }));
-  } else if (type === "json") {
-    await copyToClipboard(data.elements, data.files);
-  } else {
-    throw new Error("Invalid export type");
-  }
-};

+ 0 - 1
packages/utils/index.ts

@@ -1,4 +1,3 @@
-export * from "./export";
 export * from "./withinBounds";
 export * from "./bbox";
 export { getCommonBounds } from "../excalidraw/element/bounds";

+ 4 - 3
packages/utils/utils.unmocked.test.ts

@@ -1,6 +1,6 @@
 import { decodePngMetadata, decodeSvgMetadata } from "../excalidraw/data/image";
 import type { ImportedDataState } from "../excalidraw/data/types";
-import * as utils from "../utils";
+import { exportToBlob, exportToSvg } from "../excalidraw/scene/export";
 import { API } from "../excalidraw/tests/helpers/api";
 
 // NOTE this test file is using the actual API, unmocked. Hence splitting it
@@ -15,13 +15,14 @@ describe("embedding scene data", () => {
 
       const sourceElements = [rectangle, ellipse];
 
-      const svgNode = await utils.exportToSvg({
+      const svgNode = await exportToSvg({
         data: {
           elements: sourceElements,
           appState: {
             viewBackgroundColor: "#ffffff",
             gridModeEnabled: false,
             exportEmbedScene: true,
+            exportBackground: true,
           },
           files: null,
         },
@@ -47,7 +48,7 @@ describe("embedding scene data", () => {
 
       const sourceElements = [rectangle, ellipse];
 
-      const blob = await utils.exportToBlob({
+      const blob = await exportToBlob({
         data: {
           elements: sourceElements,
           appState: {

+ 3 - 47
yarn.lock

@@ -3887,11 +3887,6 @@ ansi-regex@^5.0.1:
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
   integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
 
-ansi-regex@^6.0.1:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654"
-  integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==
-
 ansi-styles@^2.2.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
@@ -9638,7 +9633,7 @@ string-natural-compare@^3.0.1:
   resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
   integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
 
-"string-width-cjs@npm:string-width@^4.2.0":
+"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3:
   version "4.2.3"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
   integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -9656,15 +9651,6 @@ string-width@^4.1.0, string-width@^4.2.0:
     is-fullwidth-code-point "^3.0.0"
     strip-ansi "^6.0.0"
 
-string-width@^4.2.3:
-  version "4.2.3"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
-  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
-  dependencies:
-    emoji-regex "^8.0.0"
-    is-fullwidth-code-point "^3.0.0"
-    strip-ansi "^6.0.1"
-
 string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
@@ -9736,34 +9722,13 @@ stringify-object@^3.3.0:
     is-obj "^1.0.1"
     is-regexp "^1.0.0"
 
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", [email protected], strip-ansi@^3.0.0, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
   dependencies:
     ansi-regex "^5.0.1"
 
-strip-ansi@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
-  integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==
-  dependencies:
-    ansi-regex "^2.0.0"
-
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
-  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
-  dependencies:
-    ansi-regex "^5.0.1"
-
-strip-ansi@^7.0.1:
-  version "7.1.0"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
-  integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
-  dependencies:
-    ansi-regex "^6.0.1"
-
 strip-bom@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
@@ -10994,7 +10959,7 @@ [email protected], workbox-window@^7.0.0:
     "@types/trusted-types" "^2.0.2"
     workbox-core "7.1.0"
 
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
   integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -11012,15 +10977,6 @@ wrap-ansi@^6.2.0:
     string-width "^4.1.0"
     strip-ansi "^6.0.0"
 
-wrap-ansi@^7.0.0:
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
-  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
-  dependencies:
-    ansi-styles "^4.0.0"
-    string-width "^4.1.0"
-    strip-ansi "^6.0.0"
-
 wrap-ansi@^8.1.0:
   version "8.1.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно