html-to-image icon indicating copy to clipboard operation
html-to-image copied to clipboard

Image is not showing in some cases iOS, Safari

Open asaleh267 opened this issue 2 years ago • 23 comments

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

asaleh267 avatar Jan 09 '23 09:01 asaleh267

Potential duplicates:

  • [#357] Some SVG elements are not visible on resulting image (60.94%)

biiibooo[bot] avatar Jan 09 '23 09:01 biiibooo[bot]

👋 @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.

biiibooo[bot] avatar Jan 09 '23 09:01 biiibooo[bot]

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

andreemic avatar Jan 22 '23 21:01 andreemic

@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

GMaiolo avatar Jan 24 '23 20:01 GMaiolo

@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)
  })
}

qq15725 avatar Feb 02 '23 10:02 qq15725

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

Icegreeen avatar Feb 23 '23 16:02 Icegreeen

@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 avatar Mar 03 '23 07:03 Lucas-lululu

@Lucas-lululu is the image very big? I assume it may be related

GMaiolo avatar Mar 05 '23 20:03 GMaiolo

@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.

jdmcleod avatar Mar 08 '23 19:03 jdmcleod

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.

acartmell avatar Apr 12 '23 22:04 acartmell

@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.

wenlittleoil avatar Apr 24 '23 09:04 wenlittleoil

@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.

GMaiolo avatar Apr 24 '23 13:04 GMaiolo

@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 image

Lucas-lululu avatar Apr 26 '23 07:04 Lucas-lululu

i have similar problem on the ios devices. but with toSvg - work ok and image always shown in the result

petermarkovich avatar May 08 '23 11:05 petermarkovich

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.

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.

natBizitza avatar May 17 '23 06:05 natBizitza

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!

jonathanrstern avatar Jun 30 '23 17:06 jonathanrstern

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 🤷‍♂️

Matt-Jensen avatar Jul 20 '23 09:07 Matt-Jensen

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.

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
}

pgYou avatar Jan 21 '24 05:01 pgYou

None of those variants work, if you have multiple different images on your element. they get replaces all with the same 1 image instead

rogerkerse avatar Mar 19 '24 11:03 rogerkerse

Any solid solutions?

liamcharmer avatar May 16 '24 22:05 liamcharmer

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;
};

spidercodeur avatar May 20 '24 22:05 spidercodeur

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;
};

Adam-Greenan avatar May 29 '24 08:05 Adam-Greenan