export.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. import {
  2. exportToCanvas as _exportToCanvas,
  3. exportToSvg as _exportToSvg,
  4. } from "../excalidraw/scene/export";
  5. import { getDefaultAppState } from "../excalidraw/appState";
  6. import { AppState, BinaryFiles } from "../excalidraw/types";
  7. import {
  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. }: Omit<ExportOpts, "getDimensions"> & {
  151. exportPadding?: number;
  152. renderEmbeddables?: boolean;
  153. }): Promise<SVGSVGElement> => {
  154. const { elements: restoredElements, appState: restoredAppState } = restore(
  155. { elements, appState },
  156. null,
  157. null,
  158. );
  159. const exportAppState = {
  160. ...restoredAppState,
  161. exportPadding,
  162. };
  163. return _exportToSvg(restoredElements, exportAppState, files, {
  164. exportingFrame,
  165. renderEmbeddables,
  166. });
  167. };
  168. export const exportToClipboard = async (
  169. opts: ExportOpts & {
  170. mimeType?: string;
  171. quality?: number;
  172. type: "png" | "svg" | "json";
  173. },
  174. ) => {
  175. if (opts.type === "svg") {
  176. const svg = await exportToSvg(opts);
  177. await copyTextToSystemClipboard(svg.outerHTML);
  178. } else if (opts.type === "png") {
  179. await copyBlobToClipboardAsPng(exportToBlob(opts));
  180. } else if (opts.type === "json") {
  181. await copyToClipboard(opts.elements, opts.files);
  182. } else {
  183. throw new Error("Invalid export type");
  184. }
  185. };
  186. export * from "./bbox";
  187. export {
  188. elementsOverlappingBBox,
  189. isElementInsideBBox,
  190. elementPartiallyOverlapsWithOrContainsBBox,
  191. } from "./withinBounds";
  192. export {
  193. serializeAsJSON,
  194. serializeLibraryAsJSON,
  195. } from "../excalidraw/data/json";
  196. export {
  197. loadFromBlob,
  198. loadSceneOrLibraryFromBlob,
  199. loadLibraryFromBlob,
  200. } from "../excalidraw/data/blob";
  201. export { getFreeDrawSvgPath } from "../excalidraw/renderer/renderElement";
  202. export { mergeLibraryItems } from "../excalidraw/data/library";