html2canvas icon indicating copy to clipboard operation
html2canvas copied to clipboard

Stuck at "0ms Starting document clone" on IOS (Safari and Chrome)

Open sharmavj89 opened this issue 1 year ago • 28 comments

I can reproduce the issue on multiple ios devices. Works like a charm on all other OS (including Android, Windows and MacOS) though. I have tried Safari and chrome and the issue showed up on both browsers. I've tried using all JS versions (non-minified) from 1.4.1 down to 1.2.0. Can't see any errors in the console either except it being stuck at "Starting document clone" at 0ms. I'm trying to screenshot a html div element that contains multiple divs (mix of imgs and text). The size of png generated on other devices is not more than 100kb though so I doubt it's got something to do with render issue due to large size as mentioned in another issue here.

This is what I'm getting in chrome console DEBUG #1 0ms Starting document clone with size 428x859 scrolled to 0,-526 And nothing happens after that.

Here's my script

Version html2canvas : 1.4.1 IOS : 16.4.1

sharmavj89 avatar Apr 19 '23 12:04 sharmavj89

Ran into the same issue. Seems to be affecting WebKit browsers.

Tested on GNOME Web (evince): Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15.

marek-saji avatar Apr 21 '23 08:04 marek-saji

I managed to figure out that in my case it stopped working after I started to use Next.js’s Image on the page. 😕 (I’m using [email protected]). Image was not used in part of the page I was capturing. After I’ve added it html2canvas promise never resolved.

marek-saji avatar Apr 21 '23 09:04 marek-saji

Yeah It's weird. I've tried multiple things but couldn't get it move forward on iOS devices. Finally, decided to use domtoimage instead. It's working fine but not the best solution. It's slower and is not as reliable as html2canvas.

sharmavj89 avatar Apr 23 '23 03:04 sharmavj89

@marek-saji I use nextimage without optimisation. and it doesn't work either. Did you manage to fix it somehow?

AdrianEasyOze avatar Apr 27 '23 08:04 AdrianEasyOze

as far as i have noticed it is not an issue of next image, but of images loaded using lazy loading. If there is a lazy loading image on the page, this causes a problem with html2canvas on safari

AdrianEasyOze avatar Apr 30 '23 21:04 AdrianEasyOze

My html2canvas was also was hanging in Safari (macOS/iOS) at #1 – "0ms" – "Starting document clone with size..." with no other error or proceeding on the console. I came accross this post and found @AdrianEasyOze 's answer was the issue. I could understand it because it used to work before and stopped only after I lazy loaded one image on the site. The image is not even inside the element to screenshot yet Safari was failing with html2canvas somehow. On removing the lazyload it started working again. Chrome / Edge do not have this problem.

envieme avatar May 03 '23 03:05 envieme

My html2canvas was also was hanging in Safari (macOS/iOS) at #1 – "0ms" – "Starting document clone with size..." with no other error or proceeding on the console. I came accross this post and found @AdrianEasyOze 's answer was the issue. I could understand it because it used to work before and stopped only after I lazy loaded one image on the site. The image is not even inside the element to screenshot yet Safari was failing with html2canvas somehow. On removing the lazyload it started working again. Chrome / Edge do not have this problem.

this is not the solution, it still not working. I have 1 image and one div block. On other devices it's work perfect. But on mobile IOS in Safari or Chrome - i get freeze on "0ms Starting document clone". I don't have lazy load images and etc.

petermarkovich avatar May 05 '23 10:05 petermarkovich

@petermarkovich If you completely remove a photo from a particular screen, does the problem still exist? Can I see how you add the photo (some code)?

AdrianEasyOze avatar May 05 '23 10:05 AdrianEasyOze

@petermarkovich If you completely remove a photo from a particular screen, does the problem still exist? Can I see how you add the photo (some code)?

yep, i use react + magento. My react app it's a part of magento page. When i test only my app without magento 2, і don't have this issue example: style={{ backgroundImage: url(${image}), backgroundSize: "cover", backgroundRepeat: "no-repeat", backgroundPosition: "top", }}

or src={image} the image = path to image || base64

petermarkovich avatar May 05 '23 11:05 petermarkovich

I have a similar issue on Windows Chrome, after trying to make screenshot of the same part multiple times

