diff options
Diffstat (limited to 'packages/excalidraw/element/image.ts')
| -rw-r--r-- | packages/excalidraw/element/image.ts | 146 |
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; + } +}; |
