feat: support appState.exportEmbedScene to embed scene data in exportToSvg util (#3777)

* feat: add embedScene attribute to exportToSvg util

* fix

* return promise

* add docs and remove

* fix

* fix tests

* use

* fix

* fix

* remove metadata and use exportEmbedScene

* fix

* fix

* fix

* fix

* IIFE
This commit is contained in:
Aakansha Doshi 2021-07-03 02:07:01 +05:30 committed by GitHub
parent 62303b8a22
commit f861a9fdd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 128 additions and 79 deletions

View File

@ -36,21 +36,27 @@ export const LibraryUnit = ({
if (!elementsToRender) { if (!elementsToRender) {
return; return;
} }
const svg = exportToSvg(elementsToRender, { let svg: SVGSVGElement;
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);
const current = ref.current!; 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 () => { return () => {
current.removeChild(svg); if (svg) {
current.removeChild(svg);
}
}; };
}, [elements, pendingElements]); }, [elements, pendingElements]);

View File

@ -34,19 +34,21 @@ const ChartPreviewBtn = (props: {
0, 0,
); );
setChartElements(elements); setChartElements(elements);
let svg: SVGSVGElement;
const svg = exportToSvg(elements, {
exportBackground: false,
viewBackgroundColor: oc.white,
});
const previewNode = previewRef.current!; const previewNode = previewRef.current!;
previewNode.appendChild(svg); (async () => {
svg = await exportToSvg(elements, {
exportBackground: false,
viewBackgroundColor: oc.white,
});
if (props.selected) { previewNode.appendChild(svg);
(previewNode.parentNode as HTMLDivElement).focus();
} if (props.selected) {
(previewNode.parentNode as HTMLDivElement).focus();
}
})();
return () => { return () => {
previewNode.removeChild(svg); previewNode.removeChild(svg);

View File

@ -35,20 +35,13 @@ export const exportCanvas = async (
throw new Error(t("alerts.cannotExportEmptyCanvas")); throw new Error(t("alerts.cannotExportEmptyCanvas"));
} }
if (type === "svg" || type === "clipboard-svg") { if (type === "svg" || type === "clipboard-svg") {
const tempSvg = exportToSvg(elements, { const tempSvg = await exportToSvg(elements, {
exportBackground, exportBackground,
exportWithDarkMode: appState.exportWithDarkMode, exportWithDarkMode: appState.exportWithDarkMode,
viewBackgroundColor, viewBackgroundColor,
exportPadding, exportPadding,
exportScale: appState.exportScale, exportScale: appState.exportScale,
metadata: exportEmbedScene: appState.exportEmbedScene && type === "svg",
appState.exportEmbedScene && type === "svg"
? await (
await import(/* webpackChunkName: "image" */ "./image")
).encodeSvgMetadata({
text: serializeAsJSON(elements, appState),
})
: undefined,
}); });
if (type === "svg") { if (type === "svg") {
await fileSave(new Blob([tempSvg.outerHTML], { type: "image/svg+xml" }), { await fileSave(new Blob([tempSvg.outerHTML], { type: "image/svg+xml" }), {
@ -57,7 +50,7 @@ export const exportCanvas = async (
}); });
return; return;
} else if (type === "clipboard-svg") { } else if (type === "clipboard-svg") {
copyTextToSystemClipboard(tempSvg.outerHTML); await copyTextToSystemClipboard(tempSvg.outerHTML);
return; return;
} }
} }

View File

@ -15,7 +15,7 @@ import Library from "./library";
export const serializeAsJSON = ( export const serializeAsJSON = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: Partial<AppState>,
): string => { ): string => {
const data: ExportedDataState = { const data: ExportedDataState = {
type: EXPORT_DATA_TYPES.excalidraw, type: EXPORT_DATA_TYPES.excalidraw,

View File

@ -19,6 +19,13 @@ Please add the latest change on the top under the correct section.
### Features ### 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 [`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`. - 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`.

View File

@ -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 | | 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 | | 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 | | 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 ##### 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 | | exportBackground | boolean | true | Indicates whether background should be exported |
| viewBackgroundColor | string | #fff | The default background color | | viewBackgroundColor | string | #fff | The default background color |
| exportWithDarkMode | boolean | false | Indicates whether to export with dark mode | | 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 ### FONT_FAMILY

View File

@ -74,15 +74,13 @@ export const exportToBlob = (
}); });
}; };
export const exportToSvg = ({ export const exportToSvg = async ({
elements, elements,
appState = getDefaultAppState(), appState = getDefaultAppState(),
exportPadding, exportPadding,
metadata,
}: Omit<ExportOpts, "getDimensions"> & { }: Omit<ExportOpts, "getDimensions"> & {
exportPadding?: number; exportPadding?: number;
metadata?: string; }): Promise<SVGSVGElement> => {
}): SVGSVGElement => {
const { elements: restoredElements, appState: restoredAppState } = restore( const { elements: restoredElements, appState: restoredAppState } = restore(
{ elements, appState }, { elements, appState },
null, null,
@ -90,7 +88,6 @@ export const exportToSvg = ({
return _exportToSvg(getNonDeletedElements(restoredElements), { return _exportToSvg(getNonDeletedElements(restoredElements), {
...restoredAppState, ...restoredAppState,
exportPadding, exportPadding,
metadata,
}); });
}; };

View File

@ -6,6 +6,7 @@ import { distance, SVG_NS } from "../utils";
import { AppState } from "../types"; import { AppState } from "../types";
import { DEFAULT_EXPORT_PADDING, THEME_FILTER } from "../constants"; import { DEFAULT_EXPORT_PADDING, THEME_FILTER } from "../constants";
import { getDefaultAppState } from "../appState"; import { getDefaultAppState } from "../appState";
import { serializeAsJSON } from "../data/json";
export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`; export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
@ -65,24 +66,35 @@ export const exportToCanvas = (
return canvas; return canvas;
}; };
export const exportToSvg = ( export const exportToSvg = async (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
{ appState: {
exportBackground,
exportPadding = DEFAULT_EXPORT_PADDING,
viewBackgroundColor,
exportWithDarkMode,
exportScale = 1,
metadata = "",
}: {
exportBackground: boolean; exportBackground: boolean;
exportPadding?: number; exportPadding?: number;
exportScale?: number; exportScale?: number;
viewBackgroundColor: string; viewBackgroundColor: string;
exportWithDarkMode?: boolean; exportWithDarkMode?: boolean;
metadata?: string; exportEmbedScene?: boolean;
}, },
): SVGSVGElement => { ): Promise<SVGSVGElement> => {
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); const [minX, minY, width, height] = getCanvasSize(elements, exportPadding);
// initialze SVG root // initialze SVG root
@ -92,7 +104,7 @@ export const exportToSvg = (
svgRoot.setAttribute("viewBox", `0 0 ${width} ${height}`); svgRoot.setAttribute("viewBox", `0 0 ${width} ${height}`);
svgRoot.setAttribute("width", `${width * exportScale}`); svgRoot.setAttribute("width", `${width * exportScale}`);
svgRoot.setAttribute("height", `${height * exportScale}`); svgRoot.setAttribute("height", `${height * exportScale}`);
if (exportWithDarkMode) { if (appState.exportWithDarkMode) {
svgRoot.setAttribute("filter", THEME_FILTER); svgRoot.setAttribute("filter", THEME_FILTER);
} }
@ -114,7 +126,7 @@ export const exportToSvg = (
`; `;
// render background rect // render background rect
if (exportBackground && viewBackgroundColor) { if (appState.exportBackground && viewBackgroundColor) {
const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect"); const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect");
rect.setAttribute("x", "0"); rect.setAttribute("x", "0");
rect.setAttribute("y", "0"); rect.setAttribute("y", "0");

View File

@ -39,7 +39,6 @@ Object {
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"metadata": undefined,
"multiElement": null, "multiElement": null,
"name": "name", "name": "name",
"openMenu": null, "openMenu": null,

View File

@ -73,11 +73,10 @@ describe("exportToSvg", () => {
const mockedExportUtil = mockedSceneExportUtils.exportToSvg as jest.Mock; const mockedExportUtil = mockedSceneExportUtils.exportToSvg as jest.Mock;
const passedElements = () => mockedExportUtil.mock.calls[0][0]; const passedElements = () => mockedExportUtil.mock.calls[0][0];
const passedOptions = () => mockedExportUtil.mock.calls[0][1]; const passedOptions = () => mockedExportUtil.mock.calls[0][1];
afterEach(jest.resetAllMocks); afterEach(jest.resetAllMocks);
it("with default arguments", () => { it("with default arguments", async () => {
utils.exportToSvg({ await utils.exportToSvg({
...diagramFactory({ ...diagramFactory({
overrides: { appState: void 0 }, overrides: { appState: void 0 },
}), }),
@ -88,13 +87,12 @@ describe("exportToSvg", () => {
// To avoid varying snapshots // To avoid varying snapshots
name: "name", name: "name",
}; };
expect(passedElements().length).toBe(3); expect(passedElements().length).toBe(3);
expect(passedOptionsWhenDefault).toMatchSnapshot(); expect(passedOptionsWhenDefault).toMatchSnapshot();
}); });
it("with deleted elements", () => { it("with deleted elements", async () => {
utils.exportToSvg({ await utils.exportToSvg({
...diagramFactory({ ...diagramFactory({
overrides: { appState: void 0 }, overrides: { appState: void 0 },
elementOverrides: { isDeleted: true }, elementOverrides: { isDeleted: true },
@ -104,18 +102,28 @@ describe("exportToSvg", () => {
expect(passedElements().length).toBe(0); expect(passedElements().length).toBe(0);
}); });
it("with exportPadding and metadata", () => { it("with exportPadding", async () => {
const METADATA = "some metada"; await utils.exportToSvg({
utils.exportToSvg({
...diagramFactory({ overrides: { appState: { name: "diagram name" } } }), ...diagramFactory({ overrides: { appState: { name: "diagram name" } } }),
exportPadding: 0, exportPadding: 0,
metadata: METADATA,
}); });
expect(passedElements().length).toBe(3); expect(passedElements().length).toBe(3);
expect(passedOptions()).toEqual( 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);
});
}); });

File diff suppressed because one or more lines are too long

View File

@ -15,16 +15,16 @@ describe("exportToSvg", () => {
viewBackgroundColor: "#ffffff", viewBackgroundColor: "#ffffff",
}; };
it("with default arguments", () => { it("with default arguments", async () => {
const svgElement = exportUtils.exportToSvg(ELEMENTS, DEFAULT_OPTIONS); const svgElement = await exportUtils.exportToSvg(ELEMENTS, DEFAULT_OPTIONS);
expect(svgElement).toMatchSnapshot(); expect(svgElement).toMatchSnapshot();
}); });
it("with background color", () => { it("with background color", async () => {
const BACKGROUND_COLOR = "#abcdef"; const BACKGROUND_COLOR = "#abcdef";
const svgElement = exportUtils.exportToSvg(ELEMENTS, { const svgElement = await exportUtils.exportToSvg(ELEMENTS, {
...DEFAULT_OPTIONS, ...DEFAULT_OPTIONS,
exportBackground: true, exportBackground: true,
viewBackgroundColor: BACKGROUND_COLOR, viewBackgroundColor: BACKGROUND_COLOR,
@ -36,8 +36,8 @@ describe("exportToSvg", () => {
); );
}); });
it("with dark mode", () => { it("with dark mode", async () => {
const svgElement = exportUtils.exportToSvg(ELEMENTS, { const svgElement = await exportUtils.exportToSvg(ELEMENTS, {
...DEFAULT_OPTIONS, ...DEFAULT_OPTIONS,
exportWithDarkMode: true, exportWithDarkMode: true,
}); });
@ -47,14 +47,12 @@ describe("exportToSvg", () => {
); );
}); });
it("with exportPadding, metadata", () => { it("with exportPadding", async () => {
const svgElement = exportUtils.exportToSvg(ELEMENTS, { const svgElement = await exportUtils.exportToSvg(ELEMENTS, {
...DEFAULT_OPTIONS, ...DEFAULT_OPTIONS,
exportPadding: 0, exportPadding: 0,
metadata: "some metadata",
}); });
expect(svgElement.innerHTML).toMatch(/some metadata/);
expect(svgElement).toHaveAttribute("height", ELEMENT_HEIGHT.toString()); expect(svgElement).toHaveAttribute("height", ELEMENT_HEIGHT.toString());
expect(svgElement).toHaveAttribute("width", ELEMENT_WIDTH.toString()); expect(svgElement).toHaveAttribute("width", ELEMENT_WIDTH.toString());
expect(svgElement).toHaveAttribute( expect(svgElement).toHaveAttribute(
@ -63,10 +61,10 @@ describe("exportToSvg", () => {
); );
}); });
it("with scale", () => { it("with scale", async () => {
const SCALE = 2; const SCALE = 2;
const svgElement = exportUtils.exportToSvg(ELEMENTS, { const svgElement = await exportUtils.exportToSvg(ELEMENTS, {
...DEFAULT_OPTIONS, ...DEFAULT_OPTIONS,
exportPadding: 0, exportPadding: 0,
exportScale: SCALE, exportScale: SCALE,
@ -81,4 +79,12 @@ describe("exportToSvg", () => {
(ELEMENT_WIDTH * SCALE).toString(), (ELEMENT_WIDTH * SCALE).toString(),
); );
}); });
it("with exportEmbedScene", async () => {
const svgElement = await exportUtils.exportToSvg(ELEMENTS, {
...DEFAULT_OPTIONS,
exportEmbedScene: true,
});
expect(svgElement.innerHTML).toMatchSnapshot();
});
}); });