image.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. // -----------------------------------------------------------------------------
  2. // ExcalidrawImageElement & related helpers
  3. // -----------------------------------------------------------------------------
  4. import { MIME_TYPES, SVG_NS } from "../constants";
  5. import { t } from "../i18n";
  6. import { AppClassProperties, DataURL, BinaryFiles } from "../types";
  7. import { isInitializedImageElement } from "./typeChecks";
  8. import {
  9. ExcalidrawElement,
  10. FileId,
  11. InitializedExcalidrawImageElement,
  12. } from "./types";
  13. export const loadHTMLImageElement = (dataURL: DataURL) => {
  14. return new Promise<HTMLImageElement>((resolve, reject) => {
  15. const image = new Image();
  16. image.onload = () => {
  17. resolve(image);
  18. };
  19. image.onerror = (error) => {
  20. reject(error);
  21. };
  22. image.src = dataURL;
  23. });
  24. };
  25. /** NOTE: updates cache even if already populated with given image. Thus,
  26. * you should filter out the images upstream if you want to optimize this. */
  27. export const updateImageCache = async ({
  28. fileIds,
  29. files,
  30. imageCache,
  31. }: {
  32. fileIds: FileId[];
  33. files: BinaryFiles;
  34. imageCache: AppClassProperties["imageCache"];
  35. }) => {
  36. const updatedFiles = new Map<FileId, true>();
  37. const erroredFiles = new Map<FileId, true>();
  38. await Promise.all(
  39. fileIds.reduce((promises, fileId) => {
  40. const fileData = files[fileId as string];
  41. if (fileData && !updatedFiles.has(fileId)) {
  42. updatedFiles.set(fileId, true);
  43. return promises.concat(
  44. (async () => {
  45. try {
  46. if (fileData.mimeType === MIME_TYPES.binary) {
  47. throw new Error("Only images can be added to ImageCache");
  48. }
  49. const imagePromise = loadHTMLImageElement(fileData.dataURL);
  50. const data = {
  51. image: imagePromise,
  52. mimeType: fileData.mimeType,
  53. } as const;
  54. // store the promise immediately to indicate there's an in-progress
  55. // initialization
  56. imageCache.set(fileId, data);
  57. const image = await imagePromise;
  58. imageCache.set(fileId, { ...data, image });
  59. } catch (error: any) {
  60. erroredFiles.set(fileId, true);
  61. }
  62. })(),
  63. );
  64. }
  65. return promises;
  66. }, [] as Promise<any>[]),
  67. );
  68. return {
  69. imageCache,
  70. /** includes errored files because they cache was updated nonetheless */
  71. updatedFiles,
  72. /** files that failed when creating HTMLImageElement */
  73. erroredFiles,
  74. };
  75. };
  76. export const getInitializedImageElements = (
  77. elements: readonly ExcalidrawElement[],
  78. ) =>
  79. elements.filter((element) =>
  80. isInitializedImageElement(element),
  81. ) as InitializedExcalidrawImageElement[];
  82. export const isHTMLSVGElement = (node: Node | null): node is SVGElement => {
  83. // lower-casing due to XML/HTML convention differences
  84. // https://johnresig.com/blog/nodename-case-sensitivity
  85. return node?.nodeName.toLowerCase() === "svg";
  86. };
  87. export const normalizeSVG = async (SVGString: string) => {
  88. const doc = new DOMParser().parseFromString(SVGString, MIME_TYPES.svg);
  89. const svg = doc.querySelector("svg");
  90. const errorNode = doc.querySelector("parsererror");
  91. if (errorNode || !isHTMLSVGElement(svg)) {
  92. throw new Error(t("errors.invalidSVGString"));
  93. } else {
  94. if (!svg.hasAttribute("xmlns")) {
  95. svg.setAttribute("xmlns", SVG_NS);
  96. }
  97. if (!svg.hasAttribute("width") || !svg.hasAttribute("height")) {
  98. const viewBox = svg.getAttribute("viewBox");
  99. let width = svg.getAttribute("width") || "50";
  100. let height = svg.getAttribute("height") || "50";
  101. if (viewBox) {
  102. const match = viewBox.match(/\d+ +\d+ +(\d+) +(\d+)/);
  103. if (match) {
  104. [, width, height] = match;
  105. }
  106. }
  107. svg.setAttribute("width", width);
  108. svg.setAttribute("height", height);
  109. }
  110. return svg.outerHTML;
  111. }
  112. };