export.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import {
  2. exportToCanvas as _exportToCanvas,
  3. type ExportToCanvasConfig,
  4. type ExportToCanvasData,
  5. exportToSvg as _exportToSvg,
  6. } from "../excalidraw/scene/export";
  7. import { restore } from "../excalidraw/data/restore";
  8. import { COLOR_WHITE, MIME_TYPES } from "../excalidraw/constants";
  9. import { encodePngMetadata } from "../excalidraw/data/image";
  10. import { serializeAsJSON } from "../excalidraw/data/json";
  11. import {
  12. copyBlobToClipboardAsPng,
  13. copyTextToSystemClipboard,
  14. copyToClipboard,
  15. } from "../excalidraw/clipboard";
  16. import { getNonDeletedElements } from "../excalidraw";
  17. export { MIME_TYPES };
  18. type ExportToBlobConfig = ExportToCanvasConfig & {
  19. mimeType?: string;
  20. quality?: number;
  21. };
  22. type ExportToSvgConfig = Pick<
  23. ExportToCanvasConfig,
  24. "canvasBackgroundColor" | "padding" | "theme" | "exportingFrame"
  25. > & {
  26. /**
  27. * if true, all embeddables passed in will be rendered when possible.
  28. */
  29. renderEmbeddables?: boolean;
  30. skipInliningFonts?: true;
  31. reuseImages?: boolean;
  32. };
  33. export const exportToCanvas = async ({
  34. data,
  35. config,
  36. }: {
  37. data: ExportToCanvasData;
  38. config?: ExportToCanvasConfig;
  39. }) => {
  40. return _exportToCanvas({
  41. data,
  42. config,
  43. });
  44. };
  45. export const exportToBlob = async ({
  46. data,
  47. config,
  48. }: {
  49. data: ExportToCanvasData;
  50. config?: ExportToBlobConfig;
  51. }): Promise<Blob> => {
  52. let { mimeType = MIME_TYPES.png, quality } = config || {};
  53. if (mimeType === MIME_TYPES.png && typeof quality === "number") {
  54. console.warn(`"quality" will be ignored for "${MIME_TYPES.png}" mimeType`);
  55. }
  56. // typo in MIME type (should be "jpeg")
  57. if (mimeType === "image/jpg") {
  58. mimeType = MIME_TYPES.jpg;
  59. }
  60. if (mimeType === MIME_TYPES.jpg && !config?.canvasBackgroundColor === false) {
  61. console.warn(
  62. `Defaulting "exportBackground" to "true" for "${MIME_TYPES.jpg}" mimeType`,
  63. );
  64. config = {
  65. ...config,
  66. canvasBackgroundColor: data.appState?.viewBackgroundColor || COLOR_WHITE,
  67. };
  68. }
  69. const canvas = await _exportToCanvas({ data, config });
  70. quality = quality ? quality : /image\/jpe?g/.test(mimeType) ? 0.92 : 0.8;
  71. return new Promise((resolve, reject) => {
  72. canvas.toBlob(
  73. async (blob) => {
  74. if (!blob) {
  75. return reject(new Error("couldn't export to blob"));
  76. }
  77. if (
  78. blob &&
  79. mimeType === MIME_TYPES.png &&
  80. data.appState?.exportEmbedScene
  81. ) {
  82. blob = await encodePngMetadata({
  83. blob,
  84. metadata: serializeAsJSON(
  85. // NOTE as long as we're using the Scene hack, we need to ensure
  86. // we pass the original, uncloned elements when serializing
  87. // so that we keep ids stable
  88. data.elements,
  89. data.appState,
  90. data.files || {},
  91. "local",
  92. ),
  93. });
  94. }
  95. resolve(blob);
  96. },
  97. mimeType,
  98. quality,
  99. );
  100. });
  101. };
  102. export const exportToSvg = async ({
  103. data,
  104. config,
  105. }: {
  106. data: ExportToCanvasData;
  107. config?: ExportToSvgConfig;
  108. }): Promise<SVGSVGElement> => {
  109. const { elements: restoredElements, appState: restoredAppState } = restore(
  110. { ...data, files: data.files || {} },
  111. null,
  112. null,
  113. );
  114. const appState = { ...restoredAppState, exportPadding: config?.padding };
  115. const elements = getNonDeletedElements(restoredElements);
  116. const files = data.files || {};
  117. return _exportToSvg({
  118. data: { elements, appState, files },
  119. config: {
  120. exportingFrame: config?.exportingFrame,
  121. renderEmbeddables: config?.renderEmbeddables,
  122. skipInliningFonts: config?.skipInliningFonts,
  123. reuseImages: config?.reuseImages,
  124. },
  125. });
  126. };
  127. export const exportToClipboard = async ({
  128. type,
  129. data,
  130. config,
  131. }: {
  132. data: ExportToCanvasData;
  133. } & (
  134. | { type: "png"; config?: ExportToBlobConfig }
  135. | { type: "svg"; config?: ExportToSvgConfig }
  136. | { type: "json"; config?: never }
  137. )) => {
  138. if (type === "svg") {
  139. const svg = await exportToSvg({ data, config });
  140. await copyTextToSystemClipboard(svg.outerHTML);
  141. } else if (type === "png") {
  142. await copyBlobToClipboardAsPng(exportToBlob({ data, config }));
  143. } else if (type === "json") {
  144. await copyToClipboard(data.elements, data.files);
  145. } else {
  146. throw new Error("Invalid export type");
  147. }
  148. };