bubkoo / html-to-image

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

Images are sometimes skipped via iOS #420

Open getCryptoAddress opened 11 months ago

getCryptoAddress commented 11 months ago

Expected Behavior

Images always render

image

Current Behavior

Images are sometimes skipped via iOS

image

For example: The background appeared only for the 3rd time ezgif com-optimize

Possible Solution

Steps To Reproduce

  1. Open https://getcryptoaddress.github.io/paper-wallets
  2. Fill the form with random data image
  3. Click "Get Paper Wallets"
  4. Click "Download"
Error Message & Stack Trace

no errors

Additional Context

Code:

  1. https://github.com/getCryptoAddress/getCryptoAddress.github.io/blob/master/src/features/DownloadPaperWallet/ui/DownloadPaperWallet.vue#L47
  2. https://github.com/getCryptoAddress/getCryptoAddress.github.io/blob/master/src/entities/PaperWallets/lib/download/downloadPaperWallet.ts#L10
  3. https://github.com/getCryptoAddress/getCryptoAddress.github.io/blob/master/src/entities/PaperWallets/lib/download/downloadHtmlAsPng.ts#L8

Your Environment

vivcat[bot] commented 11 months ago

👋 @getCryptoAddress

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.

morlimoore commented 11 months ago

Hi @getCryptoAddress, have you been able to move past this yet? I am using the html-to-image V1.11.11 and I experience the same issue as you. The image is only visible on the third download, and I don't know the cause of that.

it4need commented 11 months ago

Having the same issues here... I can reproduce this issue even on v1.11.0

jonathanrstern commented 11 months ago

Same here

riccardoperra commented 11 months ago

This is an iOS issue unfortunately https://bugs.webkit.org/show_bug.cgi?id=219770

I partially resolved this adding a delay of 250ms after the creation of the svg and before drawing to the canvas. 250 is just a magic number that I know in most of times image has been loaded. For images of high size and if you haven’t loaded it into the dom probably it’s not enough

toCanvas()

CheckCoder commented 11 months ago

Same here

jonathanrstern commented 11 months ago

Finally found a reliable solution: the modern-screenshot library (a fork of html-to-image). Completely fixed the problem for me!

H/t @qq15725

https://github.com/qq15725/modern-screenshot

riccardoperra commented 11 months ago

beware that this solution seems still clone image like everyone else (https://github.com/qq15725/modern-screenshot/blob/main/src/clone-image.ts), so probably it is fixed by implicitly being asynchronous and adding some delays, since it uses worker (I don't know if it's a good idea, since the major computation only concerns the dom which is not available in the worker side)

image

default drawImageInterval is 100 https://github.com/qq15725/modern-screenshot/blob/0973c6c302e91b3d49921876f5c021b5ddda833a/src/create-context.ts#L53

jonathanrstern commented 11 months ago

He explained the difference here:

https://github.com/bubkoo/html-to-image/issues/361#issuecomment-1413526381

What do you think?

riccardoperra commented 11 months ago

Delaying the final canvas rendering by a fixed timeout can be the solution for most of cases.

It’s valid in my opinion since there isn’t any way to know if safari will render the image.

What I don’t like it’s this magic number which doesn’t guarantee it works every time 😅, but probably it’s the best it can be done at the moment.

Anyway this solution seems delaying n seconds for each image present in the html structure, so if you have like 10 images, it will be delayed for 1000ms~ (10 * 100ms) 🤔 but I don’t have the full understand of the code so I may be wrong.

Another important thing: what’s the device we are using to render that image, and how much mb is it?

Assuming we are trying to render an image around 0-1mb, most of iOS and macOS device can render it with a timeout of 100. If the image gets bigger, we surely need to increment this timeout. But I think there are others factors like device speed (what if energy save mode is on?)

currently I think the most safest way of rendering an image without having issues is using puppeteer/playwright with chrome under the hood 🤷🏻‍♂️ doing this will also allows to export the same image for all sort of devices (e.g. safari still can’t handle box-shadow), but it may be expensive

qq15725 commented 11 months ago

10 image ref = 10 (canvasContext2d.drawImage('data:image/svg+xml,html-to-svg-data') and await 100ms)

I don't know how it works, but it works 😄

jonathanrstern commented 11 months ago

Can confirm that it's working beautifully! Thanks again @qq15725 :)

(For me, it's working on photos >5MB... will post an update if it stops working but for now I think we're good to go)

CheckCoder commented 11 months ago

10 image ref = 10 (canvasContext2d.drawImage('data:image/svg+xml,html-to-svg-data') and await 100ms)

I don't know how it works, but it works 😄

Thanks, work for me too.

getCryptoAddress commented 10 months ago

https://github.com/bubkoo/html-to-image/blob/master/src/index.ts#L58 if add

await new Promise((resolve) => setTimeout(resolve, 1));
image

Safari will render always

UPDATED ok, I created PR https://github.com/bubkoo/html-to-image/pull/423

UPDATED I'm wrong. I reproduced bug again. Not 8 times from 10, but 3 times from 10. It is better, but not solution

getCryptoAddress commented 10 months ago

Vue code example

<script lang="ts" setup>
     // ....

     // get svg from "html-to-image" for html
     canvasSvgElement.value = decodeURIComponent(await toSvg(targetElement));

    // wait vue render
    await nextTick();

    // get images into svg
    const images = canvasImageWrapperElement.value?.querySelectorAll("img");

    // wait all images
    if (images) {
      await Promise.all(
        [...images].map((img) => {
          return new Promise((resolve, reject) => {
            if (img.complete) {
              resolve(null);
            } else {
              img.onload = resolve;
              img.onerror = reject;
            }
          });
        })
      );

      console.log("images loaded"); // <--- it's true for safari

How I understand, safari needs time to render, not just load https://github.com/bubkoo/html-to-image/blob/master/src/embed-images.ts#L57 This is just a hypothesis, anyway the code above works in safari