HurYur avatar May 05 '23 12:05 HurYur

@petermarkovich If you completely remove a photo from a particular screen, does the problem still exist? Can I see how you add the photo (some code)?

yep, i use react + magento. My react app it's a part of magento page. When i test only my app without magento 2, і don't have this issue example: style={{ backgroundImage: url(${image}), backgroundSize: "cover", backgroundRepeat: "no-repeat", backgroundPosition: "top", }}

or src={image} the image = path to image || base64.

but this is super strange. In prod server i have this issue. In local env with production mode - there is no errors ...

petermarkovich avatar May 05 '23 12:05 petermarkovich

We had issues with lazy loaded images anywhere in the dom, not just within the element targeted for export. No error logs, but the output logs show that the process hangs before the dom gets cloned

borie88 avatar Jun 08 '23 08:06 borie88

Same issue raised today, caused by at least one image in the current document that has loading="lazy" 👍🏼

AshleyRedman avatar Jun 15 '23 14:06 AshleyRedman

Same issue. When setting { useCORS: true } problem is solved. html2canvas(document.body, { useCORS: true })

cyanyiyi avatar Jul 15 '23 12:07 cyanyiyi

I had this issue. useCors did not resolve. I was generating charts and converting them for pdfs. All that was required on my side was to add like a 2 second delay between when the charts were generated and when i tried to convert them.

TheGreatAlgo avatar Jul 21 '23 08:07 TheGreatAlgo

Indeed - useCORS solution didn't help at all. On the other hand, loading='lazy' was the problem by me. Even though (as mentioned) the img that was lazy loaded, was completely out of the screenshoted part of HTML. But since the plugin clones the whole document (as far as I understand it), it really doesn't matter if the lazy loaded img is in the screenshoted part, or somewhere else.

KamilStehlicek avatar Jul 21 '23 10:07 KamilStehlicek

When I use version 1.0.0-rc.4, the issue is no longer present.

adangcc avatar Aug 09 '23 12:08 adangcc

Html2canvas Safari Problem: Not Working

I came to you with a complete solution proposal. It worked perfectly for me. I couldn't find it anywhere on the internet, I finally solved it with my own methods.

Problem: The problem is caused by CORS policy on Safari and LazyLoad. It could also be due to just one of them. Depending on the situation, you can edit the function, primarily LazyLoad.

Solution: You need to exclude external URLs that may be against CORS policy and any LazyLoad images on the page, even if they are not in Canvas.

Solution Code:

html2canvas(element, {
        useCORS: true, allowTaint: true, ignoreElements: function (e) {
// Here, ignore external URL links and lazyload images
            if ((e.tagName === "A" && e.host !== window.location.host) || e.getAttribute('loading') === "lazy") {
                return true;
            } else {
                return false;
            }
        }
    }).then(function (canvas) {
    //   Make what do you want with canvas data.
    })

My Function (Start Download With a Button, Copy a Display None Div):

/*
Function Parameters:
elementID: The element you want to copy.
buttonID: Button to start the process
name: File name for save
toggleDisplay: If the element you want to convert to Canvas is Display: None, you should choose True. Thus, it will translate as Display: Block before the process starts. After copying, Display: will be converted back to None. If your element to be copied is already visible, select False.
titleHtml: It changes the HTML on the button after the download starts.
titleWait: Changes the HTML on the button during the process.
*/
function downloadHtmlToImage(elementID, buttonID, name, toggleDisplay = true, extension = 'jpg',titleHtml = '<i class="me-2 bi bi-check"></i>Download Started',titleWait = '<i class="fa fa-spin fa-spinner me-2"></i>Wait...') {

    let button = $('#' + buttonID)[0];
    $(button).html(titleWait);
    $(button).attr('disabled', 'disabled');
    var element = document.querySelector("#" + elementID);
    if(toggleDisplay){
element.style.display = 'block';
}
// With ignoreElements, one of the html2canvas parameters, we exclude external URL and loading="lazy" images on the entire page. You can change this as you wish. If you have to use a LazyLoad image, you can remove all LazyLoad attributes from the page before processing. 
    html2canvas(element, {
        useCORS: true, allowTaint: true, ignoreElements: function (e) {
            if ((e.tagName === "A" && e.host !== window.location.host) || e.getAttribute('loading') === "lazy") {
                return true;
            } else {
                return false;
            }
        }
    }).then(function (canvas) {
        let mimeType = null;
        if (extension === 'jpg' || extension === 'jpeg') {
            mimeType = 'image/jpeg';
        } else if (extension === 'png') {
            mimeType = 'image/png';
        } else {
            extension = 'jpg';
            mimeType = 'image/jpeg';
        }
        var a = $("<a style='display:none' id='js-downloder'>")
            .attr("href", canvas.toDataURL(mimeType))
            .attr("download", name + '.' + extension)
            .appendTo("body");
        a[0].click();
        a.remove();
        $(button).html(titleHtml);
        $(button).removeAttr('disabled');

    })
    if(toggleDisplay){
    element.style.display = 'none';

}
}

