diff --git a/src/components/LibraryUnit.tsx b/src/components/LibraryUnit.tsx index 30fc4f12..4eb6b780 100644 --- a/src/components/LibraryUnit.tsx +++ b/src/components/LibraryUnit.tsx @@ -36,21 +36,27 @@ export const LibraryUnit = ({ if (!elementsToRender) { return; } - const svg = exportToSvg(elementsToRender, { - exportBackground: false, - viewBackgroundColor: oc.white, - }); - for (const child of ref.current!.children) { - if (child.tagName !== "svg") { - continue; - } - ref.current!.removeChild(child); - } - ref.current!.appendChild(svg); - + let svg: SVGSVGElement; const current = ref.current!; + + (async () => { + svg = await exportToSvg(elementsToRender, { + exportBackground: false, + viewBackgroundColor: oc.white, + }); + for (const child of ref.current!.children) { + if (child.tagName !== "svg") { + continue; + } + current!.removeChild(child); + } + current!.appendChild(svg); + })(); + return () => { - current.removeChild(svg); + if (svg) { + current.removeChild(svg); + } }; }, [elements, pendingElements]); diff --git a/src/components/PasteChartDialog.tsx b/src/components/PasteChartDialog.tsx index 3c609211..9f42355a 100644 --- a/src/components/PasteChartDialog.tsx +++ b/src/components/PasteChartDialog.tsx @@ -34,19 +34,21 @@ const ChartPreviewBtn = (props: { 0, ); setChartElements(elements); - - const svg = exportToSvg(elements, { - exportBackground: false, - viewBackgroundColor: oc.white, - }); - + let svg: SVGSVGElement; const previewNode = previewRef.current!; - previewNode.appendChild(svg); + (async () => { + svg = await exportToSvg(elements, { + exportBackground: false, + viewBackgroundColor: oc.white, + }); - if (props.selected) { - (previewNode.parentNode as HTMLDivElement).focus(); - } + previewNode.appendChild(svg); + + if (props.selected) { + (previewNode.parentNode as HTMLDivElement).focus(); + } + })(); return () => { previewNode.removeChild(svg); diff --git a/src/data/index.ts b/src/data/index.ts index 62d7e864..c1b50d8d 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -35,20 +35,13 @@ export const exportCanvas = async ( throw new Error(t("alerts.cannotExportEmptyCanvas")); } if (type === "svg" || type === "clipboard-svg") { - const tempSvg = exportToSvg(elements, { + const tempSvg = await exportToSvg(elements, { exportBackground, exportWithDarkMode: appState.exportWithDarkMode, viewBackgroundColor, exportPadding, exportScale: appState.exportScale, - metadata: - appState.exportEmbedScene && type === "svg" - ? await ( - await import(/* webpackChunkName: "image" */ "./image") - ).encodeSvgMetadata({ - text: serializeAsJSON(elements, appState), - }) - : undefined, + exportEmbedScene: appState.exportEmbedScene && type === "svg", }); if (type === "svg") { await fileSave(new Blob([tempSvg.outerHTML], { type: "image/svg+xml" }), { @@ -57,7 +50,7 @@ export const exportCanvas = async ( }); return; } else if (type === "clipboard-svg") { - copyTextToSystemClipboard(tempSvg.outerHTML); + await copyTextToSystemClipboard(tempSvg.outerHTML); return; } } diff --git a/src/data/json.ts b/src/data/json.ts index 6ca7c75b..e1ed15de 100644 --- a/src/data/json.ts +++ b/src/data/json.ts @@ -15,7 +15,7 @@ import Library from "./library"; export const serializeAsJSON = ( elements: readonly ExcalidrawElement[], - appState: AppState, + appState: Partial, ): string => { const data: ExportedDataState = { type: EXPORT_DATA_TYPES.excalidraw, diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index 901e7ef8..dd3a5e2b 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -19,6 +19,13 @@ Please add the latest change on the top under the correct section. ### Features +- Support `appState.exportEmbedScene` attribute in [`exportToSvg`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportToSvg) which allows to embed the scene data. + + #### BREAKING CHANGE + + - The attribute `metadata` is now removed as `metadata` was only used to embed scene data which is now supported with the `appState.exportEmbedScene` attribute. + - [`exportToSvg`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportToSvg) now resolves to a promise which resolves to `svg` of the exported drawing. + - Expose [`loadLibraryFromBlob`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#loadLibraryFromBlobY), [`loadFromBlob`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#loadFromBlob), and [`getFreeDrawSvgPath`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#getFreeDrawSvgPath). - Expose [`FONT_FAMILY`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#FONT_FAMILY) so that consumer can use when passing `initialData.appState.currentItemFontFamily`. diff --git a/src/packages/excalidraw/README_NEXT.md b/src/packages/excalidraw/README_NEXT.md index 1c0c010a..1cb4d852 100644 --- a/src/packages/excalidraw/README_NEXT.md +++ b/src/packages/excalidraw/README_NEXT.md @@ -829,9 +829,8 @@ exportToSvg({ | elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L78) | | The elements to exported as svg | | appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L42) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L11) | The app state of the scene | | exportPadding | number | 10 | The padding to be added on canvas | -| metadata | string | '' | The metadata to be embedded in svg | -This function returns a svg with the exported elements. +This function returns a promise which resolves to svg of the exported drawing. ##### Additional attributes of appState for `export\*` APIs @@ -840,6 +839,7 @@ This function returns a svg with the exported elements. | exportBackground | boolean | true | Indicates whether background should be exported | | viewBackgroundColor | string | #fff | The default background color | | exportWithDarkMode | boolean | false | Indicates whether to export with dark mode | +| exportEmbedScene | boolean | false | Indicates whether scene data should be embedded in svg. This will increase the svg size. | ### FONT_FAMILY diff --git a/src/packages/utils.ts b/src/packages/utils.ts index 3fb446f6..3a21e190 100644 --- a/src/packages/utils.ts +++ b/src/packages/utils.ts @@ -74,15 +74,13 @@ export const exportToBlob = ( }); }; -export const exportToSvg = ({ +export const exportToSvg = async ({ elements, appState = getDefaultAppState(), exportPadding, - metadata, }: Omit & { exportPadding?: number; - metadata?: string; -}): SVGSVGElement => { +}): Promise => { const { elements: restoredElements, appState: restoredAppState } = restore( { elements, appState }, null, @@ -90,7 +88,6 @@ export const exportToSvg = ({ return _exportToSvg(getNonDeletedElements(restoredElements), { ...restoredAppState, exportPadding, - metadata, }); }; diff --git a/src/scene/export.ts b/src/scene/export.ts index 81823c14..dabe80f9 100644 --- a/src/scene/export.ts +++ b/src/scene/export.ts @@ -6,6 +6,7 @@ import { distance, SVG_NS } from "../utils"; import { AppState } from "../types"; import { DEFAULT_EXPORT_PADDING, THEME_FILTER } from "../constants"; import { getDefaultAppState } from "../appState"; +import { serializeAsJSON } from "../data/json"; export const SVG_EXPORT_TAG = ``; @@ -65,24 +66,35 @@ export const exportToCanvas = ( return canvas; }; -export const exportToSvg = ( +export const exportToSvg = async ( elements: readonly NonDeletedExcalidrawElement[], - { - exportBackground, - exportPadding = DEFAULT_EXPORT_PADDING, - viewBackgroundColor, - exportWithDarkMode, - exportScale = 1, - metadata = "", - }: { + appState: { exportBackground: boolean; exportPadding?: number; exportScale?: number; viewBackgroundColor: string; exportWithDarkMode?: boolean; - metadata?: string; + exportEmbedScene?: boolean; }, -): SVGSVGElement => { +): Promise => { + const { + exportPadding = DEFAULT_EXPORT_PADDING, + viewBackgroundColor, + exportScale = 1, + exportEmbedScene, + } = appState; + let metadata = ""; + if (exportEmbedScene) { + try { + metadata = await ( + await import(/* webpackChunkName: "image" */ "../../src/data/image") + ).encodeSvgMetadata({ + text: serializeAsJSON(elements, appState), + }); + } catch (err) { + console.error(err); + } + } const [minX, minY, width, height] = getCanvasSize(elements, exportPadding); // initialze SVG root @@ -92,7 +104,7 @@ export const exportToSvg = ( svgRoot.setAttribute("viewBox", `0 0 ${width} ${height}`); svgRoot.setAttribute("width", `${width * exportScale}`); svgRoot.setAttribute("height", `${height * exportScale}`); - if (exportWithDarkMode) { + if (appState.exportWithDarkMode) { svgRoot.setAttribute("filter", THEME_FILTER); } @@ -114,7 +126,7 @@ export const exportToSvg = ( `; // render background rect - if (exportBackground && viewBackgroundColor) { + if (appState.exportBackground && viewBackgroundColor) { const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect"); rect.setAttribute("x", "0"); rect.setAttribute("y", "0"); diff --git a/src/tests/packages/__snapshots__/utils.test.ts.snap b/src/tests/packages/__snapshots__/utils.test.ts.snap index 3fd22f6d..81f142f3 100644 --- a/src/tests/packages/__snapshots__/utils.test.ts.snap +++ b/src/tests/packages/__snapshots__/utils.test.ts.snap @@ -39,7 +39,6 @@ Object { "isResizing": false, "isRotating": false, "lastPointerDownWith": "mouse", - "metadata": undefined, "multiElement": null, "name": "name", "openMenu": null, diff --git a/src/tests/packages/utils.test.ts b/src/tests/packages/utils.test.ts index 1eaad109..cf7821e3 100644 --- a/src/tests/packages/utils.test.ts +++ b/src/tests/packages/utils.test.ts @@ -73,11 +73,10 @@ describe("exportToSvg", () => { const mockedExportUtil = mockedSceneExportUtils.exportToSvg as jest.Mock; const passedElements = () => mockedExportUtil.mock.calls[0][0]; const passedOptions = () => mockedExportUtil.mock.calls[0][1]; - afterEach(jest.resetAllMocks); - it("with default arguments", () => { - utils.exportToSvg({ + it("with default arguments", async () => { + await utils.exportToSvg({ ...diagramFactory({ overrides: { appState: void 0 }, }), @@ -88,13 +87,12 @@ describe("exportToSvg", () => { // To avoid varying snapshots name: "name", }; - expect(passedElements().length).toBe(3); expect(passedOptionsWhenDefault).toMatchSnapshot(); }); - it("with deleted elements", () => { - utils.exportToSvg({ + it("with deleted elements", async () => { + await utils.exportToSvg({ ...diagramFactory({ overrides: { appState: void 0 }, elementOverrides: { isDeleted: true }, @@ -104,18 +102,28 @@ describe("exportToSvg", () => { expect(passedElements().length).toBe(0); }); - it("with exportPadding and metadata", () => { - const METADATA = "some metada"; - - utils.exportToSvg({ + it("with exportPadding", async () => { + await utils.exportToSvg({ ...diagramFactory({ overrides: { appState: { name: "diagram name" } } }), exportPadding: 0, - metadata: METADATA, }); expect(passedElements().length).toBe(3); expect(passedOptions()).toEqual( - expect.objectContaining({ exportPadding: 0, metadata: METADATA }), + expect.objectContaining({ exportPadding: 0 }), ); }); + + it("with exportEmbedScene", async () => { + await utils.exportToSvg({ + ...diagramFactory({ + overrides: { + appState: { name: "diagram name", exportEmbedScene: true }, + }, + }), + }); + + expect(passedElements().length).toBe(3); + expect(passedOptions().exportEmbedScene).toBe(true); + }); }); diff --git a/src/tests/scene/__snapshots__/export.test.ts.snap b/src/tests/scene/__snapshots__/export.test.ts.snap index 6e683378..aee07550 100644 --- a/src/tests/scene/__snapshots__/export.test.ts.snap +++ b/src/tests/scene/__snapshots__/export.test.ts.snap @@ -70,3 +70,22 @@ exports[`exportToSvg with default arguments 1`] = ` `; + +exports[`exportToSvg with exportEmbedScene 1`] = ` +" + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1SQW7CMFx1MDAxMLzzisi9XCKRpIFQbrRUVaWqPXBAatWDiTexhbGD7Vx1MDAwMFx1MDAxMeLvtVx1MDAxZEjaqP1BfbC0szO76/WcXHUwMDA2QYBMXVx1MDAwMppcdTAwMDVcYo5cdTAwMTnmjCh8QEOH70FpJoVNxT7WslKZZ1JjytloxKVcdTAwMTVQqU3DXHUwMDA3XHUwMDBlW1x1MDAxMEZbxoeNg+Dkb5thxKn2K7V7m+dcdTAwMWImSLzLtunLYv707qWedLScJErauHaNb9M2PjBiqMWiMGwxXG6soKZcdTAwMDdiUXA3Zodoo+RcdTAwMDZcdTAwMWUkl8pccnJcdTAwMTP607Ve42xTKFlcdNJxojHG67zj5Izzpal5s1x1MDAwMJzRSlx1MDAwMep1WF1H7OGtTku74E5lW1x1MDAxNlSA1j80ssRcdTAwMTkzde9Vbr7ymfjtfvbrU6zKS1x1MDAxZKRd8G0yXHUwMDAw4ksl0WSc3oXTNtP9b1x1MDAxNId99FVcbv/XUTSdhmFcdTAwMTKnk5bB9MJ+tfFlc8w1dHt0K3xsbNCMKirO2/TVaIThrVx1MDAxNFx1MDAwNHn8PPz3yr9X/vRcbnDOSlxyXHUwMDE3r9jbv1x1MDAwN+GyXFxcdTAwMWFsXHUwMDFjpXFcdTAwMGXaMzjc//I3uT9Of1x1MDAxZZy/XHUwMDAw6cxEtiJ9 + + + + " +`; diff --git a/src/tests/scene/export.test.ts b/src/tests/scene/export.test.ts index e31d4ff4..f9e0211e 100644 --- a/src/tests/scene/export.test.ts +++ b/src/tests/scene/export.test.ts @@ -15,16 +15,16 @@ describe("exportToSvg", () => { viewBackgroundColor: "#ffffff", }; - it("with default arguments", () => { - const svgElement = exportUtils.exportToSvg(ELEMENTS, DEFAULT_OPTIONS); + it("with default arguments", async () => { + const svgElement = await exportUtils.exportToSvg(ELEMENTS, DEFAULT_OPTIONS); expect(svgElement).toMatchSnapshot(); }); - it("with background color", () => { + it("with background color", async () => { const BACKGROUND_COLOR = "#abcdef"; - const svgElement = exportUtils.exportToSvg(ELEMENTS, { + const svgElement = await exportUtils.exportToSvg(ELEMENTS, { ...DEFAULT_OPTIONS, exportBackground: true, viewBackgroundColor: BACKGROUND_COLOR, @@ -36,8 +36,8 @@ describe("exportToSvg", () => { ); }); - it("with dark mode", () => { - const svgElement = exportUtils.exportToSvg(ELEMENTS, { + it("with dark mode", async () => { + const svgElement = await exportUtils.exportToSvg(ELEMENTS, { ...DEFAULT_OPTIONS, exportWithDarkMode: true, }); @@ -47,14 +47,12 @@ describe("exportToSvg", () => { ); }); - it("with exportPadding, metadata", () => { - const svgElement = exportUtils.exportToSvg(ELEMENTS, { + it("with exportPadding", async () => { + const svgElement = await exportUtils.exportToSvg(ELEMENTS, { ...DEFAULT_OPTIONS, exportPadding: 0, - metadata: "some metadata", }); - expect(svgElement.innerHTML).toMatch(/some metadata/); expect(svgElement).toHaveAttribute("height", ELEMENT_HEIGHT.toString()); expect(svgElement).toHaveAttribute("width", ELEMENT_WIDTH.toString()); expect(svgElement).toHaveAttribute( @@ -63,10 +61,10 @@ describe("exportToSvg", () => { ); }); - it("with scale", () => { + it("with scale", async () => { const SCALE = 2; - const svgElement = exportUtils.exportToSvg(ELEMENTS, { + const svgElement = await exportUtils.exportToSvg(ELEMENTS, { ...DEFAULT_OPTIONS, exportPadding: 0, exportScale: SCALE, @@ -81,4 +79,12 @@ describe("exportToSvg", () => { (ELEMENT_WIDTH * SCALE).toString(), ); }); + + it("with exportEmbedScene", async () => { + const svgElement = await exportUtils.exportToSvg(ELEMENTS, { + ...DEFAULT_OPTIONS, + exportEmbedScene: true, + }); + expect(svgElement.innerHTML).toMatchSnapshot(); + }); });