diff options
| author | kj_sh604 | 2026-03-15 16:19:35 -0400 |
|---|---|---|
| committer | kj_sh604 | 2026-03-15 16:19:35 -0400 |
| commit | 6ec259a0e71174651bae95d4628138bf6fd68742 (patch) | |
| tree | 5e33c6a5ec091ecabfcb257fdc7b6a88ed8754ac /packages/excalidraw/data/index.ts | |
| parent | 16c8578b15c727f22921f8a80a56ee4d4e7f2272 (diff) | |
refactor: packages/
Diffstat (limited to 'packages/excalidraw/data/index.ts')
| -rw-r--r-- | packages/excalidraw/data/index.ts | 202 |
1 files changed, 202 insertions, 0 deletions
diff --git a/packages/excalidraw/data/index.ts b/packages/excalidraw/data/index.ts new file mode 100644 index 0000000..04cdcaf --- /dev/null +++ b/packages/excalidraw/data/index.ts @@ -0,0 +1,202 @@ +import { + copyBlobToClipboardAsPng, + copyTextToSystemClipboard, +} from "../clipboard"; +import { + DEFAULT_EXPORT_PADDING, + DEFAULT_FILENAME, + IMAGE_MIME_TYPES, + isFirefox, + MIME_TYPES, +} from "../constants"; +import { getNonDeletedElements } from "../element"; +import { isFrameLikeElement } from "../element/typeChecks"; +import type { + ExcalidrawElement, + ExcalidrawFrameLikeElement, + NonDeletedExcalidrawElement, +} from "../element/types"; +import { getElementsOverlappingFrame } from "../frame"; +import { t } from "../i18n"; +import { getSelectedElements, isSomeElementSelected } from "../scene"; +import { exportToCanvas, exportToSvg } from "../scene/export"; +import type { ExportType } from "../scene/types"; +import type { AppState, BinaryFiles } from "../types"; +import { cloneJSON } from "../utils"; +import { canvasToBlob } from "./blob"; +import type { FileSystemHandle } from "./filesystem"; +import { fileSave } from "./filesystem"; +import { serializeAsJSON } from "./json"; + +export { loadFromBlob } from "./blob"; +export { loadFromJSON, saveAsJSON } from "./json"; + +export type ExportedElements = readonly NonDeletedExcalidrawElement[] & { + _brand: "exportedElements"; +}; + +export const prepareElementsForExport = ( + elements: readonly ExcalidrawElement[], + { selectedElementIds }: Pick<AppState, "selectedElementIds">, + exportSelectionOnly: boolean, +) => { + elements = getNonDeletedElements(elements); + + const isExportingSelection = + exportSelectionOnly && + isSomeElementSelected(elements, { selectedElementIds }); + + let exportingFrame: ExcalidrawFrameLikeElement | null = null; + let exportedElements = isExportingSelection + ? getSelectedElements( + elements, + { selectedElementIds }, + { + includeBoundTextElement: true, + }, + ) + : elements; + + if (isExportingSelection) { + if ( + exportedElements.length === 1 && + isFrameLikeElement(exportedElements[0]) + ) { + exportingFrame = exportedElements[0]; + exportedElements = getElementsOverlappingFrame(elements, exportingFrame); + } else if (exportedElements.length > 1) { + exportedElements = getSelectedElements( + elements, + { selectedElementIds }, + { + includeBoundTextElement: true, + includeElementsInFrames: true, + }, + ); + } + } + + return { + exportingFrame, + exportedElements: cloneJSON(exportedElements) as ExportedElements, + }; +}; + +export const exportCanvas = async ( + type: Omit<ExportType, "backend">, + elements: ExportedElements, + appState: AppState, + files: BinaryFiles, + { + exportBackground, + exportPadding = DEFAULT_EXPORT_PADDING, + viewBackgroundColor, + name = appState.name || DEFAULT_FILENAME, + fileHandle = null, + exportingFrame = null, + }: { + exportBackground: boolean; + exportPadding?: number; + viewBackgroundColor: string; + /** filename, if applicable */ + name?: string; + fileHandle?: FileSystemHandle | null; + exportingFrame: ExcalidrawFrameLikeElement | null; + }, +) => { + if (elements.length === 0) { + throw new Error(t("alerts.cannotExportEmptyCanvas")); + } + if (type === "svg" || type === "clipboard-svg") { + const svgPromise = exportToSvg( + elements, + { + exportBackground, + exportWithDarkMode: appState.exportWithDarkMode, + viewBackgroundColor, + exportPadding, + exportScale: appState.exportScale, + exportEmbedScene: appState.exportEmbedScene && type === "svg", + }, + files, + { exportingFrame }, + ); + + if (type === "svg") { + return fileSave( + svgPromise.then((svg) => { + return new Blob([svg.outerHTML], { type: MIME_TYPES.svg }); + }), + { + description: "Export to SVG", + name, + extension: appState.exportEmbedScene ? "excalidraw.svg" : "svg", + mimeTypes: [IMAGE_MIME_TYPES.svg], + fileHandle, + }, + ); + } else if (type === "clipboard-svg") { + const svg = await svgPromise.then((svg) => svg.outerHTML); + try { + await copyTextToSystemClipboard(svg); + } catch (e) { + throw new Error(t("errors.copyToSystemClipboardFailed")); + } + return; + } + } + + const tempCanvas = exportToCanvas(elements, appState, files, { + exportBackground, + viewBackgroundColor, + exportPadding, + exportingFrame, + }); + + if (type === "png") { + let blob = canvasToBlob(tempCanvas); + + if (appState.exportEmbedScene) { + blob = blob.then((blob) => + import("./image").then(({ encodePngMetadata }) => + encodePngMetadata({ + blob, + metadata: serializeAsJSON(elements, appState, files, "local"), + }), + ), + ); + } + + return fileSave(blob, { + description: "Export to PNG", + name, + extension: appState.exportEmbedScene ? "excalidraw.png" : "png", + mimeTypes: [IMAGE_MIME_TYPES.png], + fileHandle, + }); + } else if (type === "clipboard") { + try { + const blob = canvasToBlob(tempCanvas); + await copyBlobToClipboardAsPng(blob); + } catch (error: any) { + console.warn(error); + if (error.name === "CANVAS_POSSIBLY_TOO_BIG") { + throw new Error(t("canvasError.canvasTooBig")); + } + // TypeError *probably* suggests ClipboardItem not defined, which + // people on Firefox can enable through a flag, so let's tell them. + if (isFirefox && error.name === "TypeError") { + throw new Error( + `${t("alerts.couldNotCopyToClipboard")}\n\n${t( + "hints.firefox_clipboard_write", + )}`, + ); + } else { + throw new Error(t("alerts.couldNotCopyToClipboard")); + } + } + } else { + // shouldn't happen + throw new Error("Unsupported export type"); + } +}; |
