diff --git a/src/embed-images.ts b/src/embed-images.ts index 0d7b51ad..27536429 100644 --- a/src/embed-images.ts +++ b/src/embed-images.ts @@ -34,62 +34,131 @@ async function embedBackground( (await embedProp('-webkit-mask-image', clonedNode, options)) } +/** + * Embeds an image from a URL into an HTML or SVG image element by converting it to a data URL + * + * @param clonedNode - The HTML or SVG image element to embed the image into + * @param options - Configuration options for the embedding process + * @returns A promise that resolves when the embedding is complete + */ async function embedImageNode( clonedNode: T, options: Options, -) { - const isImageElement = isInstanceOfElement(clonedNode, HTMLImageElement) +): Promise { + // Check if node is an image element that needs embedding + const isHTMLImage = isInstanceOfElement(clonedNode, HTMLImageElement) + const isSVGImage = isInstanceOfElement(clonedNode, SVGImageElement) - if ( - !(isImageElement && !isDataUrl(clonedNode.src)) && - !( - isInstanceOfElement(clonedNode, SVGImageElement) && - !isDataUrl(clonedNode.href.baseVal) - ) - ) { + // Skip if not an image element or if already using a data URL + if (isHTMLImage && isDataUrl(clonedNode.src)) { + return + } + + if (isSVGImage && isDataUrl(clonedNode.href.baseVal)) { return } - const url = isImageElement ? clonedNode.src : clonedNode.href.baseVal + if (!isHTMLImage && !isSVGImage) { + return + } + + // Get the URL from the appropriate attribute based on element type + const url = isHTMLImage ? clonedNode.src : clonedNode.href.baseVal - const dataURL = await resourceToDataURL(url, getMimeType(url), options) - await new Promise((resolve, reject) => { - clonedNode.onload = resolve - clonedNode.onerror = options.onImageErrorHandler - ? (...attributes) => { + // Convert the resource to a data URL + const mimeType = getMimeType(url) + const dataURL = await resourceToDataURL(url, mimeType, options) + + // Handle different types of image elements + if (isHTMLImage) { + await updateHTMLImageElement( + clonedNode as HTMLImageElement, + dataURL, + options, + ) + } else if (isSVGImage) { + await updateSVGImageElement(clonedNode as SVGImageElement, dataURL) + } +} + +/** + * Updates an HTML image element with the data URL + * + * @param imgElement - The HTML image element to update + * @param dataURL - The data URL to set + * @param options - Configuration options + * @returns A promise that resolves when the image is loaded + */ +async function updateHTMLImageElement( + imgElement: HTMLImageElement, + dataURL: string, + options: Options, +): Promise { + return new Promise((resolve, reject) => { + // Create error handler function + const errorHandler = options.onImageErrorHandler + ? (event: Event) => { try { - resolve(options.onImageErrorHandler!(...attributes)) + const result = options.onImageErrorHandler!(event) + resolve() + return result } catch (error) { reject(error) + return undefined } } - : reject + : (event: Event) => { + reject(event) + return undefined + } - const image = clonedNode as HTMLImageElement - if (image.decode) { - image.decode = resolve as any + // Optimize loading strategy + if (imgElement.loading === 'lazy') { + imgElement.loading = 'eager' } - if (image.loading === 'lazy') { - image.loading = 'eager' - } + imgElement.onerror = errorHandler + imgElement.srcset = '' - if (isImageElement) { - clonedNode.srcset = '' - clonedNode.src = dataURL + // Use decode method if available for better performance + if (imgElement.decode) { + imgElement.src = dataURL + imgElement + .decode() + .then(() => resolve()) + .catch(errorHandler) } else { - clonedNode.href.baseVal = dataURL + imgElement.onload = () => resolve() + imgElement.src = dataURL } }) } +/** + * Updates an SVG image element with the data URL + * + * @param svgImgElement - The SVG image element to update + * @param dataURL - The data URL to set + * @returns A promise that resolves when the image is loaded + */ +async function updateSVGImageElement( + svgImgElement: SVGImageElement, + dataURL: string, +): Promise { + return new Promise((resolve, reject) => { + svgImgElement.onload = () => resolve() + svgImgElement.onerror = (event) => reject(event) + svgImgElement.href.baseVal = dataURL + }) +} + async function embedChildren( clonedNode: T, options: Options, ) { const children = toArray(clonedNode.childNodes) - const deferreds = children.map((child) => embedImages(child, options)) - await Promise.all(deferreds).then(() => clonedNode) + const deferrers = children.map((child) => embedImages(child, options)) + await Promise.all(deferrers).then(() => clonedNode) } export async function embedImages( diff --git a/src/util.ts b/src/util.ts index 3d430c8f..9ab8cba5 100644 --- a/src/util.ts +++ b/src/util.ts @@ -196,19 +196,35 @@ export function canvasToBlob( }) } -export function createImage(url: string): Promise { - return new Promise((resolve, reject) => { - const img = new Image() - img.onload = () => { - img.decode().then(() => { - requestAnimationFrame(() => resolve(img)) - }) - } +export async function createImage(url: string): Promise { + const img = new Image() + img.crossOrigin = 'anonymous' + img.decoding = 'async' + + const loadPromise = new Promise((resolve, reject) => { img.onerror = reject - img.crossOrigin = 'anonymous' - img.decoding = 'async' - img.src = url + img.onload = async () => { + try { + if (img.decode) { + await img.decode() + + return new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) + .then(() => resolve(img)) + .catch(reject) + } + + resolve(img) + } catch (error) { + reject(error) + } + } }) + + img.src = url + + return loadPromise } export async function svgToDataURL(svg: SVGElement): Promise { diff --git a/test/spec/util.spec.ts b/test/spec/util.spec.ts new file mode 100644 index 00000000..841f65ee --- /dev/null +++ b/test/spec/util.spec.ts @@ -0,0 +1,20 @@ +import { svgToDataURL, nodeToDataURL } from '../../src/util' + +describe('svgToDataURL', () => { + it('should convert an SVG element to a data URL', async () => { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + const dataURL = await svgToDataURL(svg) + + expect(dataURL).toContain('data:image/svg+xml;charset=utf-8') + }) +}) + +describe('nodeToDataURL', () => { + it('should convert an HTML node to a data URL', async () => { + const div = document.createElement('div') + div.textContent = 'Hello, world!' + const dataURL = await nodeToDataURL(div, 100, 100) + + expect(dataURL).toContain('data:image/svg+xml;charset=utf-8') + }) +})