export.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  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. },
  64. {
  65. renderScrollbars: false,
  66. renderSelection: false,
  67. renderOptimizations: false,
  68. renderGrid: false,
  69. },
  70. );
  71. return tempCanvas;
  72. };
  73. export const exportToSvg = (
  74. elements: readonly NonDeletedExcalidrawElement[],
  75. {
  76. exportBackground,
  77. exportPadding = 10,
  78. viewBackgroundColor,
  79. scale = 1,
  80. shouldAddWatermark,
  81. metadata = "",
  82. }: {
  83. exportBackground: boolean;
  84. exportPadding?: number;
  85. scale?: number;
  86. viewBackgroundColor: string;
  87. shouldAddWatermark: boolean;
  88. metadata?: string;
  89. },
  90. ): SVGSVGElement => {
  91. const sceneElements = getElementsAndWatermark(elements, shouldAddWatermark);
  92. const [minX, minY, width, height] = getCanvasSize(
  93. sceneElements,
  94. exportPadding,
  95. shouldAddWatermark,
  96. );
  97. // initialze SVG root
  98. const svgRoot = document.createElementNS(SVG_NS, "svg");
  99. svgRoot.setAttribute("version", "1.1");
  100. svgRoot.setAttribute("xmlns", SVG_NS);
  101. svgRoot.setAttribute("viewBox", `0 0 ${width} ${height}`);
  102. svgRoot.setAttribute("width", `${width * scale}`);
  103. svgRoot.setAttribute("height", `${height * scale}`);
  104. svgRoot.innerHTML = `
  105. ${SVG_EXPORT_TAG}
  106. ${metadata}
  107. <defs>
  108. <style>
  109. @font-face {
  110. font-family: "Virgil";
  111. src: url("https://excalidraw.com/FG_Virgil.woff2");
  112. }
  113. @font-face {
  114. font-family: "Cascadia";
  115. src: url("https://excalidraw.com/Cascadia.woff2");
  116. }
  117. </style>
  118. </defs>
  119. `;
  120. // render background rect
  121. if (exportBackground && viewBackgroundColor) {
  122. const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect");
  123. rect.setAttribute("x", "0");
  124. rect.setAttribute("y", "0");
  125. rect.setAttribute("width", `${width}`);
  126. rect.setAttribute("height", `${height}`);
  127. rect.setAttribute("fill", viewBackgroundColor);
  128. svgRoot.appendChild(rect);
  129. }
  130. const rsvg = rough.svg(svgRoot);
  131. renderSceneToSvg(sceneElements, rsvg, svgRoot, {
  132. offsetX: -minX + exportPadding,
  133. offsetY: -minY + exportPadding,
  134. });
  135. return svgRoot;
  136. };
  137. const getElementsAndWatermark = (
  138. elements: readonly NonDeletedExcalidrawElement[],
  139. shouldAddWatermark: boolean,
  140. ): readonly NonDeletedExcalidrawElement[] => {
  141. let _elements = [...elements];
  142. if (shouldAddWatermark) {
  143. const [, , maxX, maxY] = getCommonBounds(elements);
  144. _elements = [..._elements, getWatermarkElement(maxX, maxY)];
  145. }
  146. return _elements;
  147. };
  148. const getWatermarkElement = (maxX: number, maxY: number) => {
  149. return newTextElement({
  150. text: t("labels.madeWithExcalidraw"),
  151. fontSize: WATERMARK_HEIGHT,
  152. fontFamily: DEFAULT_FONT_FAMILY,
  153. textAlign: "right",
  154. verticalAlign: DEFAULT_VERTICAL_ALIGN,
  155. x: maxX,
  156. y: maxY + WATERMARK_HEIGHT,
  157. strokeColor: oc.gray[5],
  158. backgroundColor: "transparent",
  159. fillStyle: "hachure",
  160. strokeWidth: 1,
  161. strokeStyle: "solid",
  162. roughness: 1,
  163. opacity: 100,
  164. strokeSharpness: "sharp",
  165. });
  166. };
  167. // calculate smallest area to fit the contents in
  168. const getCanvasSize = (
  169. elements: readonly NonDeletedExcalidrawElement[],
  170. exportPadding: number,
  171. shouldAddWatermark: boolean,
  172. ): [number, number, number, number] => {
  173. const [minX, minY, maxX, maxY] = getCommonBounds(elements);
  174. const width = distance(minX, maxX) + exportPadding * 2;
  175. const height =
  176. distance(minY, maxY) +
  177. exportPadding +
  178. (shouldAddWatermark ? 0 : exportPadding);
  179. return [minX, minY, width, height];
  180. };
  181. export const getExportSize = (
  182. elements: readonly NonDeletedExcalidrawElement[],
  183. exportPadding: number,
  184. shouldAddWatermark: boolean,
  185. scale: number,
  186. ): [number, number] => {
  187. const sceneElements = getElementsAndWatermark(elements, shouldAddWatermark);
  188. const [, , width, height] = getCanvasSize(
  189. sceneElements,
  190. exportPadding,
  191. shouldAddWatermark,
  192. ).map((dimension) => Math.trunc(dimension * scale));
  193. return [width, height];
  194. };