123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213 |
- 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,
- reuseImages,
- }: Omit<ExportOpts, "getDimensions"> & {
- exportPadding?: number;
- renderEmbeddables?: boolean;
- skipInliningFonts?: true;
- reuseImages?: boolean;
- }): Promise<SVGSVGElement> => {
- const { elements: restoredElements, appState: restoredAppState } = restore(
- { elements, appState },
- null,
- null,
- );
- const exportAppState = {
- ...restoredAppState,
- exportPadding,
- };
- return _exportToSvg(restoredElements, exportAppState, files, {
- exportingFrame,
- renderEmbeddables,
- skipInliningFonts,
- reuseImages,
- });
- };
- 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");
- }
- };
|