export.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import rough from "roughjs/bin/rough";
  2. import oc from "open-color";
  3. import { newTextElement } from "../element";
  4. import { NonDeletedExcalidrawElement } from "../element/types";
  5. import { getCommonBounds } from "../element/bounds";
  6. import { renderScene, renderSceneToSvg } from "../renderer/renderScene";
  7. import { distance, SVG_NS } from "../utils";
  8. import { AppState } from "../types";
  9. import { t } from "../i18n";
  10. import { DEFAULT_FONT_FAMILY, DEFAULT_VERTICAL_ALIGN } from "../constants";
  11. import { getDefaultAppState } from "../appState";
  12. export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
  13. const WATERMARK_HEIGHT = 16;
  14. export const exportToCanvas = (
  15. elements: readonly NonDeletedExcalidrawElement[],
  16. appState: AppState,
  17. {
  18. exportBackground,
  19. exportPadding = 10,
  20. viewBackgroundColor,
  21. scale = 1,
  22. shouldAddWatermark,
  23. }: {
  24. exportBackground: boolean;
  25. exportPadding?: number;
  26. scale?: number;
  27. viewBackgroundColor: string;
  28. shouldAddWatermark: boolean;
  29. },
  30. createCanvas: (width: number, height: number) => HTMLCanvasElement = (
  31. width,
  32. height,
  33. ) => {
  34. const tempCanvas = document.createElement("canvas");
  35. tempCanvas.width = width * scale;
  36. tempCanvas.height = height * scale;
  37. return tempCanvas;
  38. },
  39. ) => {
  40. const sceneElements = getElementsAndWatermark(elements, shouldAddWatermark);
  41. const [minX, minY, width, height] = getCanvasSize(
  42. sceneElements,
  43. exportPadding,
  44. shouldAddWatermark,
  45. );
  46. const tempCanvas = createCanvas(width, height);
  47. renderScene(
  48. sceneElements,
  49. appState,
  50. null,
  51. scale,
  52. rough.canvas(tempCanvas),
  53. tempCanvas,
  54. {
  55. viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
  56. scrollX: -minX + exportPadding,
  57. scrollY: -minY + exportPadding,
  58. zoom: getDefaultAppState().zoom,
  59. remotePointerViewportCoords: {},
  60. remoteSelectedElementIds: {},
  61. shouldCacheIgnoreZoom: false,
  62. remotePointerUsernames: {},
  63. remotePointerUserStates: {},
  64. },
  65. {
  66. renderScrollbars: false,
  67. renderSelection: false,
  68. renderOptimizations: false,
  69. renderGrid: false,
  70. },
  71. );
  72. return tempCanvas;
  73. };
  74. export const exportToSvg = (
  75. elements: readonly NonDeletedExcalidrawElement[],
  76. {
  77. exportBackground,
  78. exportPadding = 10,
  79. viewBackgroundColor,
  80. scale = 1,
  81. shouldAddWatermark,
  82. metadata = "",
  83. }: {
  84. exportBackground: boolean;
  85. exportPadding?: number;
  86. scale?: number;
  87. viewBackgroundColor: string;
  88. shouldAddWatermark: boolean;
  89. metadata?: string;
  90. },
  91. ): SVGSVGElement => {
  92. const sceneElements = getElementsAndWatermark(elements, shouldAddWatermark);
  93. const [minX, minY, width, height] = getCanvasSize(
  94. sceneElements,
  95. exportPadding,
  96. shouldAddWatermark,
  97. );
  98. // initialze SVG root
  99. const svgRoot = document.createElementNS(SVG_NS, "svg");
  100. svgRoot.setAttribute("version", "1.1");
  101. svgRoot.setAttribute("xmlns", SVG_NS);
  102. svgRoot.setAttribute("viewBox", `0 0 ${width} ${height}`);
  103. svgRoot.setAttribute("width", `${width * scale}`);
  104. svgRoot.setAttribute("height", `${height * scale}`);
  105. svgRoot.innerHTML = `
  106. ${SVG_EXPORT_TAG}
  107. ${metadata}
  108. <defs>
  109. <style>
  110. @font-face {
  111. font-family: "Virgil";
  112. src: url("https://excalidraw.com/FG_Virgil.woff2");
  113. }
  114. @font-face {
  115. font-family: "Cascadia";
  116. src: url("https://excalidraw.com/Cascadia.woff2");
  117. }
  118. </style>
  119. </defs>
  120. `;
  121. // render background rect
  122. if (exportBackground && viewBackgroundColor) {
  123. const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect");
  124. rect.setAttribute("x", "0");
  125. rect.setAttribute("y", "0");
  126. rect.setAttribute("width", `${width}`);
  127. rect.setAttribute("height", `${height}`);
  128. rect.setAttribute("fill", viewBackgroundColor);
  129. svgRoot.appendChild(rect);
  130. }
  131. const rsvg = rough.svg(svgRoot);
  132. renderSceneToSvg(sceneElements, rsvg, svgRoot, {
  133. offsetX: -minX + exportPadding,
  134. offsetY: -minY + exportPadding,
  135. });
  136. return svgRoot;
  137. };
  138. const getElementsAndWatermark = (
  139. elements: readonly NonDeletedExcalidrawElement[],
  140. shouldAddWatermark: boolean,
  141. ): readonly NonDeletedExcalidrawElement[] => {
  142. let _elements = [...elements];
  143. if (shouldAddWatermark) {
  144. const [, , maxX, maxY] = getCommonBounds(elements);
  145. _elements = [..._elements, getWatermarkElement(maxX, maxY)];
  146. }
  147. return _elements;
  148. };
  149. const getWatermarkElement = (maxX: number, maxY: number) => {
  150. return newTextElement({
  151. text: t("labels.madeWithExcalidraw"),
  152. fontSize: WATERMARK_HEIGHT,
  153. fontFamily: DEFAULT_FONT_FAMILY,
  154. textAlign: "right",
  155. verticalAlign: DEFAULT_VERTICAL_ALIGN,
  156. x: maxX,
  157. y: maxY + WATERMARK_HEIGHT,
  158. strokeColor: oc.gray[5],
  159. backgroundColor: "transparent",
  160. fillStyle: "hachure",
  161. strokeWidth: 1,
  162. strokeStyle: "solid",
  163. roughness: 1,
  164. opacity: 100,
  165. strokeSharpness: "sharp",
  166. });
  167. };
  168. // calculate smallest area to fit the contents in
  169. const getCanvasSize = (
  170. elements: readonly NonDeletedExcalidrawElement[],
  171. exportPadding: number,
  172. shouldAddWatermark: boolean,
  173. ): [number, number, number, number] => {
  174. const [minX, minY, maxX, maxY] = getCommonBounds(elements);
  175. const width = distance(minX, maxX) + exportPadding * 2;
  176. const height =
  177. distance(minY, maxY) +
  178. exportPadding +
  179. (shouldAddWatermark ? 0 : exportPadding);
  180. return [minX, minY, width, height];
  181. };
  182. export const getExportSize = (
  183. elements: readonly NonDeletedExcalidrawElement[],
  184. exportPadding: number,
  185. shouldAddWatermark: boolean,
  186. scale: number,
  187. ): [number, number] => {
  188. const sceneElements = getElementsAndWatermark(elements, shouldAddWatermark);
  189. const [, , width, height] = getCanvasSize(
  190. sceneElements,
  191. exportPadding,
  192. shouldAddWatermark,
  193. ).map((dimension) => Math.trunc(dimension * scale));
  194. return [width, height];
  195. };