112 lines
3.4 KiB
TypeScript
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) {
|
||
|
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;
|
||
|
}
|
||
|
};
|