Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
sharmavj89 opened this issue Apr 19, 2023 · 28 comments
Open

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

sharmavj89 opened this issue Apr 19, 2023 · 28 comments

Comments

@sharmavj89
Copy link

sharmavj89 commented Apr 19, 2023

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

<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.js"></script> <script> function htmlToCanvas(element) { const canvas = document.createElement("canvas"); return html2canvas(element).then(canvasElement => { return canvasElement.toDataURL("image/png"); }); } const elementToCanvas = document.getElementById("canvas-content"); htmlToCanvas(elementToCanvas).then(imgBase64Element => { console.log(imgBase64Element); } </script>

Version
html2canvas : 1.4.1
IOS : 16.4.1

@marek-saji
Copy link

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
Copy link

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 next@13.1.6). Image was not used in part of the page I was capturing. After I’ve added it html2canvas promise never resolved.

@sharmavj89
Copy link
Author

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.

@AdrianEasyOze
Copy link

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

@AdrianEasyOze
Copy link

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

@envieme
Copy link

envieme commented May 3, 2023

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.

@petermarkovich
Copy link

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.

@AdrianEasyOze
Copy link

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

@petermarkovich
Copy link

petermarkovich commented May 5, 2023

@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

@HurYur
Copy link

HurYur commented May 5, 2023

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

@petermarkovich
Copy link

petermarkovich commented May 5, 2023

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

@borie88
Copy link

borie88 commented Jun 8, 2023

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

@AshleyRedman
Copy link

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

@cyanyiyi
Copy link

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

@TheGreatAlgo
Copy link

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.

@KamilStehlicek
Copy link

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.

@adangcc
Copy link

adangcc commented Aug 9, 2023

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

@allject
Copy link

allject commented Aug 11, 2023

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>

@huynhiruuza
Copy link

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"

@allject
Copy link

allject commented Aug 24, 2023

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 :)

@349989153
Copy link

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

@stachbial
Copy link

stachbial commented Dec 19, 2023

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
    }

@joshpayette
Copy link

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.

@LeoonLiang
Copy link

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!

@vuquyet8080
Copy link

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

@sirius-pro
Copy link

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

@joshpayette
Copy link

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.

@ShanteshSindgi
Copy link

ShanteshSindgi commented Jun 6, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests