Browse Source

support embedding scene data to PNG/SVG (#2219)

Co-authored-by: Lipis <lipiridis@gmail.com>
David Luzar 4 years ago
parent
commit
5950fa9a40

+ 3 - 0
CHANGELOG.md

@@ -0,0 +1,3 @@
+## 2020-10-13
+
+- Added ability to embed scene source into exported PNG/SVG files so you can import the scene from them (open via `Load` button or drag & drop). #2219

+ 32 - 0
package-lock.json

@@ -5901,6 +5901,11 @@
         }
       }
     },
+    "crc-32": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-0.3.0.tgz",
+      "integrity": "sha1-aj02h/W67EH36bmf4ZU6Ll0Zd14="
+    },
     "crc32-stream": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-3.0.1.tgz",
@@ -17341,6 +17346,28 @@
       "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz",
       "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA=="
     },
+    "png-chunk-text": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/png-chunk-text/-/png-chunk-text-1.0.0.tgz",
+      "integrity": "sha1-HGAG2ONLpHHTjhycVLP1PhCF4Y8="
+    },
+    "png-chunks-encode": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/png-chunks-encode/-/png-chunks-encode-1.0.0.tgz",
+      "integrity": "sha1-2epeNcru7XgmWMGre6+npe2xqHg=",
+      "requires": {
+        "crc-32": "^0.3.0",
+        "sliced": "^1.0.1"
+      }
+    },
+    "png-chunks-extract": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/png-chunks-extract/-/png-chunks-extract-1.0.0.tgz",
+      "integrity": "sha1-+tSpBeZmUhlzUcZeNbksZDEeRy0=",
+      "requires": {
+        "crc-32": "^0.3.0"
+      }
+    },
     "pnp-webpack-plugin": {
       "version": "1.6.4",
       "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz",
@@ -20125,6 +20152,11 @@
         }
       }
     },
+    "sliced": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
+      "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E="
+    },
     "snapdragon": {
       "version": "0.8.2",
       "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",

+ 3 - 0
package.json

@@ -35,6 +35,9 @@
     "nanoid": "2.1.11",
     "node-sass": "4.14.1",
     "open-color": "1.7.0",
+    "png-chunk-text": "1.0.0",
+    "png-chunks-encode": "1.0.0",
+    "png-chunks-extract": "1.0.0",
     "points-on-curve": "0.2.0",
     "pwacompat": "2.0.17",
     "react": "16.13.1",

+ 20 - 0
src/actions/actionExport.tsx

@@ -43,6 +43,26 @@ export const actionChangeExportBackground = register({
   ),
 });
 
