export.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  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, measureText } from "../utils";
  8. import { normalizeScroll } from "./scroll";
  9. import { AppState } from "../types";
  10. import { t } from "../i18n";
  11. export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
  12. export function exportToCanvas(
  13. elements: readonly NonDeletedExcalidrawElement[],
  14. appState: AppState,
  15. {
  16. exportBackground,
  17. exportPadding = 10,
  18. viewBackgroundColor,
  19. scale = 1,
  20. shouldAddWatermark,
  21. }: {
  22. exportBackground: boolean;
  23. exportPadding?: number;
  24. scale?: number;
  25. viewBackgroundColor: string;
  26. shouldAddWatermark: boolean;
  27. },
  28. createCanvas: (width: number, height: number) => any = function (
  29. width,
  30. height,
  31. ) {
  32. const tempCanvas = document.createElement("canvas");
  33. tempCanvas.width = width * scale;
  34. tempCanvas.height = height * scale;
  35. return tempCanvas;
  36. },
  37. ) {
  38. let sceneElements = elements;
  39. if (shouldAddWatermark) {
  40. const [, , maxX, maxY] = getCommonBounds(elements);
  41. sceneElements = [...sceneElements, getWatermarkElement(maxX, maxY)];
  42. }
  43. // calculate smallest area to fit the contents in
  44. const [minX, minY, maxX, maxY] = getCommonBounds(sceneElements);
  45. const width = distance(minX, maxX) + exportPadding * 2;
  46. const height =
  47. distance(minY, maxY) +
  48. exportPadding +
  49. (shouldAddWatermark ? 0 : exportPadding);
  50. const tempCanvas: any = createCanvas(width, height);
  51. renderScene(
  52. sceneElements,
  53. appState,
  54. null,
  55. scale,
  56. rough.canvas(tempCanvas),
  57. tempCanvas,
  58. {
  59. viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
  60. scrollX: normalizeScroll(-minX + exportPadding),
  61. scrollY: normalizeScroll(-minY + exportPadding),
  62. zoom: 1,
  63. remotePointerViewportCoords: {},
  64. remoteSelectedElementIds: {},
  65. shouldCacheIgnoreZoom: false,
  66. remotePointerUsernames: {},
  67. },
  68. {
  69. renderScrollbars: false,
  70. renderSelection: false,
  71. renderOptimizations: false,
  72. },
  73. );
  74. return tempCanvas;
  75. }
  76. export function exportToSvg(
  77. elements: readonly NonDeletedExcalidrawElement[],
  78. {
  79. exportBackground,
  80. exportPadding = 10,
  81. viewBackgroundColor,
  82. shouldAddWatermark,
  83. }: {
  84. exportBackground: boolean;
  85. exportPadding?: number;
  86. viewBackgroundColor: string;
  87. shouldAddWatermark: boolean;
  88. },
  89. ): SVGSVGElement {
  90. let sceneElements = elements;
  91. if (shouldAddWatermark) {
  92. const [, , maxX, maxY] = getCommonBounds(elements);
  93. sceneElements = [...sceneElements, getWatermarkElement(maxX, maxY)];
  94. }
  95. // calculate canvas dimensions
  96. const [minX, minY, maxX, maxY] = getCommonBounds(sceneElements);
  97. const width = distance(minX, maxX) + exportPadding * 2;
  98. const height =
  99. distance(minY, maxY) +
  100. exportPadding +
  101. (shouldAddWatermark ? 0 : exportPadding);
  102. // initialze SVG root
  103. const svgRoot = document.createElementNS(SVG_NS, "svg");
  104. svgRoot.setAttribute("version", "1.1");
  105. svgRoot.setAttribute("xmlns", SVG_NS);
  106. svgRoot.setAttribute("viewBox", `0 0 ${width} ${height}`);
  107. svgRoot.innerHTML = `
  108. ${SVG_EXPORT_TAG}
  109. <defs>
  110. <style>
  111. @font-face {
  112. font-family: "Virgil";
  113. src: url("https://excalidraw.com/FG_Virgil.woff2");
  114. }
  115. @font-face {
  116. font-family: "Cascadia";
  117. src: url("https://excalidraw.com/Cascadia.woff2");
  118. }
  119. </style>
  120. </defs>
  121. `;
  122. // render backgroiund rect
  123. if (exportBackground && viewBackgroundColor) {
  124. const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect");
  125. rect.setAttribute("x", "0");
  126. rect.setAttribute("y", "0");
  127. rect.setAttribute("width", `${width}`);
  128. rect.setAttribute("height", `${height}`);
  129. rect.setAttribute("fill", viewBackgroundColor);
  130. svgRoot.appendChild(rect);
  131. }
  132. const rsvg = rough.svg(svgRoot);
  133. renderSceneToSvg(sceneElements, rsvg, svgRoot, {
  134. offsetX: -minX + exportPadding,
  135. offsetY: -minY + exportPadding,
  136. });
  137. return svgRoot;
  138. }
  139. function getWatermarkElement(maxX: number, maxY: number) {
  140. const text = t("labels.madeWithExcalidraw");
  141. const font = "16px Virgil";
  142. const { width: textWidth } = measureText(text, font);
  143. return newTextElement({
  144. text,
  145. font,
  146. textAlign: "center",
  147. x: maxX - textWidth / 2,
  148. y: maxY + 16,
  149. strokeColor: oc.gray[5],
  150. backgroundColor: "transparent",
  151. fillStyle: "hachure",
  152. strokeWidth: 1,
  153. strokeStyle: "solid",
  154. roughness: 1,
  155. opacity: 100,
  156. });
  157. }