export.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import {
  2. exportToCanvas as _exportToCanvas,
  3. exportToSvg as _exportToSvg,
  4. } from "../excalidraw/scene/export";
  5. import { getDefaultAppState } from "../excalidraw/appState";
  6. import type { AppState, BinaryFiles } from "../excalidraw/types";
  7. import type {
  8. ExcalidrawElement,
  9. ExcalidrawFrameLikeElement,
  10. NonDeleted,
  11. } from "../excalidraw/element/types";
  12. import { restore } from "../excalidraw/data/restore";
  13. import { MIME_TYPES } from "../excalidraw/constants";
  14. import { encodePngMetadata } from "../excalidraw/data/image";
  15. import { serializeAsJSON } from "../excalidraw/data/json";
  16. import {
  17. copyBlobToClipboardAsPng,
  18. copyTextToSystemClipboard,
  19. copyToClipboard,
  20. } from "../excalidraw/clipboard";
  21. export { MIME_TYPES };
  22. type ExportOpts = {
  23. elements: readonly NonDeleted<ExcalidrawElement>[];
  24. appState?: Partial<Omit<AppState, "offsetTop" | "offsetLeft">>;
  25. files: BinaryFiles | null;
  26. maxWidthOrHeight?: number;
  27. exportingFrame?: ExcalidrawFrameLikeElement | null;
  28. getDimensions?: (
  29. width: number,
  30. height: number,
  31. ) => { width: number; height: number; scale?: number };
  32. };
  33. export const exportToCanvas = ({
  34. elements,
  35. appState,
  36. files,
  37. maxWidthOrHeight,
  38. getDimensions,
  39. exportPadding,
  40. exportingFrame,
  41. }: ExportOpts & {
  42. exportPadding?: number;
  43. }) => {
  44. const { elements: restoredElements, appState: restoredAppState } = restore(
  45. { elements, appState },
  46. null,
  47. null,
  48. );
  49. const { exportBackground, viewBackgroundColor } = restoredAppState;
  50. return _exportToCanvas(
  51. restoredElements,
  52. { ...restoredAppState, offsetTop: 0, offsetLeft: 0, width: 0, height: 0 },
  53. files || {},
  54. { exportBackground, exportPadding, viewBackgroundColor, exportingFrame },
  55. (width: number, height: number) => {
  56. const canvas = document.createElement("canvas");
  57. if (maxWidthOrHeight) {
  58. if (typeof getDimensions === "function") {
  59. console.warn(
  60. "`getDimensions()` is ignored when `maxWidthOrHeight` is supplied.",
  61. );
  62. }
  63. const max = Math.max(width, height);
  64. // if content is less then maxWidthOrHeight, fallback on supplied scale
  65. const scale =
  66. maxWidthOrHeight < max
  67. ? maxWidthOrHeight / max
  68. : appState?.exportScale ?? 1;
  69. canvas.width = width * scale;
  70. canvas.height = height * scale;
  71. return {
  72. canvas,
  73. scale,
  74. };
  75. }
  76. const ret = getDimensions?.(width, height) || { width, height };
  77. canvas.width = ret.width;
  78. canvas.height = ret.height;
  79. return {
  80. canvas,
  81. scale: ret.scale ?? 1,
  82. };
  83. },
  84. );
  85. };
  86. export const exportToBlob = async (
  87. opts: ExportOpts & {
  88. mimeType?: string;
  89. quality?: number;
  90. exportPadding?: number;
  91. },
  92. ): Promise<Blob> => {
  93. let { mimeType = MIME_TYPES.png, quality } = opts;
  94. if (mimeType === MIME_TYPES.png && typeof quality === "number") {
  95. console.warn(`"quality" will be ignored for "${MIME_TYPES.png}" mimeType`);
  96. }
  97. // typo in MIME type (should be "jpeg")
  98. if (mimeType === "image/jpg") {
  99. mimeType = MIME_TYPES.jpg;
  100. }
  101. if (mimeType === MIME_TYPES.jpg && !opts.appState?.exportBackground) {
  102. console.warn(
  103. `Defaulting "exportBackground" to "true" for "${MIME_TYPES.jpg}" mimeType`,
  104. );
  105. opts = {
  106. ...opts,
  107. appState: { ...opts.appState, exportBackground: true },
  108. };
  109. }
  110. const canvas = await exportToCanvas(opts);
  111. quality = quality ? quality : /image\/jpe?g/.test(mimeType) ? 0.92 : 0.8;
  112. return new Promise((resolve, reject) => {
  113. canvas.toBlob(
  114. async (blob) => {
  115. if (!blob) {
  116. return reject(new Error("couldn't export to blob"));
  117. }
  118. if (
  119. blob &&
  120. mimeType === MIME_TYPES.png &&
  121. opts.appState?.exportEmbedScene
  122. ) {
  123. blob = await encodePngMetadata({
  124. blob,
  125. metadata: serializeAsJSON(
  126. // NOTE as long as we're using the Scene hack, we need to ensure
  127. // we pass the original, uncloned elements when serializing
  128. // so that we keep ids stable
  129. opts.elements,
  130. opts.appState,
  131. opts.files || {},
  132. "local",
  133. ),
  134. });
  135. }
  136. resolve(blob);
  137. },
  138. mimeType,
  139. quality,
  140. );
  141. });
  142. };
  143. export const exportToSvg = async ({
  144. elements,
  145. appState = getDefaultAppState(),
  146. files = {},
  147. exportPadding,
  148. renderEmbeddables,
  149. exportingFrame,
  150. skipInliningFonts,
  151. reuseImages,
  152. }: Omit<ExportOpts, "getDimensions"> & {
  153. exportPadding?: number;
  154. renderEmbeddables?: boolean;
  155. skipInliningFonts?: true;
  156. reuseImages?: boolean;
  157. }): Promise<SVGSVGElement> => {
  158. const { elements: restoredElements, appState: restoredAppState } = restore(
  159. { elements, appState },
  160. null,
  161. null,
  162. );
  163. const exportAppState = {
  164. ...restoredAppState,
  165. exportPadding,
  166. };
  167. return _exportToSvg(restoredElements, exportAppState, files, {
  168. exportingFrame,
  169. renderEmbeddables,
  170. skipInliningFonts,
  171. reuseImages,
  172. });
  173. };
  174. export const exportToClipboard = async (
  175. opts: ExportOpts & {
  176. mimeType?: string;
  177. quality?: number;
  178. type: "png" | "svg" | "json";
  179. },
  180. ) => {
  181. if (opts.type === "svg") {
  182. const svg = await exportToSvg(opts);
  183. await copyTextToSystemClipboard(svg.outerHTML);
  184. } else if (opts.type === "png") {
  185. await copyBlobToClipboardAsPng(exportToBlob(opts));
  186. } else if (opts.type === "json") {
  187. await copyToClipboard(opts.elements, opts.files);
  188. } else {
  189. throw new Error("Invalid export type");
  190. }
  191. };