html-to-image
html-to-image copied to clipboard
Is there a way to wait for a few seconds before capturing the canvas to render an image?
General Summary of the Problem
The canvas captures the html node before it's completely rendered on the canvas. I'm just assuming that this is the case because when:
- I first "download" the image, it renders a with the right position styles, but no
elements loaded in yet.
- Second time, it renders the some
elements, but the biggest image is not painted in yet.
- Third time, I guess everything has properly loaded before, so it now actually loads the image that I want.
Here's a visual of those three cases I mentioned:
1st download | 2nd download | 3rd download |
---|---|---|
![]() |
![]() |
![]() |
In other words, I'm assuming that the problem is because the <img />
elements are not completely loaded into the canvas yet but the html-to-image
library captures it and renders it right away.
Expected Behavior
The expected behavior should be rendering the 3rd image right away.
Current Behavior
(already explained this in the general summary) This bug also doesn't happen on the computer/laptop but happens on my phone for some reason. I'm just assuming that the computer has more computing resources than a phone.
Possible Solution
I think the easiest fix for this when working with <img />
that take time to load, is to just simply delay the "capture" of the image on the canvas.
So is there a way to delay the "capture" of the image while it gets rendered on the canvas?
Steps To Reproduce
Can't really give steps to reproduce but try using an element with a large image as a node and then use downloadjs
with that and try downloading on a phone.
- html-to-image: 1.11.4
- OS: iOS version 16.2
- Browser: Safari
👋 @Blankeos
Thanks for opening your first issue here! If you're reporting a 🐞 bug, please make sure you include steps to reproduce it. To help make it easier for us to investigate your issue, please follow the contributing guidelines.
We get a lot of issues on this repo, so please be patient and we will get back to you as soon as we can.
-
You need to
waitUntilLoad
before the dom renders the image -
But it could also be a
Safari
problem https://github.com/bubkoo/html-to-image/issues/361#issuecomment-1413526381
waitUntilLoad
code:
https://github.com/qq15725/modern-screenshot/blob/v4.2.12/src/utils.ts#L185-L198
export const isElementNode = (node: Node): node is Element => node.nodeType === 1 // Node.ELEMENT_NODE
export const isSVGElementNode = (node: Element): node is SVGElement => typeof (node as SVGElement).className === 'object'
export const isSVGImageElementNode = (node: Element): node is SVGImageElement => isSVGElementNode(node) && node.tagName === 'IMAGE'
export const isHTMLElementNode = (node: Node): node is HTMLElement => isElementNode(node) && typeof (node as HTMLElement).style !== 'undefined' && !isSVGElementNode(node)
export const isImageElement = (node: Element): node is HTMLImageElement => node.tagName === 'IMG'
export const isVideoElement = (node: Element): node is HTMLVideoElement => node.tagName === 'VIDEO'
export const consoleWarn = (...args: any[]) => console.warn(...args)
export function getDocument<T extends Node>(target?: T | null): Document {
return (
(
target && isElementNode(target as any)
? target?.ownerDocument
: target
) ?? window.document
) as any
}
export function createImage(url: string, ownerDocument?: Document | null, useCORS = false): HTMLImageElement {
const img = getDocument(ownerDocument).createElement('img')
if (useCORS) {
img.crossOrigin = 'anonymous'
}
img.decoding = 'sync'
img.loading = 'eager'
img.src = url
return img
}
type Media = HTMLVideoElement | HTMLImageElement | SVGImageElement
interface LoadMediaOptions {
ownerDocument?: Document
timeout?: number
}
export function loadMedia<T extends Media>(media: T, options?: LoadMediaOptions): Promise<T>
export function loadMedia(media: string, options?: LoadMediaOptions): Promise<HTMLImageElement>
export function loadMedia(media: any, options?: LoadMediaOptions): Promise<any> {
return new Promise(resolve => {
const { timeout, ownerDocument } = options ?? {}
const node: Media = typeof media === 'string'
? createImage(media, getDocument(ownerDocument))
: media
let timer: any = null
let removeEventListeners: null | (() => void) = null
function onResolve() {
resolve(node)
timer && clearTimeout(timer)
removeEventListeners?.()
}
if (timeout) {
timer = setTimeout(onResolve, timeout)
}
if (isVideoElement(node)) {
const poster = node.poster
if (poster) {
return loadMedia(poster, options).then(resolve)
}
const currentSrc = (node.currentSrc || node.src)
if (node.readyState >= 2 || !currentSrc) {
return onResolve()
}
const onLoadeddata = onResolve
const onError = (error: any) => {
consoleWarn(
'Video load failed',
currentSrc,
error,
)
onResolve()
}
removeEventListeners = () => {
node.removeEventListener('loadeddata', onLoadeddata)
node.removeEventListener('error', onError)
}
node.addEventListener('loadeddata', onLoadeddata, { once: true })
node.addEventListener('error', onError, { once: true })
} else {
const currentSrc = isSVGImageElementNode(node)
? node.href.baseVal
: (node.currentSrc || node.src)
if (!currentSrc) {
return onResolve()
}
const onLoad = async () => {
if (isImageElement(node) && 'decode' in node) {
try {
await node.decode()
} catch (error) {
consoleWarn(
'Failed to decode image, trying to render anyway',
node.dataset.originalSrc || currentSrc,
error,
)
}
}
onResolve()
}
const onError = (error: any) => {
consoleWarn(
'Image load failed',
node.dataset.originalSrc || currentSrc,
error,
)
onResolve()
}
if (isImageElement(node) && node.complete) {
return onLoad()
}
removeEventListeners = () => {
node.removeEventListener('load', onLoad)
node.removeEventListener('error', onError)
}
node.addEventListener('load', onLoad, { once: true })
node.addEventListener('error', onError, { once: true })
}
})
}
export async function waitUntilLoad(node: Node, timeout: number) {
if (isHTMLElementNode(node)) {
if (isImageElement(node) || isVideoElement(node)) {
await loadMedia(node, { timeout })
} else {
await Promise.all(
['img', 'video'].flatMap(selectors => {
return Array.from(node.querySelectorAll(selectors))
.map(el => loadMedia(el as any, { timeout }))
}),
)
}
}
}
Some libs use promises, html-to-image for example. It runs in the background.
Safari, perhaps in the name of performance, ignores these promises and the HTML is converted to PNG without the images included in the html block.
We found the solution here:
https://github.com/bubkoo/html-to-image/issues/361#issuecomment-1402537176
Also avoid using .then and .catch in functions, opt for async functions with await. It may take a few seconds, so just add a toast to let the user know something is up.