Here, example HTML:

<button role="button" onclick="downloadHtmlToImage('myElementForDownload','downloadButtonID','fileNameDownloaded')" id="downloadButtonID">My Button for Start Download</button>

allject avatar Aug 11 '23 00:08 allject

Html2canvas Safari Problem: Not Working

I came to you with a complete solution proposal. It worked perfectly for me. I couldn't find it anywhere on the internet, I finally solved it with my own methods.

Problem: The problem is caused by CORS policy on Safari and LazyLoad. It could also be due to just one of them. Depending on the situation, you can edit the function, primarily LazyLoad.

Solution: You need to exclude external URLs that may be against CORS policy and any LazyLoad images on the page, even if they are not in Canvas.

Solution Code:

html2canvas(element, {
        useCORS: true, allowTaint: true, ignoreElements: function (e) {
// Here, ignore external URL links and lazyload images
            if ((e.tagName === "A" && e.host !== window.location.host) || e.getAttribute('loading') === "lazy") {
                return true;
            } else {
                return false;
            }
        }
    }).then(function (canvas) {
    //   Make what do you want with canvas data.
    })

My Function (Start Download With a Button, Copy a Display None Div):

/*
Function Parameters:
elementID: The element you want to copy.
buttonID: Button to start the process
name: File name for save
toggleDisplay: If the element you want to convert to Canvas is Display: None, you should choose True. Thus, it will translate as Display: Block before the process starts. After copying, Display: will be converted back to None. If your element to be copied is already visible, select False.
titleHtml: It changes the HTML on the button after the download starts.
titleWait: Changes the HTML on the button during the process.
*/
function downloadHtmlToImage(elementID, buttonID, name, toggleDisplay = true, extension = 'jpg',titleHtml = '<i class="me-2 bi bi-check"></i>Download Started',titleWait = '<i class="fa fa-spin fa-spinner me-2"></i>Wait...') {

    let button = $('#' + buttonID)[0];
    $(button).html(titleWait);
    $(button).attr('disabled', 'disabled');
    var element = document.querySelector("#" + elementID);
    if(toggleDisplay){
element.style.display = 'block';
}
// With ignoreElements, one of the html2canvas parameters, we exclude external URL and loading="lazy" images on the entire page. You can change this as you wish. If you have to use a LazyLoad image, you can remove all LazyLoad attributes from the page before processing. 
    html2canvas(element, {
        useCORS: true, allowTaint: true, ignoreElements: function (e) {
            if ((e.tagName === "A" && e.host !== window.location.host) || e.getAttribute('loading') === "lazy") {
                return true;
            } else {
                return false;
            }
        }
    }).then(function (canvas) {
        let mimeType = null;
        if (extension === 'jpg' || extension === 'jpeg') {
            mimeType = 'image/jpeg';
        } else if (extension === 'png') {
            mimeType = 'image/png';
        } else {
            extension = 'jpg';
            mimeType = 'image/jpeg';
        }
        var a = $("<a style='display:none' id='js-downloder'>")
            .attr("href", canvas.toDataURL(mimeType))
            .attr("download", name + '.' + extension)
            .appendTo("body");
        a[0].click();
        a.remove();
        $(button).html(titleHtml);
        $(button).removeAttr('disabled');

    })
    if(toggleDisplay){
    element.style.display = 'none';

}
}

