mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-10-31 02:44:50 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			211 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			211 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import {
 | |
|   exportToCanvas as _exportToCanvas,
 | |
|   exportToSvg as _exportToSvg,
 | |
| } from "../excalidraw/scene/export";
 | |
| import { getDefaultAppState } from "../excalidraw/appState";
 | |
| import type { AppState, BinaryFiles } from "../excalidraw/types";
 | |
| import type {
 | |
|   ExcalidrawElement,
 | |
|   ExcalidrawFrameLikeElement,
 | |
|   NonDeleted,
 | |
| } from "../excalidraw/element/types";
 | |
| import { restore } from "../excalidraw/data/restore";
 | |
| import { MIME_TYPES } from "../excalidraw/constants";
 | |
| import { encodePngMetadata } from "../excalidraw/data/image";
 | |
| import { serializeAsJSON } from "../excalidraw/data/json";
 | |
| import {
 | |
|   copyBlobToClipboardAsPng,
 | |
|   copyTextToSystemClipboard,
 | |
|   copyToClipboard,
 | |
| } from "../excalidraw/clipboard";
 | |
| 
 | |
| export { MIME_TYPES };
 | |
| 
 | |
| type ExportOpts = {
 | |
|   elements: readonly NonDeleted<ExcalidrawElement>[];
 | |
|   appState?: Partial<Omit<AppState, "offsetTop" | "offsetLeft">>;
 | |
|   files: BinaryFiles | null;
 | |
|   maxWidthOrHeight?: number;
 | |
|   exportingFrame?: ExcalidrawFrameLikeElement | null;
 | |
|   getDimensions?: (
 | |
|     width: number,
 | |
|     height: number,
 | |
|   ) => { width: number; height: number; scale?: number };
 | |
| };
 | |
| 
 | |
| export const exportToCanvas = ({
 | |
|   elements,
 | |
|   appState,
 | |
|   files,
 | |
|   maxWidthOrHeight,
 | |
|   getDimensions,
 | |
|   exportPadding,
 | |
|   exportingFrame,
 | |
| }: ExportOpts & {
 | |
|   exportPadding?: number;
 | |
| }) => {
 | |
|   const { elements: restoredElements, appState: restoredAppState } = restore(
 | |
|     { elements, appState },
 | |
|     null,
 | |
|     null,
 | |
|   );
 | |
|   const { exportBackground, viewBackgroundColor } = restoredAppState;
 | |
|   return _exportToCanvas(
 | |
|     restoredElements,
 | |
|     { ...restoredAppState, offsetTop: 0, offsetLeft: 0, width: 0, height: 0 },
 | |
|     files || {},
 | |
|     { exportBackground, exportPadding, viewBackgroundColor, exportingFrame },
 | |
|     (width: number, height: number) => {
 | |
|       const canvas = document.createElement("canvas");
 | |
| 
 | |
|       if (maxWidthOrHeight) {
 | |
|         if (typeof getDimensions === "function") {
 | |
|           console.warn(
 | |
|             "`getDimensions()` is ignored when `maxWidthOrHeight` is supplied.",
 | |
|           );
 | |
|         }
 | |
| 
 | |
|         const max = Math.max(width, height);
 | |
| 
 | |
|         // if content is less then maxWidthOrHeight, fallback on supplied scale
 | |
|         const scale =
 | |
|           maxWidthOrHeight < max
 | |
|             ? maxWidthOrHeight / max
 | |
|             : appState?.exportScale ?? 1;
 | |
| 
 | |
|         canvas.width = width * scale;
 | |
|         canvas.height = height * scale;
 | |
| 
 | |
|         return {
 | |
|           canvas,
 | |
|           scale,
 | |
|         };
 | |
|       }
 | |
| 
 | |
|       const ret = getDimensions?.(width, height) || { width, height };
 | |
| 
 | |
|       canvas.width = ret.width;
 | |
|       canvas.height = ret.height;
 | |
| 
 | |
|       return {
 | |
|         canvas,
 | |
|         scale: ret.scale ?? 1,
 | |
|       };
 | |
|     },
 | |
|   );
 | |
| };
 | |
| 
 | |
