bubkoo / html-to-image

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

Image is not showing in some cases iOS, Safari #361

Open asaleh267 opened 1 year ago

asaleh267 commented 1 year ago

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

vivcat[bot] commented 1 year ago

Potential duplicates:

vivcat[bot] commented 1 year ago

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

andreemic commented 1 year ago

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

GMaiolo commented 1 year ago

@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
qq15725 commented 1 year ago

@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)
  })
}
Icegreeen commented 1 year ago

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
Lucas-lululu commented 1 year ago

@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

GMaiolo commented 1 year ago

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

jdmcleod commented 1 year ago

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

acartmell commented 1 year ago

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.

wenlittleoil commented 1 year ago

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

GMaiolo commented 1 year ago

@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 commented 1 year ago

@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
petermarkovich commented 1 year ago

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

natBizitza commented 1 year ago

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.

jonathanrstern commented 1 year ago

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!

Matt-Jensen commented 11 months ago

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

pgYou commented 5 months ago

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 }

rogerkerse commented 3 months ago

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

liamcharmer commented 1 month ago

Any solid solutions?

spidercodeur commented 1 month ago

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;
};
Adam-Greenan commented 1 month ago

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