Getting a scene in and out: data/ import and export
This module follows a dropped file — a .excalidraw JSON, or a PNG/SVG carrying an embedded scene — through data/blob.ts and data/json.ts back into live elements and appState, and then traces the mirror-image export path in data/index.ts and data/json.ts that serializes the Scene back out. The throughline is that persistence code is not a special App-level channel: it decodes/encodes bytes, then hands the result to the same Scene.replaceAllElements / updateScene commit point and the same Actions dispatch you've already seen used for drawing.
Inside loadSceneOrLibraryFromBlob
packages/excalidraw/data/blob.ts · lines 138–195The signature carries the two pieces of context an import needs beyond the file itself: localAppState/localElements (the current Scene, used to repair bindings and preserve scroll position) and an optional native FileSystemFileHandle so later saves can write back to the same file.
It hands the raw blob to parseFileContents first, so by this point any PNG/SVG-embedded payload has already been unwrapped into a plain JSON string - this function never has to know it was looking at an image.
JSON.parse turns the text into an object; if that fails on a file whose type is a recognized image, the failure is reclassified as ImageSceneDataError rather than a generic parse error - that's the signal App later uses to fall back to inserting the file as a plain picture instead of showing a corrupt-file message.
This is the branch that matters for a .excalidraw drop: elements go through restoreElements (repairing bindings, dropping invisible elements) and appState through restoreAppState, seeded with the local theme/fileHandle and merged against whatever appState the file carried. The output is exactly the { elements, appState, files } triple that the Scene-commit step already knows how to consume.
The same dropped-file plumbing also recognizes an .excalidrawlib payload by its content shape (not its file extension) and routes it to the library system instead of the Scene.
Anything that's neither a valid scene nor a valid library collapses to one generic "invalid file" error, while an ImageSceneDataError is deliberately rethrown unchanged so the caller can distinguish "this image just has no embedded scene" from "this file is garbage".
export const loadSceneOrLibraryFromBlob = async ( blob: Blob | File, /** @see restore.localAppState */ localAppState: AppState | null, localElements: readonly ExcalidrawElement[] | null, /** FileSystemFileHandle. Defaults to `blob.handle` if defined, otherwise null. */ fileHandle?: FileSystemFileHandle | null,) => { const contents = await parseFileContents(blob); let data; try { try { data = JSON.parse(contents); } catch (error: any) { if (isSupportedImageFile(blob)) { throw new ImageSceneDataError( "Image doesn't contain scene", "IMAGE_NOT_CONTAINS_SCENE_DATA", ); } throw error; } if (isValidExcalidrawData(data)) { return { type: MIME_TYPES.excalidraw, data: { elements: restoreElements(data.elements, localElements, { repairBindings: true, deleteInvisibleElements: true, }), appState: restoreAppState( { theme: localAppState?.theme, fileHandle: fileHandle || blob.handle || null, ...cleanAppStateForExport(data.appState || {}), ...(localAppState ? getScrollToContentState(data.elements || [], localAppState) : {}), }, localAppState, ), files: data.files || {}, }, }; } else if (isValidLibrary(data)) { return { type: MIME_TYPES.excalidrawlib, data, }; } throw new Error("Error: invalid file"); } catch (error: any) { if (error instanceof ImageSceneDataError) { throw error; } throw new Error("Error: invalid file"); }};
const parseFileContents = async (blob: Blob | File): Promise<string> => { let contents: string; if (blob.type === MIME_TYPES.png) { try { return await (await import("./image")).decodePngMetadata(blob); } catch (error: any) { if (error.message === "INVALID") { throw new ImageSceneDataError( "Image doesn't contain scene", "IMAGE_NOT_CONTAINS_SCENE_DATA", ); } else { throw new ImageSceneDataError("Error: cannot restore image"); } } } else { 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 === MIME_TYPES.svg) { try { return decodeSvgBase64Payload({ svg: contents, }); } catch (error: any) { if (error.message === "INVALID") { throw new ImageSceneDataError( "Image doesn't contain scene", "IMAGE_NOT_CONTAINS_SCENE_DATA", ); } else { throw new ImageSceneDataError("Error: cannot restore image"); } } } } return contents;};
packages/excalidraw/data/blob.ts · lines 32–80export const serializeAsJSON = ( elements: readonly ExcalidrawElement[], appState: Partial<AppState>, files: BinaryFiles, type: "local" | "database",): string => { const data: ExportedDataState = { type: EXPORT_DATA_TYPES.excalidraw, version: VERSIONS.excalidraw, source: getExportSource(), elements, appState: type === "local" ? cleanAppStateForExport(appState) : clearAppStateForDatabase(appState), files: type === "local" ? filterOutDeletedFiles(elements, files) : // will be stripped from JSON undefined, }; return JSON.stringify(data, null, 2);};export const saveAsJSON = async ({ data, filename, fileHandle,}: { data: MaybePromise<JSONExportData>; filename: string; fileHandle: AppState["fileHandle"];}) => { const blob = Promise.resolve(data).then(({ elements, appState, files }) => { const serialized = serializeAsJSON(elements, appState, files, "local"); return new Blob([serialized], { type: MIME_TYPES.excalidraw, }); }); const savedFileHandle = await fileSave(blob, { name: filename, extension: "excalidraw", description: "Excalidraw file", fileHandle: isImageFileHandle(fileHandle) ? null : fileHandle, }); return { fileHandle: savedFileHandle };};
packages/excalidraw/data/json.ts · lines 52–100Checkpoint
Answered in place — nothing is graded, everything is explained. 0 / 2 passed
You export a drawing to PNG with "embed scene" enabled, then later drag that PNG back onto the canvas. Why do you get the original editable elements back instead of just an inserted picture?
After loadSceneOrLibraryFromBlob returns a parsed { elements, appState, files } object, what actually makes those elements the live scene content?