2021-10-21 22:05:48 +02:00
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// 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 });
|
2021-11-02 14:24:16 +02:00
|
|
|
} catch (error: any) {
|
2021-10-21 22:05:48 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2022-04-15 12:05:10 +02:00
|
|
|
if (!svg.hasAttribute("width") || !svg.hasAttribute("height")) {
|
|
|
|
const viewBox = svg.getAttribute("viewBox");
|
|
|
|
let width = svg.getAttribute("width") || "50";
|
|
|
|
let height = svg.getAttribute("height") || "50";
|
|
|
|
if (viewBox) {
|
|
|
|
const match = viewBox.match(/\d+ +\d+ +(\d+) +(\d+)/);
|
|
|
|
if (match) {
|
|
|
|
[, width, height] = match;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
svg.setAttribute("width", width);
|
|
|
|
svg.setAttribute("height", height);
|
|
|
|
}
|
|
|
|
|
2021-10-21 22:05:48 +02:00
|
|
|
return svg.outerHTML;
|
|
|
|
}
|
|
|
|
};
|