niklasvh / html2canvas

Screenshots with JavaScript
https://html2canvas.hertzen.com/
MIT License
30.48k stars 4.8k forks source link

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

Open sharmavj89 opened 1 year ago

sharmavj89 commented 1 year ago

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

marek-saji commented 1 year ago

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

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

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

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

AdrianEasyOze commented 1 year ago

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

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

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

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

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

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

petermarkovich commented 1 year ago

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

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

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

cyanyiyi commented 1 year ago

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

TheGreatAlgo commented 1 year ago

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

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

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

allject commented 1 year ago

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

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

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 commented 11 months ago

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 commented 9 months ago

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 commented 9 months ago

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 commented 8 months ago

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 commented 7 months ago

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 commented 5 months ago

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 commented 5 months ago

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 commented 4 months ago

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