utils.ts 5.2 KB

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