4

I am building a chrome extension using Javascript which gets URLs of the opened tabs and saves the html file, but the problem with html is that it does not render images on webpages fully. So i changed my code to loop each URLs of the tabs and then save each html pages as image file automatically in one click on the download icon of the chrome extension. I have been researching for more than 2 days but nothing happened. Please see my code files and guide me. I know there is library in nodejs but i want to perform html to jpeg/png maker using chrome extension only.

Only icons i have not attached which i have used in this extension, all code i have and the text in popup.js which is commented i have tried.

Current output of the attached code enter image description here

Updated popup.js

File popup.js - This file has functions which gets all the URLs of the opened tabs in the browser window

// script for popup.html
window.onload = () => {  
    
    // var html2obj = html2canvas(document.body);
    // var queue  = html2obj.parse();
    // var canvas = html2obj.render(queue);
    // var img = canvas.toDataURL("image/png");
    
    let btn = document.querySelector("#btnDL");
    btn.innerHTML = "Download";

    function display(){
        // alert('Click button is pressed')
        window.open("image/url");
    }

    btn.addEventListener('click', display);
}

chrome.windows.getAll({populate:true}, getAllOpenWindows);

function getAllOpenWindows(winData) {

    var tabs = [];
    for (var i in winData) {
      if (winData[i].focused === true) {
          var winTabs = winData[i].tabs;
          var totTabs = winTabs.length;
  
          console.log("Number of opened tabs: "+ totTabs);
  
          for (var j=0; j<totTabs;j++) {

                tabs.push(winTabs[j].url);
                
                // Get the HTML string in the tab_html_string
                tab_html_string = get_html_string(winTabs[j].url)
                
                // get the HTML document of each tab
                tab_document = get_html_document(tab_html_string)

                console.log(tab_document)

                let canvasref = document.querySelector("#capture");
                canvasref.appendChild(tab_document.body);

                html2canvas(document.querySelector("#capture")).then(canvasref => {
                    
                    document.body.appendChild(canvasref)
                    var img = canvasref.toDataURL("image/png");
                    window.open(img)

                });

          }
      }
    }
    console.log(tabs);
}

function get_html_document(tab_html_string){
    
    /**
     * Convert a template string into HTML DOM nodes
     */
    
    var parser = new DOMParser();
    var doc = parser.parseFromString(tab_html_string, 'text/html');
    return doc;

}

function get_html_string(URL_string){

    /**
     * Convert a URL into HTML string
     */
    
    let xhr = new XMLHttpRequest();
    xhr.open('GET', URL_string, false);

    try {
        xhr.send();
        if (xhr.status != 200) {
            alert(`Error ${xhr.status}: ${xhr.statusText}`);
        } else {
                return xhr.response                                
            }

    } catch(err) {
        // instead of onerror
        alert("Request failed");
    }
}

File popup.html - This file represent icon and click functionality on the chrome browser search bar

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <!-- <meta name="viewport" content="width=device-width, initial-scale=1.0"> -->
    <script src= './html2canvas.min.js'></script>
    <script src= './jquery.min.js'></script>
    <script src='./popup.js'></script>

    <title>Capture extension</title>
    
        <!--
          - JavaScript and HTML must be in separate files: see our Content Security
          - Policy documentation[1] for details and explanation.
          -
          - [1]: http://developer.chrome.com/extensions/contentSecurityPolicy.html
         -->
</head>
<body>

    <button id="btnDL"></button>
    
</body>
</html>

File manifest.json - This file is used by the chrome browser to execute the chrome extension