Here, example HTML:

<button role="button" onclick="downloadHtmlToImage('myElementForDownload','downloadButtonID','fileNameDownloaded')" id="downloadButtonID">My Button for Start Download</button>

loading="lazy" is the default attribute of the next/image We can move it to loading="eager"

huynhiruuza avatar Aug 24 '23 02:08 huynhiruuza

loading="lazy" is the default attribute of the next/image We can move it to loading="eager"

In fact, with a little more effort, you can do it like this.

  1. Before Html2Canvas starts processing, you remove all HTML elements that have the value loading="lazy" and replace it with any value. For example: myCustomAttribute="itWillReplace"
  2. Html2Canvas works. In this process, there is no problem with CORS or LazyLoad in the structure retrieved from the DOM.
  3. Instead of myCustomAttribute="itWillReplace", we can replace it with the tag loading="lazy" wherever it is present.

Just a solution suggestion in a very primitive way :)

allject avatar Aug 24 '23 02:08 allject

When I use version 1.0.0-rc.4, the issue is no longer present.

1.0.0-rc.4 produces blank image on ios

349989153 avatar Oct 21 '23 10:10 349989153

Hi guys, I had the same problem as You on Safari and indeed changing the loading attributes of images to eager solves the problem. I wanted to use the 'onclone' method from options as I found it more intuitive, but it seems to be called too late so instead - I change the attributes of all the images inside the captured dom node to 'eager' mode. Here is a snippet that worked perfeclty for me (of course after taking the screenshot, you can change those attributes back):

    const getElementImage = async (sourceElement: HTMLElement) => {
        Array.from(sourceElement.querySelectorAll('img'))?.forEach((img) => {
            if (img.getAttribute('loading') === 'lazy') img.setAttribute('loading', 'eager')
        });

        const canvas = await html2canvas(sourceElement, {
            useCORS: true,
            allowTaint: true,
            logging: true,
            height: sourceElement.clientHeight || window.innerHeight,
            width: sourceElement.clientWidth || window.innerWidth,
            ignoreElements: (el) =>
                el.nodeName.toLowerCase() === 'canvas' ||
                el.getAttribute('loading') === 'lazy'
        });
        const base64 = canvas.toDataURL('image/jpeg', 1.0);

        const image = new Image();
        image.width = sourceElement.offsetWidth || sourceElement.clientWidth;
        image.height = sourceElement.offsetHeight || sourceElement.clientHeight;
        image.src = base64;
        return image
    }

stachbial avatar Dec 19 '23 12:12 stachbial

Hi! I just wanted to chime in that removing lazy loading from all images in my site resolved this issue. Only iOS was not exporting an image. I'm using NextJS with the next/image tag. Adding priority={true} to all <Image /> tags totally resolved this issue.

joshpayette avatar Dec 30 '23 17:12 joshpayette

Hi guys, I had the same problem as You on Safari and indeed changing the loading attributes of images to eager solves the problem. I wanted to use the 'onclone' method from options as I found it more intuitive, but it seems to be called too late so instead - I change the attributes of all the images inside the captured dom node to 'eager' mode. Here is a snippet that worked perfeclty for me (of course after taking the screenshot, you can change those attributes back):

    const getElementImage = async (sourceElement: HTMLElement) => {
        Array.from(sourceElement.querySelectorAll('img'))?.forEach((img) => {
            if (img.getAttribute('loading') === 'lazy') img.setAttribute('loading', 'eager')
        });

        const canvas = await html2canvas(sourceElement, {
            useCORS: true,
            allowTaint: true,
            logging: true,
            height: sourceElement.clientHeight || window.innerHeight,
            width: sourceElement.clientWidth || window.innerWidth,
            ignoreElements: (el) =>
                el.nodeName.toLowerCase() === 'canvas' ||
                el.getAttribute('loading') === 'lazy'
        });
        const base64 = canvas.toDataURL('image/jpeg', 1.0);

        const image = new Image();
        image.width = sourceElement.offsetWidth || sourceElement.clientWidth;
        image.height = sourceElement.offsetHeight || sourceElement.clientHeight;
        image.src = base64;
        return image
    }

work for me! But I don't have a loading node in my dom, Anyway, it works for me!