+export const actionChangeExportEmbedScene = register({
+  name: "changeExportEmbedScene",
+  perform: (_elements, appState, value) => {
+    return {
+      appState: { ...appState, exportEmbedScene: value },
+      commitToHistory: false,
+    };
+  },
+  PanelComponent: ({ appState, updateData }) => (
+    <label title={t("labels.exportEmbedScene_details")}>
+      <input
+        type="checkbox"
+        checked={appState.exportEmbedScene}
+        onChange={(event) => updateData(event.target.checked)}
+      />{" "}
+      {t("labels.exportEmbedScene")}
+    </label>
+  ),
+});
+
 export const actionChangeShouldAddWatermark = register({
   name: "changeShouldAddWatermark",
   perform: (_elements, appState, value) => {

+ 1 - 0
src/actions/types.ts

@@ -44,6 +44,7 @@ export type ActionName =
   | "finalize"
   | "changeProjectName"
   | "changeExportBackground"
+  | "changeExportEmbedScene"
   | "changeShouldAddWatermark"
   | "saveScene"
   | "saveAsScene"

+ 2 - 0
src/appState.ts

@@ -25,6 +25,7 @@ export const getDefaultAppState = (): Omit<
     elementType: "selection",
     elementLocked: false,
     exportBackground: true,
+    exportEmbedScene: false,
     shouldAddWatermark: false,
     currentItemStrokeColor: oc.black,
     currentItemBackgroundColor: "transparent",
@@ -112,6 +113,7 @@ const APP_STATE_STORAGE_CONF = (<
   elementType: { browser: true, export: false },
   errorMessage: { browser: false, export: false },
   exportBackground: { browser: true, export: false },
+  exportEmbedScene: { browser: true, export: false },
   gridSize: { browser: true, export: true },
   height: { browser: false, export: false },
   isBindingEnabled: { browser: false, export: false },

+ 40 - 0
src/base64.ts

@@ -0,0 +1,40 @@
+// `btoa(unescape(encodeURIComponent(str)))` hack doesn't work in edge cases and
+// `unescape` API shouldn't be used anyway.
+// This implem is ~10x faster than using fromCharCode in a loop (in Chrome).
+const stringToByteString = (str: string): Promise<string> => {
+  return new Promise((resolve, reject) => {
+    const blob = new Blob([new TextEncoder().encode(str)]);
+    const reader = new FileReader();
+    reader.onload = function (event) {
+      if (!event.target || typeof event.target.result !== "string") {
+        return reject(new Error("couldn't convert to byte string"));
+      }
+      resolve(event.target.result);
+    };
+    reader.readAsBinaryString(blob);
+  });
+};
+
+function byteStringToArrayBuffer(byteString: string) {
+  const buffer = new ArrayBuffer(byteString.length);
+  const bufferView = new Uint8Array(buffer);
+  for (let i = 0, len = byteString.length; i < len; i++) {
+    bufferView[i] = byteString.charCodeAt(i);
+  }
+  return buffer;
+}
+
+const byteStringToString = (byteString: string) => {
+  return new TextDecoder("utf-8").decode(byteStringToArrayBuffer(byteString));
+};
+
+// -----------------------------------------------------------------------------
+
+export const stringToBase64 = async (str: string) => {
+  return btoa(await stringToByteString(str));
+};
+
+// async to align with stringToBase64
+export const base64ToString = async (base64: string) => {
+  return byteStringToString(atob(base64));
+};

+ 24 - 4
src/components/App.tsx

@@ -125,6 +125,7 @@ import {
   DEFAULT_VERTICAL_ALIGN,
   GRID_SIZE,
   LOCAL_STORAGE_KEY_COLLAB_FORCE_FLAG,
+  MIME_TYPES,
 } from "../constants";
 import {
   INITIAL_SCENE_UPDATE_TIMEOUT,
@@ -3788,9 +3789,28 @@ class App extends React.Component<ExcalidrawProps, AppState> {
   private handleCanvasOnDrop = async (
     event: React.DragEvent<HTMLCanvasElement>,
   ) => {
-    const libraryShapes = event.dataTransfer.getData(
-      "application/vnd.excalidrawlib+json",
-    );
+    try {
+      const file = event.dataTransfer.files[0];
+      if (file?.type === "image/png" || file?.type === "image/svg+xml") {
+        const { elements, appState } = await loadFromBlob(file, this.state);
+        this.syncActionResult({
+          elements,
+          appState: {
+            ...(appState || this.state),
+            isLoading: false,
+          },
+          commitToHistory: true,
+        });
+        return;
+      }
+    } catch (error) {
+      return this.setState({
+        isLoading: false,
+        errorMessage: error.message,
+      });
+    }
+
+    const libraryShapes = event.dataTransfer.getData(MIME_TYPES.excalidraw);
     if (libraryShapes !== "") {
       this.addElementsFromPasteOrLibrary(
         JSON.parse(libraryShapes),
@@ -3835,7 +3855,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
           this.setState({ isLoading: false, errorMessage: error.message });
         });
     } else if (
-      file?.type === "application/vnd.excalidrawlib+json" ||
+      file?.type === MIME_TYPES.excalidrawlib ||
       file?.name.endsWith(".excalidrawlib")
     ) {
       Library.importLibrary(file)

+ 1 - 0
src/components/ExportDialog.tsx

@@ -156,6 +156,7 @@ const ExportModal = ({
           </Stack.Row>
         </div>
         {actionManager.renderAction("changeExportBackground")}
+        {actionManager.renderAction("changeExportEmbedScene")}
         {someElementIsSelected && (
           <div>
             <label>

+ 2 - 1
src/components/LibraryUnit.tsx

@@ -6,6 +6,7 @@ import "./LibraryUnit.scss";
 import { t } from "../i18n";
 import useIsMobile from "../is-mobile";
 import { LibraryItem } from "../types";
+import { MIME_TYPES } from "../constants";
 
 // fa-plus
 const PLUS_ICON = (
@@ -78,7 +79,7 @@ export const LibraryUnit = ({
         onDragStart={(event) => {
           setIsHovered(false);
           event.dataTransfer.setData(
-            "application/vnd.excalidrawlib+json",
+            MIME_TYPES.excalidrawlib,
             JSON.stringify(elements),
           );
         }}

+ 5 - 0
src/constants.ts

@@ -84,3 +84,8 @@ export const CANVAS_ONLY_ACTIONS = ["selectAll"];
 export const GRID_SIZE = 20; // TODO make it configurable?
 
 export const LOCAL_STORAGE_KEY_COLLAB_FORCE_FLAG = "collabLinkForceLoadFlag";
+
+export const MIME_TYPES = {
+  excalidraw: "application/vnd.excalidraw+json",
+  excalidrawlib: "application/vnd.excalidrawlib+json",
+};

+ 45 - 14
src/data/blob.ts

@@ -4,21 +4,52 @@ import { t } from "../i18n";
 import { AppState } from "../types";
 import { LibraryData, ImportedDataState } from "./types";
 import { calculateScrollCenter } from "../scene";
+import { MIME_TYPES } from "../constants";
+import { base64ToString } from "../base64";
 
-const loadFileContents = async (blob: any) => {
+export const parseFileContents = async (blob: Blob | File) => {
   let contents: string;
-  if ("text" in Blob) {
-    contents = await blob.text();
+  if (blob.type === "image/png") {
+    const { default: decodePng } = await import("png-chunks-extract");
+    const { default: tEXt } = await import("png-chunk-text");
+    const chunks = decodePng(new Uint8Array(await blob.arrayBuffer()));
+
+    const metadataChunk = chunks.find((chunk) => chunk.name === "tEXt");
+    if (metadataChunk) {
+      const metadata = tEXt.decode(metadataChunk.data);
+      if (metadata.keyword === MIME_TYPES.excalidraw) {
+        return metadata.text;
+      }
+      throw new Error(t("alerts.imageDoesNotContainScene"));
+    } else {
+      throw new Error(t("alerts.imageDoesNotContainScene"));
+    }
   } else {
-    contents = await new Promise((resolve) => {
-      const reader = new FileReader();
-      reader.readAsText(blob, "utf8");
-      reader.onloadend = () => {
-        if (reader.readyState === FileReader.DONE) {
-          resolve(reader.result as string);
+    if ("text" in Blob) {
+      contents = await blob.text();
+    } else {
+      contents = await new Promise((resolve) => {
+        const reader = new FileReader();
+        reader.readAsText(blob, "utf8");
+        reader.onloadend = () => {
+          if (reader.readyState === FileReader.DONE) {
+            resolve(reader.result as string);
+          }
+        };
+      });
+    }
+    if (blob.type === "image/svg+xml") {
+      if (contents.includes(`payload-type:${MIME_TYPES.excalidraw}`)) {
+        const match = contents.match(
+          /<!-- payload-start -->(.+?)<!-- payload-end -->/,
+        );
+        if (!match) {
+          throw new Error(t("alerts.imageDoesNotContainScene"));
         }
-      };
-    });
+        return base64ToString(match[1]);
+      }
+      throw new Error(t("alerts.imageDoesNotContainScene"));
+    }
   }
   return contents;
 };
@@ -33,7 +64,7 @@ export const loadFromBlob = async (
     (window as any).handle = blob.handle;
   }
 
-  const contents = await loadFileContents(blob);
+  const contents = await parseFileContents(blob);
   try {
     const data: ImportedDataState = JSON.parse(contents);
     if (data.type !== "excalidraw") {
@@ -57,8 +88,8 @@ export const loadFromBlob = async (
   }
 };
 
-export const loadLibraryFromBlob = async (blob: any) => {
-  const contents = await loadFileContents(blob);
+export const loadLibraryFromBlob = async (blob: Blob) => {
+  const contents = await parseFileContents(blob);
   const data: LibraryData = JSON.parse(contents);
   if (data.type !== "excalidrawlib") {
     throw new Error(t("alerts.couldNotLoadInvalidFile"));

+ 27 - 1
src/data/index.ts

@@ -19,6 +19,8 @@ import { serializeAsJSON } from "./json";
 import { ExportType } from "../scene/types";
 import { restore } from "./restore";
 import { ImportedDataState } from "./types";
+import { MIME_TYPES } from "../constants";
+import { stringToBase64 } from "../base64";
 
 export { loadFromBlob } from "./blob";
 export { saveAsJSON, loadFromJSON } from "./json";
@@ -300,11 +302,21 @@ export const exportCanvas = async (
     return window.alert(t("alerts.cannotExportEmptyCanvas"));
   }
   if (type === "svg" || type === "clipboard-svg") {
+    let metadata = "";
+
+    if (appState.exportEmbedScene && type === "svg") {
+      metadata += `<!-- payload-type:${MIME_TYPES.excalidraw} -->`;
+      metadata += "<!-- payload-start -->";
+      metadata += await stringToBase64(serializeAsJSON(elements, appState));
+      metadata += "<!-- payload-end -->";
+    }
+
     const tempSvg = exportToSvg(elements, {
       exportBackground,
       viewBackgroundColor,
       exportPadding,
       shouldAddWatermark,
+      metadata,
     });
     if (type === "svg") {
       await fileSave(new Blob([tempSvg.outerHTML], { type: "image/svg+xml" }), {
@@ -330,8 +342,22 @@ export const exportCanvas = async (
 
   if (type === "png") {
     const fileName = `${name}.png`;
-    tempCanvas.toBlob(async (blob: any) => {
+    tempCanvas.toBlob(async (blob) => {
       if (blob) {
+        if (appState.exportEmbedScene) {
+          const { default: tEXt } = await import("png-chunk-text");
+          const { default: encodePng } = await import("png-chunks-encode");
+          const { default: decodePng } = await import("png-chunks-extract");
+          const chunks = decodePng(new Uint8Array(await blob.arrayBuffer()));
+          const metadata = tEXt.encode(
+            MIME_TYPES.excalidraw,
+            serializeAsJSON(elements, appState),
+          );
+          // insert metadata before last chunk (iEND)
+          chunks.splice(-1, 0, metadata);
+          blob = new Blob([encodePng(chunks)], { type: "image/png" });
+        }
+
         await fileSave(blob, {
           fileName: fileName,
           extensions: [".png"],

+ 4 - 3
src/data/json.ts

@@ -6,6 +6,7 @@ import { fileOpen, fileSave } from "browser-nativefs";
 import { loadFromBlob } from "./blob";
 import { loadLibrary } from "./localStorage";
 import { Library } from "./library";
+import { MIME_TYPES } from "../constants";
 
 export const serializeAsJSON = (
   elements: readonly ExcalidrawElement[],
@@ -48,8 +49,8 @@ export const saveAsJSON = async (
 export const loadFromJSON = async (localAppState: AppState) => {
   const blob = await fileOpen({
     description: "Excalidraw files",
-    extensions: [".json", ".excalidraw"],
-    mimeTypes: ["application/json"],
+    extensions: [".json", ".excalidraw", ".png", ".svg"],
+    mimeTypes: ["application/json", "image/png", "image/svg+xml"],
   });
   return loadFromBlob(blob, localAppState);
 };
@@ -76,7 +77,7 @@ export const saveLibraryAsJSON = async () => {
   );
   const fileName = "library.excalidrawlib";
   const blob = new Blob([serialized], {
-    type: "application/vnd.excalidrawlib+json",
+    type: MIME_TYPES.excalidrawlib,
   });
   await fileSave(blob, {
     fileName,

+ 22 - 0
src/global.d.ts

@@ -41,6 +41,28 @@ type ResolutionType<T extends (...args: any) => any> = T extends (
 // https://github.com/krzkaczor/ts-essentials
 type MarkOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
 
+// PNG encoding/decoding
+// -----------------------------------------------------------------------------
+type TEXtChunk = { name: "tEXt"; data: Uint8Array };
+
+declare module "png-chunk-text" {
+  function encode(
+    name: string,
+    value: string,
+  ): { name: "tEXt"; data: Uint8Array };
+  function decode(data: Uint8Array): { keyword: string; text: string };
+}
+declare module "png-chunks-encode" {
+  function encode(chunks: TEXtChunk[]): Uint8Array;
+  export = encode;
+}
+declare module "png-chunks-extract" {
+  function extract(buffer: Uint8Array): TEXtChunk[];
+  export = extract;
+}
+// -----------------------------------------------------------------------------
+
+// -----------------------------------------------------------------------------
 // type getter for interface's callable type
 // src: https://stackoverflow.com/a/58658851/927631
 // -----------------------------------------------------------------------------

+ 1 - 1
src/index-node.ts

@@ -75,7 +75,7 @@ const canvas = exportToCanvas(
 
 const fs = require("fs");
 const out = fs.createWriteStream("test.png");
-const stream = canvas.createPNGStream();
+const stream = (canvas as any).createPNGStream();
 stream.pipe(out);
 out.on("finish", () => {
   console.info("test.png was created.");

+ 5 - 1
src/locales/en.json

@@ -32,6 +32,8 @@
     "fontFamily": "Font family",
     "onlySelected": "Only selected",
     "withBackground": "With Background",
+    "exportEmbedScene": "Embed scene into exported file",
+    "exportEmbedScene_details": "Scene data will be saved into the exported PNG/SVG file so that the scene can be restored from it.\nWill increase exported file size.",
     "addWatermark": "Add \"Made with Excalidraw\"",
     "handDrawn": "Hand-drawn",
     "normal": "Normal",
@@ -115,7 +117,9 @@
     "uploadedSecurly": "The upload has been secured with end-to-end encryption, which means that Excalidraw server and third parties can't read the content.",
     "loadSceneOverridePrompt": "Loading external drawing will replace your existing content. Do you wish to continue?",
     "errorLoadingLibrary": "There was an error loading the third party library.",
-    "confirmAddLibrary": "This will add {{numShapes}} shape(s) to your library. Are you sure?"
+    "confirmAddLibrary": "This will add {{numShapes}} shape(s) to your library. Are you sure?",
+    "imageDoesNotContainScene": "Image file doesn't contain scene data. Have you enabled this during export?",
+    "cannotRestoreFromImage": "Scene couldn't be restored from this image file"
   },
   "toolBar": {
     "selection": "Selection",

+ 8 - 2
src/scene/export.ts

@@ -29,7 +29,10 @@ export const exportToCanvas = (
     viewBackgroundColor: string;
     shouldAddWatermark: boolean;
   },
-  createCanvas: (width: number, height: number) => any = (width, height) => {
+  createCanvas: (width: number, height: number) => HTMLCanvasElement = (
+    width,
+    height,
+  ) => {
     const tempCanvas = document.createElement("canvas");
     tempCanvas.width = width * scale;
     tempCanvas.height = height * scale;
@@ -44,7 +47,7 @@ export const exportToCanvas = (
     shouldAddWatermark,
   );
 
-  const tempCanvas: any = createCanvas(width, height);
+  const tempCanvas = createCanvas(width, height);
 
   renderScene(
     sceneElements,
@@ -81,11 +84,13 @@ export const exportToSvg = (
     exportPadding = 10,
     viewBackgroundColor,
     shouldAddWatermark,
+    metadata = "",
   }: {
     exportBackground: boolean;
     exportPadding?: number;
     viewBackgroundColor: string;
     shouldAddWatermark: boolean;
+    metadata?: string;
   },
 ): SVGSVGElement => {
   const sceneElements = getElementsAndWatermark(elements, shouldAddWatermark);
@@ -104,6 +109,7 @@ export const exportToSvg = (
 
   svgRoot.innerHTML = `
   ${SVG_EXPORT_TAG}
+  ${metadata}
   <defs>
     <style>
       @font-face {

+ 83 - 0
src/tests/__snapshots__/regressionTests.test.tsx.snap

@@ -27,6 +27,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -473,6 +474,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -925,6 +927,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": false,
@@ -1686,6 +1689,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -1875,6 +1879,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -2318,6 +2323,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -2556,6 +2562,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -2705,6 +2712,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -3167,6 +3175,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -3460,6 +3469,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -3649,6 +3659,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -3878,6 +3889,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -4115,6 +4127,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -4483,6 +4496,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -4763,6 +4777,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -5055,6 +5070,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -5248,6 +5264,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -5397,6 +5414,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -5835,6 +5853,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -6138,6 +6157,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -8103,6 +8123,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -8450,6 +8471,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -8690,6 +8712,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -8928,6 +8951,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -9228,6 +9252,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -9377,6 +9402,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -9526,6 +9552,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -9675,6 +9702,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -9850,6 +9878,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -10025,6 +10054,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -10200,6 +10230,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -10375,6 +10406,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -10524,6 +10556,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -10673,6 +10706,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -10848,6 +10882,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -10997,6 +11032,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -11172,6 +11208,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -11873,6 +11910,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -12111,6 +12149,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -12198,6 +12237,7 @@ Object {
   "elementType": "rectangle",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -12283,6 +12323,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -13160,6 +13201,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -13596,6 +13638,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -13945,6 +13988,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -14211,6 +14255,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -14398,6 +14443,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -15222,6 +15268,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -15943,6 +15990,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -16565,6 +16613,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -17092,6 +17141,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -17573,6 +17623,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -17965,6 +18016,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -18272,6 +18324,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -18498,6 +18551,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -19375,6 +19429,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -20147,6 +20202,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -20818,6 +20874,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -21392,6 +21449,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -21541,6 +21599,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -21834,6 +21893,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -22127,6 +22187,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -22276,6 +22337,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -22457,6 +22519,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -22691,6 +22754,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -23000,6 +23064,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -23824,6 +23889,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -24117,6 +24183,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -24410,6 +24477,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -24774,6 +24842,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -24926,6 +24995,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -25232,6 +25302,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -25472,6 +25543,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -25784,6 +25856,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -25869,6 +25942,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -26018,6 +26092,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -26824,6 +26899,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -26909,6 +26985,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -27646,6 +27723,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -28036,6 +28114,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -28294,6 +28373,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -28381,6 +28461,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -28858,6 +28939,7 @@ Object {
   "elementType": "text",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,
@@ -28943,6 +29025,7 @@ Object {
   "elementType": "selection",
   "errorMessage": null,
   "exportBackground": true,
+  "exportEmbedScene": false,
   "gridSize": null,
   "height": 768,
   "isBindingEnabled": true,

+ 1 - 0
src/types.ts

@@ -47,6 +47,7 @@ export type AppState = {
   elementType: typeof SHAPES[number]["value"];
   elementLocked: boolean;
   exportBackground: boolean;
+  exportEmbedScene: boolean;
   shouldAddWatermark: boolean;
   currentItemStrokeColor: string;
   currentItemBackgroundColor: string;