{ 
      "manifest_version": 2,
      "name": "CIP screen capture",
      "description": "One-click download for files from open tabs",
      "version": "1.4.0.2",
      "browser_action": {
        "default_popup": "popup.html"
      },
      "permissions": [
        "downloads", "tabs", "<all_urls>"
      ],
      "options_page": "options.html"
}
Nick jones
  • 63
  • 1
  • 20
  • The chrome extension currently only extracts URLs of all the opened tabs, If you have option for different approach to save the opened tabs in the windows then please discuss – Nick jones Jan 15 '21 at 04:58
  • html2canvas should work on browser. You don't need nodeJS http://html2canvas.hertzen.com/getting-started. You should be able to import its scripts from some public CDN – João Pimentel Ferreira Jan 15 '21 at 08:25
  • Check https://cdnjs.com/libraries/html2canvas – João Pimentel Ferreira Jan 15 '21 at 08:26
  • yes but i am confused with https://developer.chrome.com/docs/extensions/reference/tabs/#method-captureVisibleTab, in this link there is funtion `captureVisibleTab` which work for capturing but the parameters and its usage in my for loop is not clear. – Nick jones Jan 15 '21 at 08:35
  • @JoãoPimentelFerreira i know about the html2canvas but if we use it in the for loop then there is need to change the focus of the tabs and download function which download all the opened tabs in image format on single click – Nick jones Jan 15 '21 at 08:40
  • @JoãoPimentelFerreira i want to know that this html2canvas library parses document.body as input but i have `winData[i]` tab information – Nick jones Jan 15 '21 at 10:11
  • Yes, `body` can be parsed as input. Just do: `html2canvas(document.querySelector("body")).then(canvas => { var img = canvas.toDataURL("image/png"); });` – João Pimentel Ferreira Jan 15 '21 at 10:17
  • @JoãoPimentelFerreira but `winData[i]` has no parameters for document and also i have to change state of active state of the tabs, which i got but the parameters of `winData[i]` has `windowId` which is same for all opened tabs. which is little confusing – Nick jones Jan 15 '21 at 10:45
  • check this: https://stackoverflow.com/questions/19758028/chrome-extension-get-dom-content – João Pimentel Ferreira Jan 15 '21 at 10:48

3 Answers3

5

You can use the library html2canvas which renders any html element, particularly the body element, and from the resulted canvas you can have your image

<script type="text/javascript" src="https://github.com/niklasvh/html2canvas/releases/download/v1.0.0-rc.7/html2canvas.min.js"></script>
<script>
  html2canvas(document.body).then(
    (canvas) => {
      var img = canvas.toDataURL("image/png"); 
    }
  );
</script>

You can get html2canvas from any public CDN, like this one. You don't need nodeJS. Or perhaps directly from the original git repo

<script type="text/javascript" src="https://github.com/niklasvh/html2canvas/releases/download/v1.0.0-rc.7/html2canvas.min.js"></script>

You can also save canvas as a blob

html2canvas(document.body).then(function(canvas) {
    // Export canvas as a blob 
    canvas.toBlob(function(blob) {
        // Generate file download
        window.saveAs(blob, "yourwebsite_screenshot.png");
    });
});
João Pimentel Ferreira
  • 14,289
  • 10
  • 80
  • 109
  • the main issue is `document.body` which i am not able to get becoz i have `winData[i]` in my for loop – Nick jones Jan 15 '21 at 10:48
  • @RinkuYadav I don't know exactly how chrome extesions work, but you just need to get the DOM element from the page. The DOM has the `document` object. It's explained here: https://stackoverflow.com/questions/19758028/chrome-extension-get-dom-content – João Pimentel Ferreira Jan 17 '21 at 12:09
  • But the `window.saveAs(blob, "yourwebsite_screenshot.png");` in this window refers to the HTML of the extension popup not the individual tabs opened in the browser. and That's why it is raising error of `document not attached to window` when i am trying to use the `widow.open` or `window.save`, I also tried `chrome.windows` – Nick jones Jan 18 '21 at 06:18
0

It's possible with vanilla JS, I guess vanilla is more likely to be used on a extension, the drawback is the end result, when a 3rd party library my already fixed the issues you will encounter with fonts and style (html2canvas or whatever).

So, vanilla js, for some reason stack overflow snippet does not permit window.open, demo here: https://jsfiddle.net/j67nqsme/

Few words on how it was implemented:

  • using html to svg transformation, a 3rd party library can be used here
  • using canvas to transform svg into pdg or jpg
  • example can export html or page to svg, png, jpg
  • it is not bullet proof - rendering can suffer, style, fonts, ratios, media query

Disclaimer: I won't help you debug or find solution of your problem, it is an answer to your question using vanilla js, from there on is your decision what you are using or how you are using it.

Source code bellow:

<html>
<body>
    <script>
        function export1() {
            const html = '<div style="color:red">this is <br> sparta </div>';
            renderHtmlToImage(html, 800, 600, "image/png");
        }

        function export2() {
            renderDocumentToImage(window.document, 800, 600, "image/png");
        }
        // format is: image/jpeg, image/png, image/svg+xml
        function exportImage(base64data, width, height, format) {
            var img = new Image();
            img.onload = function () {
                if ("image/svg+xml" == format) {
                    var w = window.open("");
                    w.document.write(img.outerHTML);
                    w.document.close();

                }
                else {
                    const canvas = document.createElement("Canvas");
                    canvas.width = width;
                    canvas.height = height;
                    const ctx = canvas.getContext('2d');
                    ctx.fillStyle = "white";
                    ctx.fillRect(0, 0, canvas.width, canvas.height);
                    ctx.drawImage(img, 0, 0);
                    var exportImage = new Image();
                    exportImage.onload = function () {
                        var w = window.open("");
                        w.document.write(exportImage.outerHTML);
                        w.document.close();
                    }
                    exportImage.src = canvas.toDataURL(format);
                }
            }
            img.src = base64data;
        }

        // format is: image/jpeg, image/png, image/svg+xml
        function renderHtmlToImage(html, width, height, format) {
            var svgData = '<svg xmlns="http://www.w3.org/2000/svg" width="' + width + '" height="' + height + '">'
                + '<foreignObject width="100%" height="100%">'
                + html2SvgXml(html)
                + '</foreignObject>'
                + '</svg>';
            var base64data = "data:image/svg+xml;base64," + btoa(svgData);
            exportImage(base64data, width, height, format);

        }

        function renderDocumentToImage(doc, width, height, format) {
            var svgData = '<svg xmlns="http://www.w3.org/2000/svg" width="' + width + '" height="' + height + '">'
                + '<foreignObject width="100%" height="100%">'
                + document2SvgXml(doc)
                + '</foreignObject>'
                + '</svg>';
            var base64data = "data:image/svg+xml;base64," + btoa(svgData);
            exportImage(base64data, width, height, format);
        }


        // plain html to svgXml
        function html2SvgXml(html) {
            var htmlDoc = document.implementation.createHTMLDocument('');

            htmlDoc.write(html);
            // process document
            return document2SvgXml(htmlDoc);
        }

        // htmlDocument to
        function document2SvgXml(htmlDoc) {
            // Set the xmlns namespace
            htmlDoc.documentElement.setAttribute('xmlns', htmlDoc.documentElement.namespaceURI);

            // Get XML
            var svcXml = (new XMLSerializer()).serializeToString(htmlDoc.body);
            return svcXml;
        }
    </script>
    <div>
        <h3 style="color:blue">My Title</h3>
        <div style="color:red">
            My Text
        </div>
        <button onclick="export1()">Export Plain Html</button>
        <button onclick="export2()">Export Entire Document</button>
    </div>
</body>
</html>
SilentTremor
  • 4,747
  • 2
  • 21
  • 34
  • Thanks i appreciate your efforts – Nick jones Jan 15 '21 at 12:40
  • I really like this and it's a clever solution, but it won't work with a lot of components - custom elements, shadow DOM, constructed stylesheets and the like won't render because the `XMLSerializer` only parses inline content. – Keith Jan 22 '21 at 14:01
  • for the right money can be done, and a bit of insanity :D – SilentTremor Jan 22 '21 at 15:10
0

There are 3 ways this could be approached:

  1. Render this in client javascript using an API like HTML2Canvas. It's isolated, runs in the browser, but relies on a re-render that could get details of the captured page wrong (see most issues with H2C). That might be fine if you just want a small preview and don't mind the odd difference. Also be careful as the render is slow and somewhat heavyweight - background renders of large pages may be noticeable by users, but so will waits for the images to render.

  2. Use a service that runs Chrome to visit the page and then screenshot it. You could write this or buy in a service like Site-Shot to do it for you. This requires a service which will have a cost, but can guarantee the render is accurate and ensures the load on the users' machines is minimal.

  3. Use the Chrome tab capture API to screenshot the tab. This is probably the best option for an extension, but currently you can only capture the current active tab. captureOffscreenTab is coming soon, but currently only in Canary behind a flag.

I'd recommend option 3, as by the time you have finished the component you could have access to the new features. If you need to release soon you could use 1 or 2 as a short term solution.

Keith
  • 150,284
  • 78
  • 298
  • 434