1
0
mirror of https://github.com/excalidraw/excalidraw.git synced 2024-11-10 11:35:52 +01:00
excalidraw/packages/utils/export.ts

211 lines
5.5 KiB
TypeScript
Raw Permalink Normal View History

2020-11-02 20:14:20 +01:00
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 {
2023-11-09 17:00:21 +01:00
ExcalidrawElement,
2023-11-23 23:07:53 +01:00
ExcalidrawFrameLikeElement,
2023-11-09 17:00:21 +01:00
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";
2020-11-02 20:14:20 +01:00
export { MIME_TYPES };
2020-11-02 20:14:20 +01:00
type ExportOpts = {
feat: Allow publishing libraries from UI (#4115) * feat: Allow publishing libraries from UI * Add status for each library item and show publish only for unpublished libs * Add publish library dialog * Pass the data to publish the library * pass lib blob * Handle old and new libraries when importing * Better error handling * Show publish success when library submitted for review * don't close library when publish success dialog open * Support multiple libs deletion and publish * Set status to published once library submitted for review * Save to LS after library published * unique key for publish and delete * fix layout shift when hover and also highlight selected library items * design improvements * migrate old library to the new one * fix * fix tests * use i18n * Support submit type in toolbutton * Use html5 form validation, add asteriks for required fields, add twitter handle, mark github handle optional * Add twitter handle in form state * revert html5 validation as fetch is giving some issues :/ * clarify types around LibraryItems * Add website optional field * event.preventDefault to make htm5 form validationw work * improve png generation by drawing a bounding box rect and aligining pngs to support multiple libs png * remove ts-ignore * add placeholders for fields * decrease clickable area for checkbox by 0.5em * add checkbox background color * rename `items` to `elements` * improve checkbox hit area * show selected library items in publish dialog * decrease dimensions by 3px to improve jerky experience when opening/closing library menu * Don't close publish dialog when clicked outside * Show selected library actions only when any library item selected and use icons instead of button * rename library to libraryItems in excalidrawLib and added migration * change icon and swap bg/color * use blue brand color for hover/selected states * prompt for confirmation when deleting library items * separate unpublished items from published * factor `LibraryMenu` into own file * i18n and minor fixes for unpublished items * fix not rendering empty cells when library empty * don't render published section if empty and unpublished is not * Add edit name functionality for library items * fix * edit lib name with onchange/blur * bump library version * prefer response error message * add library urls to ENV vars * mark lib item name as required * Use input only for lib item name * better error validation for lib items * fix label styling for lib items * design and i18n fixes * Save publish dialog data to local storage and clear once published * Add a note about MIT License * Add note for guidelines * Add tooltip for publish button * Show spinner in submit button when submission is in progress * assign id for older lib items when installed and set status as published for all lib when installed * update export icon and support export library for selected items * move LibraryMenuItems into its own component as its best to keep one comp per file * fix spec * Refactoring the library actions for reusablility * show only load when items not present * close on click outside in publish dialog * ad dialog description and tweak copy * vertically center input labels * align input styles * move author name input to other usernames * rename param * inline to simplify * fix to not inline `undefined` class names * fix version & include only latest lib schema in library export type * await response callback * refactor types * refactor * i18n * align casing & tweaks * move ls logic to publishLibrary * support removal of item inside publish dialog * fix labels for trash icon when items selected * replace window.confirm for removal libs with confirm dialog * fix input/textarea styling * move library item menu scss to its own file * use blue for load and cyan for publish * reduce margin for submit and make submit => Submit * Make library items header sticky * move publish icon to left so there is no jerkiness when unpublish items selected * update url * fix grid gap between lib items * Mark older items imported from initial data as unpublished * add text to publish button on non-mobile * add items counter * fix test * show personal and excal libs sections and personal goes first * show toast on adding to library via contextMenu * Animate plus icon and not the pending item * fix snap * use i18n when no item in publish dialog * tweak style of new lib item * show empty cells for both sections and set status as published for installed libs * fix * push selected item first in unpublished section * set status as published for imported from webiste but unpublished for json * Add items to the begining of library * add `created` library item attr * fix test * use `defaultValue` instead of `value` * fix dark theme styles * fix toggle button not closing library * close library menu on Escape * tweak publish dialog item remove style * fix remove icon in publish dialog Co-authored-by: dwelle <luzar.david@gmail.com>
2021-11-17 19:23:43 +01:00
elements: readonly NonDeleted<ExcalidrawElement>[];
appState?: Partial<Omit<AppState, "offsetTop" | "offsetLeft">>;
files: BinaryFiles | null;
maxWidthOrHeight?: number;
2023-11-23 23:07:53 +01:00
exportingFrame?: ExcalidrawFrameLikeElement | null;
getDimensions?: (
2020-11-02 20:14:20 +01:00
width: number,
height: number,
) => { width: number; height: number; scale?: number };
2020-11-02 20:14:20 +01:00
};
export const exportToCanvas = ({
2020-11-02 20:14:20 +01:00
elements,
appState,
files,
maxWidthOrHeight,
getDimensions,
exportPadding,
2023-11-09 17:00:21 +01:00
exportingFrame,
}: ExportOpts & {
exportPadding?: number;
}) => {
const { elements: restoredElements, appState: restoredAppState } = restore(
{ elements, appState },
null,
null,
);
const { exportBackground, viewBackgroundColor } = restoredAppState;
2020-11-02 20:14:20 +01:00
return _exportToCanvas(
2023-11-09 17:00:21 +01:00
restoredElements,
{ ...restoredAppState, offsetTop: 0, offsetLeft: 0, width: 0, height: 0 },
files || {},
2023-11-09 17:00:21 +01:00
{ exportBackground, exportPadding, viewBackgroundColor, exportingFrame },
2020-11-02 20:14:20 +01:00
(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 };
2020-11-02 20:14:20 +01:00
canvas.width = ret.width;
canvas.height = ret.height;
return {
canvas,
scale: ret.scale ?? 1,
};
2020-11-02 20:14:20 +01:00
},
);
};
export const exportToBlob = async (
2020-11-02 20:14:20 +01:00
opts: ExportOpts & {
mimeType?: string;
quality?: number;
exportPadding?: number;
2020-11-02 20:14:20 +01:00
},
): Promise<Blob> => {
let { mimeType = MIME_TYPES.png, quality } = opts;
2020-11-02 20:14:20 +01:00
if (mimeType === MIME_TYPES.png && typeof quality === "number") {
console.warn(`"quality" will be ignored for "${MIME_TYPES.png}" mimeType`);
2020-11-02 20:14:20 +01:00
}
// typo in MIME type (should be "jpeg")
2020-11-02 20:14:20 +01:00
if (mimeType === "image/jpg") {
mimeType = MIME_TYPES.jpg;
2020-11-02 20:14:20 +01:00
}
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 },
};
}
2023-11-09 17:00:21 +01:00
const canvas = await exportToCanvas(opts);
2020-11-02 20:14:20 +01:00
quality = quality ? quality : /image\/jpe?g/.test(mimeType) ? 0.92 : 0.8;
return new Promise((resolve, reject) => {
2020-11-02 20:14:20 +01:00
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",
),
});
}
2020-11-02 20:14:20 +01:00
resolve(blob);
},
mimeType,
quality,
);
});
};
export const exportToSvg = async ({
2020-11-02 20:14:20 +01:00
elements,
appState = getDefaultAppState(),
files = {},
2020-11-02 20:14:20 +01:00
exportPadding,
renderEmbeddables,
2023-11-09 17:00:21 +01:00
exportingFrame,
skipInliningFonts,
}: Omit<ExportOpts, "getDimensions"> & {
2020-11-02 20:14:20 +01:00
exportPadding?: number;
renderEmbeddables?: boolean;
skipInliningFonts?: true;
}): Promise<SVGSVGElement> => {
const { elements: restoredElements, appState: restoredAppState } = restore(
{ elements, appState },
null,
null,
);
const exportAppState = {
...restoredAppState,
exportPadding,
};
2023-11-09 17:00:21 +01:00
return _exportToSvg(restoredElements, exportAppState, files, {
exportingFrame,
renderEmbeddables,
skipInliningFonts,
2023-11-09 17:00:21 +01:00
});
2020-11-02 20:14:20 +01:00
};
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");
}
};