mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
support embedding scene data to PNG/SVG (#2219)
Co-authored-by: Lipis <lipiridis@gmail.com>
This commit is contained in:
parent
7618ca48d7
commit
5950fa9a40
20 changed files with 329 additions and 27 deletions
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue