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) {
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]);

View File

@ -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);

View File

@ -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;
}
}

View File

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

View File

@ -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`.

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 |
| 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

View File

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

View File

@ -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 = `<!-- svg-source:excalidraw -->`;
@ -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<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);
// 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");

View File

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

View File

@ -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);
});
});

File diff suppressed because one or more lines are too long

View File

@ -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();
});
});