bubkoo / html-to-image

✂️ Generates an image from a DOM node using HTML5 canvas and SVG.
MIT License
5.79k stars 544 forks source link

First render of image (on mobile) is broken, on refresh rendered image is correct #292

Open joshembling opened 2 years ago

joshembling commented 2 years ago

There is an issue I have surfaced on mobile devices - particularly using Safari where the first render of the image is completely broken, formatting issues, font issues etc.

On refresh these are resolved and the image is correct.

Here is my code:

htmlToImage
        .toJpeg(node, {
            backgroundColor: "#621934",
            width: "700",
        })
        .then(function (dataUrl) {
            var img = new Image();
            img.src = dataUrl;
            img.className = "rendered-img";
            final.appendChild(img);

            downloadImage.addEventListener("click", () => {
                var link = document.createElement("a");
                link.download = "img.jpeg";
                link.href = dataUrl;
                link.click();
            });
        })
        .catch(function (error) {
            console.error("oops, something went wrong!", error);
        });
biiibooo[bot] commented 2 years ago

Hiya! This issue has gone quiet. Spooky quiet. 👻 We get a lot of issues, so we currently close issues after 60 days of inactivity. It’s been at least 20 days since the last update here. If we missed this issue or if you want to keep it open, please reply here. You can also add the label "not-stale" to keep this issue open! As a friendly reminder: the best way to see this issue, or any other, fixed is to open a Pull Request.

Thanks for being a part of the Antv community! 💪💯

jam-fran commented 2 years ago

Hey @joshembling -- I've recently been experiencing the exact same issue. I thought it might be because I wasn't setting certain CSS properties on my child elements (width, height, position, etc.), but I've experimented a ton with those with no luck. Very strange that it's on mobile only (cannot reproduce simulating mobile screen size on my desktop either). Were you able to find any fixes or workarounds?

joshembling commented 2 years ago

Hey @joshembling -- I've recently been experiencing the exact same issue. I thought it might be because I wasn't setting certain CSS properties on my child elements (width, height, position, etc.), but I've experimented a ton with those with no luck. Very strange that it's on mobile only (cannot reproduce simulating mobile screen size on my desktop either). Were you able to find any fixes or workarounds?

No I haven't found a fix, unfortunately. There doesn't seem to be much support for this package anymore either so I can't see it being fixed anytime soon.

Sovai commented 2 years ago

It's the same this issue on domtoimage

https://github.com/tsayen/dom-to-image/issues/343

I had to call it twice to make it work on iPhones

keyvnchristian commented 1 year ago

So i was able to solve this using html2canvas this one

here's what I'm doing

let node = document.getElementById(`someid`);
html2canvas(node, {
    useCORS: true, //useCORS if there's one or more images using external URL
}).then(function (canvas) {
    const dataURL = canvas.toDataURL();
    download(dataURL, 'image.png'); // download here using downloadJs
})

Hope this helps

athisun commented 1 year ago

I've done some trial-and-error testing on this today and noticed that rendering in Safari (16.2, 18614.3.7.1.5, in my case) seems to fail when there are any interactions with the canvas. I'm attempting to render a Chart.js 3.9.1 canvas element and noticed it's blank for the first render AND if any events were triggered immediately prior (hover, click, drag, etc).

I'm still not entirely sure why it works, but my solution was:

async function downloadChart() {
  // 1. Disable all sources of interactivity
  // TODO: Experiment with https://www.chartjs.org/docs/3.9.1/configuration/animations.html#disabling-animation
  chart.options = {
    ...chart.options,
    events: [], // Disable all event listeners
    plugins: { ... }, // Disable plugins, e.g. { enabled: false }
  };
  chart.update('none'); // Update the chart with 'none' animation

  ...

  // 2. Waits and renders
  // Chart.js default animation duration is 400ms
  await new Promise((resolve) => {
    setTimeout(resolve, 500); // Wait for any existing animations/events to resolve
  });
  await toBlob(domToImgContainer); // First render, discard. Safari bug?
  chart.update('none'); // Update the chart with 'none' animation. For some reason this helps prevent in-progress animations causing a failure.
  await toBlob(domToImgContainer); // Second render, discard. Does this fill buffer 1 of 2?
  await new Promise((resolve) => {
    setTimeout(resolve, 500); // Wait for any new animations/events to resolve
  });
  const blob = await toBlob(domToImgContainer); // Final render, export
  if (!blob) return;

  ...

  // 3. Open or download the result
}

My best guess is that interactivity flushes the drawing buffer in Safari, or something similar. I read in another Github issue that preserveDrawingBuffer: true solved their blank canvas issues:

...and after reading https://stackoverflow.com/a/27747016/8680333, I have a hunch that it could help, but haven't had time to test it. This might also explain why my solution above is working for me, since I'd be filling both buffers before Safari gets to flush (invalidate?) either of them.

Hope this helps someone! Curious to hear if anyone has success or finds out more information about this behaviour.

xianjianlf2 commented 1 year ago

I've done some trial-and-error testing on this today and noticed that rendering in Safari (16.2, 18614.3.7.1.5, in my case) seems to fail when there are any interactions with the canvas. I'm attempting to render a Chart.js 3.9.1 canvas element and noticed it's blank for the first render AND if any events were triggered immediately prior (hover, click, drag, etc).

I'm still not entirely sure why it works, but my solution was:

async function downloadChart() {
  // 1. Disable all sources of interactivity
  // TODO: Experiment with https://www.chartjs.org/docs/3.9.1/configuration/animations.html#disabling-animation
  chart.options = {
    ...chart.options,
    events: [], // Disable all event listeners
    plugins: { ... }, // Disable plugins, e.g. { enabled: false }
  };
  chart.update('none'); // Update the chart with 'none' animation

  ...

  // 2. Waits and renders
  // Chart.js default animation duration is 400ms
  await new Promise((resolve) => {
    setTimeout(resolve, 500); // Wait for any existing animations/events to resolve
  });
  await toBlob(domToImgContainer); // First render, discard. Safari bug?
  chart.update('none'); // Update the chart with 'none' animation. For some reason this helps prevent in-progress animations causing a failure.
  await toBlob(domToImgContainer); // Second render, discard. Does this fill buffer 1 of 2?
  await new Promise((resolve) => {
    setTimeout(resolve, 500); // Wait for any new animations/events to resolve
  });
  const blob = await toBlob(domToImgContainer); // Final render, export
  if (!blob) return;

  ...

  // 3. Open or download the result
}

My best guess is that interactivity flushes the drawing buffer in Safari, or something similar. I read in another Github issue that preserveDrawingBuffer: true solved their blank canvas issues:

...and after reading https://stackoverflow.com/a/27747016/8680333, I have a hunch that it could help, but haven't had time to test it. This might also explain why my solution above is working for me, since I'd be filling both buffers before Safari gets to flush (invalidate?) either of them.

Hope this helps someone! Curious to hear if anyone has success or finds out more information about this behaviour.

Thanks,it works