excalidraw/src/element/image.ts

112 lines
3.4 KiB
TypeScript

// -----------------------------------------------------------------------------
// ExcalidrawImageElement & related helpers
// -----------------------------------------------------------------------------
import { MIME_TYPES, SVG_NS } from "../constants";
import { t } from "../i18n";
import { AppClassProperties, DataURL, BinaryFiles } from "../types";
import { isInitializedImageElement } from "./typeChecks";
import {
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 = async (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(t("errors.invalidSVGString"));
} else {
if (!svg.hasAttribute("xmlns")) {
svg.setAttribute("xmlns", SVG_NS);
}
return svg.outerHTML;
}
};