LeoonLiang avatar Jan 16 '24 03:01 LeoonLiang

Html2canvas Safari Problem: Not Working

I came to you with a complete solution proposal. It worked perfectly for me. I couldn't find it anywhere on the internet, I finally solved it with my own methods.

Problem: The problem is caused by CORS policy on Safari and LazyLoad. It could also be due to just one of them. Depending on the situation, you can edit the function, primarily LazyLoad.

Solution: You need to exclude external URLs that may be against CORS policy and any LazyLoad images on the page, even if they are not in Canvas.

Solution Code:

html2canvas(element, {
        useCORS: true, allowTaint: true, ignoreElements: function (e) {
// Here, ignore external URL links and lazyload images
            if ((e.tagName === "A" && e.host !== window.location.host) || e.getAttribute('loading') === "lazy") {
                return true;
            } else {
                return false;
            }
        }
    }).then(function (canvas) {
    //   Make what do you want with canvas data.
    })

My Function (Start Download With a Button, Copy a Display None Div):

/*
Function Parameters:
elementID: The element you want to copy.
buttonID: Button to start the process
name: File name for save
toggleDisplay: If the element you want to convert to Canvas is Display: None, you should choose True. Thus, it will translate as Display: Block before the process starts. After copying, Display: will be converted back to None. If your element to be copied is already visible, select False.
titleHtml: It changes the HTML on the button after the download starts.
titleWait: Changes the HTML on the button during the process.
*/
function downloadHtmlToImage(elementID, buttonID, name, toggleDisplay = true, extension = 'jpg',titleHtml = '<i class="me-2 bi bi-check"></i>Download Started',titleWait = '<i class="fa fa-spin fa-spinner me-2"></i>Wait...') {

    let button = $('#' + buttonID)[0];
    $(button).html(titleWait);
    $(button).attr('disabled', 'disabled');
    var element = document.querySelector("#" + elementID);
    if(toggleDisplay){
element.style.display = 'block';
}
// With ignoreElements, one of the html2canvas parameters, we exclude external URL and loading="lazy" images on the entire page. You can change this as you wish. If you have to use a LazyLoad image, you can remove all LazyLoad attributes from the page before processing. 
    html2canvas(element, {
        useCORS: true, allowTaint: true, ignoreElements: function (e) {
            if ((e.tagName === "A" && e.host !== window.location.host) || e.getAttribute('loading') === "lazy") {
                return true;
            } else {
                return false;
            }
        }
    }).then(function (canvas) {
        let mimeType = null;
        if (extension === 'jpg' || extension === 'jpeg') {
            mimeType = 'image/jpeg';
        } else if (extension === 'png') {
            mimeType = 'image/png';
        } else {
            extension = 'jpg';
            mimeType = 'image/jpeg';
        }
        var a = $("<a style='display:none' id='js-downloder'>")
            .attr("href", canvas.toDataURL(mimeType))
            .attr("download", name + '.' + extension)
            .appendTo("body");
        a[0].click();
        a.remove();
        $(button).html(titleHtml);
        $(button).removeAttr('disabled');

    })
    if(toggleDisplay){
    element.style.display = 'none';

}
}

Here, example HTML:

<button role="button" onclick="downloadHtmlToImage('myElementForDownload','downloadButtonID','fileNameDownloaded')" id="downloadButtonID">My Button for Start Download</button>

Thanks, it worked for me <3

vuquyet8080 avatar Mar 07 '24 05:03 vuquyet8080

i have a solutions for you, just before use html2canvas code load this to change all images from lazy load to eager load:

const lazyElements = document.querySelectorAll('[loading="lazy"]');
lazyElements.forEach(element => {
  element.setAttribute('loading', 'eager');
});

sirius-pro avatar Apr 15 '24 16:04 sirius-pro

I ended up implementing a similar solution, a variable called isScreenshotMode that allows me to make subtle layout changes, as well as toggling my next/image to non-lazy load for the html2canvas function.

joshpayette avatar Apr 15 '24 16:04 joshpayette

For me , its stuck at 0ms, and page gets refreshed always in ios safari and chrome, Any solution ?

ShanteshSindgi avatar Jun 06 '24 08:06 ShanteshSindgi