html-to-image
html-to-image copied to clipboard
Image is not showing in some cases iOS, Safari
The html is converted to png without the images included in the html block. It shows white background replaced instead of the images
It happens sometimes not everytime, specially on iOS, Safari devices
Potential duplicates:
- [#357] Some SVG elements are not visible on resulting image (60.94%)
👋 @asaleh267
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.
steps to reproduce: try to render an element which includes an image tag like
<img src="https://via.placeholder.com/150"/>
sometimes it has enough time to load the image, sometimes it doesn't
@asaleh267 a quick workaround is to execute the function several times (2 or 3 times) like this:
await toPng(...);
await toPng(...);
await toPng(...);
const result = await toPng(...); // This should have the images properly loaded
@asaleh267 this problem seems to be a bug of Safari
, when drawing svg+xml, some images are not decoded
A temporary fix
https://github.com/qq15725/modern-screenshot/blob/v4.2.12/src/converts/image-to-canvas.ts#L29-L39
Example code:
const loadedImageCounts = IS_SAFARI ? (context.images.size || 1) : 1
for (let i = 0; i < loadedImageCounts; i++) {
await new Promise<void>(resolve => {
setTimeout(() => {
try {
context?.drawImage(loaded, 0, 0, canvas.width, canvas.height)
} catch (error) {
console.warn('Failed to image to canvas', error)
}
resolve()
}, 100 + i)
})
}
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.
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.
The solution below solved my problem:
@asaleh267 a quick patch is to execute the function several times (2 or 3 times) like this:
await toPng(...); await toPng(...); await toPng(...); const result = await toPng(...); // This should have the images properly loaded
@asaleh267 a quick patch is to execute the function several times (2 or 3 times) like this:
await toPng(...); await toPng(...); await toPng(...); const result = await toPng(...); // This should have the images properly loaded
This still doesn't solve the problem, I increase the number of calls to 5 times, but the picture still can't be loaded
@Lucas-lululu is the image very big? I assume it may be related
@Lucas-lululu is the image very big? I assume it may be related
The workaround also is not working for me, and I don't think image size affects it.
Instead of trying to guess the right number of times to call await toPng(...)
, here's a workaround that should hopefully work more consistently. All you need to do is adjust the value for minDataLength
based on the image you're generating. You can determine that by logging console.log(dataUrl.length)
in a browser that doesn't have this issue, like Chrome.
const buildPng = async () => {
const element = document.getElementById('image-node');
let dataUrl = '';
const minDataLength = 2000000;
let i = 0;
const maxAttempts = 10;
while (dataUrl.length < minDataLength && i < maxAttempts) {
dataUrl = await toPng(element);
i += 1;
}
return dataUrl;
};
This worked for me when working with large assets that took multiple attempts until the call worked.
@asaleh267 a quick patch is to execute the function several times (2 or 3 times) like this:
await toPng(...); await toPng(...); await toPng(...); const result = await toPng(...); // This should have the images properly loaded
I don't think it's a good way to solve this problem.
@asaleh267 a quick patch is to execute the function several times (2 or 3 times) like this:
await toPng(...); await toPng(...); await toPng(...); const result = await toPng(...); // This should have the images properly loaded
I don't think it's a good way to solve this problem.
It's not. It's a workaround.
@Lucas-lululu is the image very big? I assume it may be related
I found that when I generated pictures, I would introduce a lot of fonts, so I disabled them all, and the pictures came out soon
i have similar problem on the ios devices. but with toSvg
- work ok and image always shown in the result
Instead of trying to guess the right number of times to call
await toPng(...)
, here's a workaround that should hopefully work more consistently. All you need to do is adjust the value forminDataLength
based on the image you're generating. You can determine that by loggingconsole.log(dataUrl.length)
in a browser that doesn't have this issue, like Chrome.const buildPng = async () => { const element = document.getElementById('image-node'); let dataUrl = ''; const minDataLength = 2000000; let i = 0; const maxAttempts = 10; while (dataUrl.length < minDataLength && i < maxAttempts) { dataUrl = await toPng(element); i += 1; } return dataUrl; };
This worked for me when working with large assets that took multiple attempts until the call worked.
This is the only thing that worked for me in Safari. Thank you! However, I would like to have a more clear understanding of why it's happening.
This works with small images - but still having trouble with larger ones.
Has anyone figured out a workaround?
html2canvas works perfectly, but it's a little slower so I was really hoping to get html-to-image to work!
This works with small images - but still having trouble with larger ones.
Has anyone figured out a workaround?
html2canvas works perfectly, but it's a little slower so I was really hoping to get html-to-image to work!
Firstly: lol at this workaround. Can't believe it works.
Secondly: Larger images do tend to fail because only part of the image will render, but will still meet the minDataLength
threshold. There's a smart solution to this problem (ie estimating a realistic byte size as the threshold), but ultimately I found generating the image about 4 times works 🤷♂️
Instead of trying to guess the right number of times to call
await toPng(...)
, here's a workaround that should hopefully work more consistently. All you need to do is adjust the value forminDataLength
based on the image you're generating. You can determine that by loggingconsole.log(dataUrl.length)
in a browser that doesn't have this issue, like Chrome.const buildPng = async () => { const element = document.getElementById('image-node'); let dataUrl = ''; const minDataLength = 2000000; let i = 0; const maxAttempts = 10; while (dataUrl.length < minDataLength && i < maxAttempts) { dataUrl = await toPng(element); i += 1; } return dataUrl; };
This worked for me when working with large assets that took multiple attempts until the call worked.
right mabey await some times, then try again is better.
const buildPng = async (node: HTMLElement) => {
let dataUrl = ''
const minDataLength = 2000000
let i = 0
const maxAttempts = 10
dataUrl = await toPng(node)
while (dataUrl.length < minDataLength && i < maxAttempts) {
await new Promise((resolve) => {
setTimeout(() => resolve(null), 300)
})
dataUrl = await toPng(node)
i += 1
}
return dataUrl
}
None of those variants work, if you have multiple different images on your element. they get replaces all with the same 1 image instead
Any solid solutions?
To optimize the only current solution, I did this. By checking the sizes, there is only one change, when it is detected, we stop the loop. without enlargement it passes in 2 or 3 cycles
const buildPng = async () => {
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
let dataUrl = '';
let i = 0;
let maxAttempts;
if (isSafari) {
maxAttempts = 5;
} else {
maxAttempts = 1;
}
let cycle = [];
let repeat = true;
while (repeat && i < maxAttempts) {
dataUrl = await toPng(contentToPrint.current as HTMLDivElement, {
fetchRequestInit: {
cache: 'no-cache',
},
skipAutoScale: true,
includeQueryParams: true,
pixelRatio: isSafari ? 1 : 3,
quality: 1,
filter: filter,
style: { paddingBottom: '100px' },
});
i += 1;
cycle[i] = dataUrl.length;
if (dataUrl.length > cycle[i - 1]) repeat = false;
}
//console.log('safari:' + isSafari + '_repeat_need_' + i);
return dataUrl;
};
To optimize the only current solution, I did this. By checking the sizes, there is only one change, when it is detected, we stop the loop. without enlargement it passes in 2 or 3 cycles
const buildPng = async () => { const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); let dataUrl = ''; let i = 0; let maxAttempts; if (isSafari) { maxAttempts = 5; } else { maxAttempts = 1; } let cycle = []; let repeat = true; while (repeat && i < maxAttempts) { dataUrl = await toPng(contentToPrint.current as HTMLDivElement, { fetchRequestInit: { cache: 'no-cache', }, skipAutoScale: true, includeQueryParams: true, pixelRatio: isSafari ? 1 : 3, quality: 1, filter: filter, style: { paddingBottom: '100px' }, }); i += 1; cycle[i] = dataUrl.length; if (dataUrl.length > cycle[i - 1]) repeat = false; } //console.log('safari:' + isSafari + '_repeat_need_' + i); return dataUrl; };
This worked brilliantly for us.
We had this issue when taking canvas images with any of the screenshot libraries including Modern Screenshot.
Our issues were not specific to Safari, it was more towards any browser that was on iOS, Safari or Chromium.
const createCanvas = async (node: HTMLImageElement) => {
const isSafariOrChrome = /safari|chrome/i.test(navigator.userAgent) && !/android/i.test(navigator.userAgent);
let dataUrl = "";
let canvas;
let i = 0;
let maxAttempts;
if (isSafariOrChrome) {
maxAttempts = 5;
} else {
maxAttempts = 1;
}
let cycle = [];
let repeat = true;
while (repeat && i < maxAttempts) {
canvas = await htmlToImage.toCanvas(node as HTMLImageElement, {
fetchRequestInit: {
cache: "no-cache",
},
skipFonts: true,
includeQueryParams: true,
quality: 1,
});
i += 1;
dataUrl = canvas.toDataURL("image/png");
cycle[i] = dataUrl.length;
if (dataUrl.length > cycle[i - 1]) repeat = false;
}
console.log("is safari or chrome:" + isSafariOrChrome + "_repeat_need_" + i);
return canvas;
};