1
0
mirror of https://github.com/excalidraw/excalidraw.git synced 2024-11-02 03:25:53 +01:00

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

Co-authored-by: Lipis <lipiridis@gmail.com>
This commit is contained in:
David Luzar 2020-10-13 14:47:07 +02:00 committed by GitHub
parent 7618ca48d7
commit 5950fa9a40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 329 additions and 27 deletions

3
CHANGELOG.md Normal file

@ -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
package-lock.json generated

@ -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",

@ -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",

@ -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) => {

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

@ -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
src/base64.ts Normal file

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

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

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

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

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

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

@ -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"],

@ -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
src/global.d.ts vendored

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

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

@ -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",

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

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

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