| export const exportToBlob = async (
 | |
|   opts: ExportOpts & {
 | |
|     mimeType?: string;
 | |
|     quality?: number;
 | |
|     exportPadding?: number;
 | |
|   },
 | |
| ): Promise<Blob> => {
 | |
|   let { mimeType = MIME_TYPES.png, quality } = opts;
 | |
| 
 | |
|   if (mimeType === MIME_TYPES.png && typeof quality === "number") {
 | |
|     console.warn(`"quality" will be ignored for "${MIME_TYPES.png}" mimeType`);
 | |
|   }
 | |
| 
 | |
|   // typo in MIME type (should be "jpeg")
 | |
|   if (mimeType === "image/jpg") {
 | |
|     mimeType = MIME_TYPES.jpg;
 | |
|   }
 | |
| 
 | |
|   if (mimeType === MIME_TYPES.jpg && !opts.appState?.exportBackground) {
 | |
|     console.warn(
 | |
|       `Defaulting "exportBackground" to "true" for "${MIME_TYPES.jpg}" mimeType`,
 | |
|     );
 | |
|     opts = {
 | |
|       ...opts,
 | |
|       appState: { ...opts.appState, exportBackground: true },
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   const canvas = await exportToCanvas(opts);
 | |
| 
 | |
|   quality = quality ? quality : /image\/jpe?g/.test(mimeType) ? 0.92 : 0.8;
 | |
| 
 | |
|   return new Promise((resolve, reject) => {
 | |
|     canvas.toBlob(
 | |
|       async (blob) => {
 | |
|         if (!blob) {
 | |
|           return reject(new Error("couldn't export to blob"));
 | |
|         }
 | |
|         if (
 | |
|           blob &&
 | |
|           mimeType === MIME_TYPES.png &&
 | |
|           opts.appState?.exportEmbedScene
 | |
|         ) {
 | |
|           blob = await encodePngMetadata({
 | |
|             blob,
 | |
|             metadata: serializeAsJSON(
 | |
|               // NOTE as long as we're using the Scene hack, we need to ensure
 | |
|               // we pass the original, uncloned elements when serializing
 | |
|               // so that we keep ids stable
 | |
|               opts.elements,
 | |
|               opts.appState,
 | |
|               opts.files || {},
 | |
|               "local",
 | |
|             ),
 | |
|           });
 | |
|         }
 | |
|         resolve(blob);
 | |
|       },
 | |
|       mimeType,
 | |
|       quality,
 | |
|     );
 | |
|   });
 | |
| };
 | |
| 
 | |
| export const exportToSvg = async ({
 | |
|   elements,
 | |
|   appState = getDefaultAppState(),
 | |
|   files = {},
 | |
|   exportPadding,
 | |
|   renderEmbeddables,
 | |
|   exportingFrame,
 | |
|   skipInliningFonts,
 | |
| }: Omit<ExportOpts, "getDimensions"> & {
 | |
|   exportPadding?: number;
 | |
|   renderEmbeddables?: boolean;
 | |
|   skipInliningFonts?: true;
 | |
| }): Promise<SVGSVGElement> => {
 | |
|   const { elements: restoredElements, appState: restoredAppState } = restore(
 | |
|     { elements, appState },
 | |
|     null,
 | |
|     null,
 | |
|   );
 | |
| 
 | |
|   const exportAppState = {
 | |
|     ...restoredAppState,
 | |
|     exportPadding,
 | |
|   };
 | |
| 
 | |
|   return _exportToSvg(restoredElements, exportAppState, files, {
 | |
|     exportingFrame,
 | |
|     renderEmbeddables,
 | |
|     skipInliningFonts,
 | |
|   });
 | |
| };
 | |
| 
 | |
| export const exportToClipboard = async (
 | |
|   opts: ExportOpts & {
 | |
|     mimeType?: string;
 | |
|     quality?: number;
 | |
|     type: "png" | "svg" | "json";
 | |
|   },
 | |
| ) => {
 | |
|   if (opts.type === "svg") {
 | |
|     const svg = await exportToSvg(opts);
 | |
|     await copyTextToSystemClipboard(svg.outerHTML);
 | |
|   } else if (opts.type === "png") {
 | |
|     await copyBlobToClipboardAsPng(exportToBlob(opts));
 | |
|   } else if (opts.type === "json") {
 | |
|     await copyToClipboard(opts.elements, opts.files);
 | |
|   } else {
 | |
|     throw new Error("Invalid export type");
 | |
|   }
 | |
| };
 | 
