Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 98 additions & 29 deletions src/embed-images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,62 +34,131 @@
(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<T extends HTMLElement | SVGImageElement>(
clonedNode: T,
options: Options,
) {
const isImageElement = isInstanceOfElement(clonedNode, HTMLImageElement)
): Promise<void> {
// 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
}

Check warning on line 63 in src/embed-images.ts

View check run for this annotation

Codecov / codecov/patch

src/embed-images.ts#L63

Added line #L63 was not covered by tests

// 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(

Check warning on line 74 in src/embed-images.ts

View check run for this annotation

Codecov / codecov/patch

src/embed-images.ts#L74

Added line #L74 was not covered by tests
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<void> {
return new Promise<void>((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<void> {
return new Promise<void>((resolve, reject) => {
svgImgElement.onload = () => resolve()
svgImgElement.onerror = (event) => reject(event)
svgImgElement.href.baseVal = dataURL
})
}

async function embedChildren<T extends HTMLElement>(
clonedNode: T,
options: Options,
) {
const children = toArray<HTMLElement>(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<T extends HTMLElement>(
Expand Down
38 changes: 27 additions & 11 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,19 +196,35 @@ export function canvasToBlob(
})
}

export function createImage(url: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => {
img.decode().then(() => {
requestAnimationFrame(() => resolve(img))
})
}
export async function createImage(url: string): Promise<HTMLImageElement> {
const img = new Image()
img.crossOrigin = 'anonymous'
img.decoding = 'async'

const loadPromise = new Promise<HTMLImageElement>((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<void>((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<string> {
Expand Down
20 changes: 20 additions & 0 deletions test/spec/util.spec.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
Loading