aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/element/image.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/excalidraw/element/image.ts')
-rw-r--r--packages/excalidraw/element/image.ts146
1 files changed, 146 insertions, 0 deletions
diff --git a/packages/excalidraw/element/image.ts b/packages/excalidraw/element/image.ts
new file mode 100644
index 0000000..32644b6
--- /dev/null
+++ b/packages/excalidraw/element/image.ts
@@ -0,0 +1,146 @@
+// -----------------------------------------------------------------------------
+// ExcalidrawImageElement & related helpers
+// -----------------------------------------------------------------------------
+
+import { MIME_TYPES, SVG_NS } from "../constants";
+import type { AppClassProperties, DataURL, BinaryFiles } from "../types";
+import { isInitializedImageElement } from "./typeChecks";
+import type {
+ ExcalidrawElement,
+ FileId,
+ InitializedExcalidrawImageElement,
+} from "./types";
+
+export const loadHTMLImageElement = (dataURL: DataURL) => {
+ return new Promise<HTMLImageElement>((resolve, reject) => {
+ const image = new Image();
+ image.onload = () => {
+ resolve(image);
+ };
+ image.onerror = (error) => {
+ reject(error);
+ };
+ image.src = dataURL;
+ });
+};
+
+/** NOTE: updates cache even if already populated with given image. Thus,
+ * you should filter out the images upstream if you want to optimize this. */
+export const updateImageCache = async ({
+ fileIds,
+ files,
+ imageCache,
+}: {
+ fileIds: FileId[];
+ files: BinaryFiles;
+ imageCache: AppClassProperties["imageCache"];
+}) => {
+ const updatedFiles = new Map<FileId, true>();
+ const erroredFiles = new Map<FileId, true>();
+
+ await Promise.all(
+ fileIds.reduce((promises, fileId) => {
+ const fileData = files[fileId as string];
+ if (fileData && !updatedFiles.has(fileId)) {
+ updatedFiles.set(fileId, true);
+ return promises.concat(
+ (async () => {
+ try {
+ if (fileData.mimeType === MIME_TYPES.binary) {
+ throw new Error("Only images can be added to ImageCache");
+ }
+
+ const imagePromise = loadHTMLImageElement(fileData.dataURL);
+ const data = {
+ image: imagePromise,
+ mimeType: fileData.mimeType,
+ } as const;
+ // store the promise immediately to indicate there's an in-progress
+ // initialization
+ imageCache.set(fileId, data);
+
+ const image = await imagePromise;
+
+ imageCache.set(fileId, { ...data, image });
+ } catch (error: any) {
+ erroredFiles.set(fileId, true);
+ }
+ })(),
+ );
+ }
+ return promises;
+ }, [] as Promise<any>[]),
+ );
+
+ return {
+ imageCache,
+ /** includes errored files because they cache was updated nonetheless */
+ updatedFiles,
+ /** files that failed when creating HTMLImageElement */
+ erroredFiles,
+ };
+};
+
+export const getInitializedImageElements = (
+ elements: readonly ExcalidrawElement[],
+) =>
+ elements.filter((element) =>
+ isInitializedImageElement(element),
+ ) as InitializedExcalidrawImageElement[];
+
+export const isHTMLSVGElement = (node: Node | null): node is SVGElement => {
+ // lower-casing due to XML/HTML convention differences
+ // https://johnresig.com/blog/nodename-case-sensitivity
+ return node?.nodeName.toLowerCase() === "svg";
+};
+
+export const normalizeSVG = (SVGString: string) => {
+ const doc = new DOMParser().parseFromString(SVGString, MIME_TYPES.svg);
+ const svg = doc.querySelector("svg");
+ const errorNode = doc.querySelector("parsererror");
+ if (errorNode || !isHTMLSVGElement(svg)) {
+ throw new Error("Invalid SVG");
+ } else {
+ if (!svg.hasAttribute("xmlns")) {
+ svg.setAttribute("xmlns", SVG_NS);
+ }
+
+ let width = svg.getAttribute("width");
+ let height = svg.getAttribute("height");
+
+ // Do not use % or auto values for width/height
+ // to avoid scaling issues when rendering at different sizes/zoom levels
+ if (width?.includes("%") || width === "auto") {
+ width = null;
+ }
+ if (height?.includes("%") || height === "auto") {
+ height = null;
+ }
+
+ const viewBox = svg.getAttribute("viewBox");
+
+ if (!width || !height) {
+ width = width || "50";
+ height = height || "50";
+
+ if (viewBox) {
+ const match = viewBox.match(
+ /\d+ +\d+ +(\d+(?:\.\d+)?) +(\d+(?:\.\d+)?)/,
+ );
+ if (match) {
+ [, width, height] = match;
+ }
+ }
+
+ svg.setAttribute("width", width);
+ svg.setAttribute("height", height);
+ }
+
+ // Make sure viewBox is set
+ if (!viewBox) {
+ svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
+ }
+
+ return svg.outerHTML;
+ }
+};