DEV Community

Cover image for Create a Google Chrome Extension Part 2: Image Grabber
Andrey Germanov
Andrey Germanov

Posted on • Updated on

Create a Google Chrome Extension Part 2: Image Grabber

Table Of Contents

Introduction
Create and open a web page with a list of images
    Open a new tab with a local extension page
        Create a page.html
        Open a new browser tab
        Send image URLs data to the page
        Receive image URLs data on the page
Create Image Downloader interface
    Create UI to display and select images
    Implement Select All function
    Implement Download function
        Get selected image URLs
        Download images by URLs
        Determine file names for images
        Create a ZIP archive
        Download a ZIP archive
Code cleanup
Styling the extension page
Publish and distribute the extension
Conclusion

Introduction

This is the second part of the article where I show how to create an Image Grabber Google Chrome Extension. The Image Grabber is an extension that allows extracting all or selected images from any web page, displayed in the Chrome browser, and downloading them as a single ZIP archive.
Before reading it, you have to read the first part of this article here:

https://dev.to/andreygermanov/create-a-google-chrome-extension-part-1-image-grabber-1foa

So, in the previous part, we created an extension, which displays a popup window with the "GRAB NOW" button. When a user presses this button, the extension injects a script to a web page, opened on a current browser tab, which grabs all <img> tags from this page, extract URLs of all images, and returns it back to the extension. Then, the extension copied this list of URLs to a clipboard.

In this part, we will change this behavior. Instead of copying to the clipboard, the extension will open a web page with a list of images and a "Download" button. Then, the user can select which images to download. Finally, when pressing the "Download" button on that page, a script will download all selected images, will compress them to an archive with the name images.zip, and will prompt the user to save this archive to a local computer.

So, by the end of this article, if you do all the steps, you will have an extension that looks and works like displayed on the next video.

During this tutorial you will learn important concepts of data exchange between different parts of Chrome Web browser, some new Javascript API functions from chrome browser namespace, concepts of working with data of binary files in Javascript, including ZIP-archives, and finally, I will explain how to prepare the extension for publishing to Chrome Web Store - a global repository of Google Chrome extensions, which will make it available for anyone in the world.

So, let's get started.

Create and open a web page with a list of images

The final step of the popup.js script in the previous part, was the onResult function, which collected an array of image URLs and copied it to a clipboard. At the current stage, this function looks like this:

/**
 * Executed after all grabImages() calls finished on 
 * remote page
 * Combines results and copy a list of image URLs 
 * to clipboard
 * 
 * @param {[]InjectionResult} frames Array 
 * of grabImage() function execution results
 */
function onResult(frames) {
    // If script execution failed on remote end 
    // and could not return results
    if (!frames || !frames.length) { 
        alert("Could not retrieve images");
        return;
    }
    // Combine arrays of image URLs from 
    // each frame to a single array
    const imageUrls = frames.map(frame=>frame.result)
                            .reduce((r1,r2)=>r1.concat(r2));
    // Copy to clipboard a string of image URLs, delimited by 
    // carriage return symbol  
    window.navigator.clipboard
          .writeText(imageUrls.join("\n"))
          .then(()=>{
             // close the extension popup after data 
             // is copied to the clipboard
             window.close();
          });
}
Enter fullscreen mode Exit fullscreen mode

So, we remove everything after the // Copy to clipboard ... comment line including this line itself, and instead, implement a function, which opens a page with a list of images:

function onResult(frames) {
    // If script execution failed on remote end 
    // and could not return results
    if (!frames || !frames.length) { 
        alert("Could not retrieve images");
        return;
    }
    // Combine arrays of image URLs from 
    // each frame to a single array
    const imageUrls = frames.map(frame=>frame.result)
                            .reduce((r1,r2)=>r1.concat(r2));
    // Open a page with a list of images and send imageUrls to it
    openImagesPage(imageUrls)
}

/**
 * Opens a page with a list of URLs and UI to select and
 * download them on a new browser tab and send an
 * array of image URLs to this page
 * 
 * @param {*} urls - Array of Image URLs to send
 */
function openImagesPage(urls) {
    // TODO: 
    // * Open a new tab with a HTML page to display an UI
    // * Send `urls` array to this page
}
Enter fullscreen mode Exit fullscreen mode

Now let's implement openImagesPage function step by step.

Open a new tab with a local extension page

Using the chrome.tabs.create function of Google Chrome API, you can create a new tab in a browser with any URL. It can be any URL on the internet or a local Html page of an extension.

Create a page HTML

Let's create a page, that we want to open. Create an HTML file with the simple name page.html and the following content. Then save it to the root of the Image Grabber extension folder:

<!DOCTYPE html>
<html>
    <head>
        <title>Image Grabber</title>
    </head>
    <body>
        <div class="header">
            <div>
                <input type="checkbox" id="selectAll"/>&nbsp;
                <span>Select all</span>
            </div>
            <span>Image Grabber</span>
            <button id="downloadBtn">Download</button>    
        </div>
        <div class="container">      
        </div>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

This markup defines a page, that consists of two sections (two divs): the header div and the container div, that have appropriate classes, which later will be used in the CSS stylesheet. Header part has controls to select all images from a list and download them. Container part, which is empty now, will be dynamically populated by images, using an array of URLs. Finally, after applying CSS styles to this page, it will look like this:

Final look

Open a new browser tab

So, it's a time to start writing the openImagesPage(urls) function in the popup.js, which we defined earlier. We will use chrome.tabs.create function to open a new tab with the page.html in it.

The syntax of chrome.tabs.create function is following:

chrome.tabs.create(createProperties,callback)
Enter fullscreen mode Exit fullscreen mode
  • createProperties is an object with parameters, that tell Chrome, which tab to open and how. In particular, it has the url parameter, that will be used to specify which page to open in the tab

  • callback is a function that will be called after the tab is created. This function has a single argument tab, that contains an object of the created tab, which, among others, contains an id parameter of this tab to communicate with it later.

So, let's create the tab:

function openImagesPage(urls) {
    // TODO: 
    // * Open a new tab with a HTML page to display an UI    
    chrome.tabs.create({"url": "page.html"},(tab) => {        
        alert(tab.id)
        // * Send `urls` array to this page
    });
}
Enter fullscreen mode Exit fullscreen mode

If you run the extension now and press the 'Grab Now' button on any browser page with images, it should open the page.html on a new tab and activate this tab. The following content should be displayed on the new tab:

Extension skeleton 1

As you see in the previous code, we defined the callback function, which later should be used to send urls array to that page, but now it should display an alert with a created tab ID. However, if you try to run this now, it will not happen, because of one interesting effect, that needs to discuss to understand what happened, and then, understand how to fix this.

So, you press the "Grab Now" button in the popup window which triggers a new tab to appear. And, in a moment when a new tab appears and activates, the popup window disappeared and is destroyed. It was destroyed BEFORE the callback was executed. This is what happens when a new tab activates and receives focus. To fix this, we should create the tab, but not activate it until doing all required actions in the callback. Only after all actions in the callback are finished, need to manually activate the tab.

The first thing that needs to do, is to specify in the chrome.tabs.create function to not automatically select the created tab. To do this, need to set the active parameter of createProperties to false:

chrome.tabs.create({url: 'page.html', active: false}, ...
Enter fullscreen mode Exit fullscreen mode

Then, inside the callback need to run all actions that needed to run (display an alert, or send a list of URLs) and in the last line of this callback, manually activate the tab.

In terms of Chrome APIs, activate a tab means update the tab status. To update a status of a tab, need to use the chrome.tabs.update function, with a very similar syntax:

chrome.tabs.update(tabId,updateProperties,callback)
Enter fullscreen mode Exit fullscreen mode
  • tabId is the id of a tab to update
  • updateProperties defines which properties of the tab to update.
  • callback function called after update operation finished. To activate a tab using this function, need to make this call:
chrome.tabs.update(tab.id,{active:true});
Enter fullscreen mode Exit fullscreen mode

We omit the callback because do not need it. Everything that is required to do with this tab should be done on previous lines of this function.

function openImagesPage(urls) {
    // TODO: 
    // * Open a new tab with a HTML page to display an UI    
    chrome.tabs.create(
        {"url": "page.html",active:false},(tab) => {        
            alert(tab.id)
            // * Send `urls` array to this page
            chrome.tabs.update(tab.id,{active: true});
        }
    );
}
Enter fullscreen mode Exit fullscreen mode

If you run the extension now and press the "Grab Now" button, everything should work as expected: tab is created, then alert displayed, then the tab will be selected and finally popup disappear.

Now, let's remove the temporary alert and define, how to send a list of image URLs to the new page and how to display an interface to manage them.

Send image URLs data to the page

Now we need to create a script, which will generate an HTML markup to display a list of images inside the container div on the page.

At the first glance, we can go the same way as we did in the previous part of this article. We can use chrome.scripting API o inject the script to the tab with page.html and this script will use image urls to generate images list inside the container. But injecting scripts it's not a true way. It's kind of hacking. It's not completely correct and legal. We should define script in a place, where it will be executed, we should not "send scripts". The only reason why we did this before, is because we did not have access to the source code of pages of sites, from which we grabbed images. But in the current case, we have full control on page.html and all scripts in it and that is why, the script, which generates an interface for that should be defined in page.html. So, let's create an empty page.js Javascript file, put it in the same folder with page.html, and include it to the page.html this way:

<!DOCTYPE html>
<html>
    <head>
        <title>Image Grabber</title>
    </head>
    <body>
        <div class="header">
            <div>
                <input type="checkbox" id="selectAll"/>&nbsp;
                <span>Select all</span>
            </div>
            <span>Image Grabber</span>
            <button id="downloadBtn">Download</button>    
        </div>
        <div class="container">      
        </div>
        <script src="/page.js"></script>        
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Now we can write in page.js whatever is required to init and create an interface. However, we still need data from popup.js - the array of urls to display images for. So, we still need to send this data to the script, that we just created.

This is a moment to introduce an important feature of Chrome API, which can be used to communicate between different parts of extension: messaging. One part of the extension can send a message with data to another part of the extension, and that other part can receive the message, process received data and respond to the sending part. Basically, the messaging API is defined under the chrome.runtime namespace and you can read the official documentation here: https://developer.chrome.com/docs/extensions/mv3/messaging/.

In particular, there is an chrome.runtime.onMessage event. If a listener is defined to this event in a script, this script will receive all events, that other scripts send to it.

For the purposes of Image Grabber, we need to send a message with a list of URLs from the popup.js script to the tab with the page.html page. The script on that page should receive that message, extract the data from it and then respond to it to confirm that data was processed correctly. Now it's time to introduce API, that is required for this.

chrome.tabs.sendMessage(tabId, message, responseFn)
Enter fullscreen mode Exit fullscreen mode
  • tabId is an id of tab to which message will be sent
  • message the message itself. Can be any Javascript object.
  • callback is a function, that is called when the received party responded to that message. This function has only one argument responseObject which contains anything, that receiver sent as a response.

So, this is what we need to call in popup.js to send a list of URLs as a message:

function openImagesPage(urls) {
    // TODO: 
    // * Open a new tab with a HTML page to display an UI    
    chrome.tabs.create(
        {"url": "page.html",active:false},(tab) => {        
            // * Send `urls` array to this page
            chrome.tabs.sendMessage(tab.id,urls,(resp) => {
                chrome.tabs.update(tab.id,{active: true});
            });                            
        }
    );
}
Enter fullscreen mode Exit fullscreen mode

In this tab, we send urls as a message to the page and activate this page only after the response to this message is received.

I would recommend wrapping this code by a setTimeout function to wait a couple of milliseconds before sending the message. Need to give some time to initialize the new tab:

function openImagesPage(urls) {
    // TODO: 
    // * Open a new tab with a HTML page to display an UI    
    chrome.tabs.create(
        {"url": "page.html",active:false},(tab) => {        
            // * Send `urls` array to this page
            setTimeout(()=>{
                chrome.tabs.sendMessage(tab.id,urls,(resp) => {
                    chrome.tabs.update(tab.id,{active: true});
                });                            
            },500);
        }
    );
}
Enter fullscreen mode Exit fullscreen mode

Receive image URLs data on the page

If you run this now, the popup window won't disappear, because it should only after receiving the response from receiving page. To receive this message, we need to define a chrome.runtime.onMessage event listener in the page.js script:

chrome.runtime.onMessage
    .addListener(function(message,sender,sendResponse) { 
        addImagesToContainer(message);               
        sendResponse("OK");
    });

/**
 * Function that used to display an UI to display a list 
 * of images
 * @param {} urls - Array of image URLs
 */
function addImagesToContainer(urls) {
    // TODO Create HTML markup inside container <div> to
    // display received images and to allow to select 
    // them for downloading
    document.write(JSON.stringify(urls));
}

Enter fullscreen mode Exit fullscreen mode

To receive a message, the destination script should add a listener to the chrome.runtime.onMessage event. The listener is a function with three arguments:

  • message - a received message object, transferred as is. (array of urls in this case)
  • sender - an object which identifies a sender of this message.
  • sendResponse - a function, that can be used to send a response to the sender. A single parameter of this function is anything that we want to send to the sender.

So, here, this listener passes a received message to an addImagesToContainer function, that will be used to create an HTML markup to display images. But right now it writes a string representation of the received array of URLs. Then, the listener responds to the sender by sendResponse function. It sends just an "OK" string as a response because it does not matter how to respond. The only fact of response is important in this case.

After it's done, when you click "GRAB NOW" button from an extension, the new page should open with something like this, as content: (depending on which tab you clicked it):

Raw URLs list

Create Image Downloader interface

We have received an array of image URLs to download from the popup window into a script, connected to the page.html and this is all that we needed from popup.js. Now, it's time to build an interface to display these images and allow download them.

Create UI to display and select images

The function addImagesToContainer(urls) already created with a placeholder code. Let's change it to really add images to the container <div>:

/**
 * Function that used to display an UI to display a list 
 * of images
 * @param {} urls - Array of image URLs
 */
function addImagesToContainer(urls) {
    if (!urls || !urls.length) {
        return;
    }
    const container = document.querySelector(".container");
    urls.forEach(url => addImageNode(container, url))
}

/**
 * Function dynamically add a DIV with image and checkbox to 
 * select it to the container DIV
 * @param {*} container - DOM node of a container div 
 * @param {*} url - URL of image 
 */
function addImageNode(container, url) {
    const div = document.createElement("div");
    div.className = "imageDiv";
    const img = document.createElement("img");
    img.src = url;
    div.appendChild(img);
    const checkbox = document.createElement("input");
    checkbox.type = "checkbox";
    checkbox.setAttribute("url",url);            
    div.appendChild(checkbox);
    container.appendChild(div)
}
Enter fullscreen mode Exit fullscreen mode

Let's clarify this code step by step.

  • addImagesToContainer function check if the array of URLs is not empty and stops if it does not contain anything.
  • Then, it queries DOM to get a node of the div element with the container class. Then this container element will be used in a function to append all images to it.
  • Next, it calls addImageNode function for each URL. It passes the container to it and the URL itself
  • Finally, the addImageNode function dynamically constructs an HTML for each image and appends it to the container.

It constructs the following HTML for each image URL:

<div class="imageDiv">
    <img src={url}/>
    <input type="checkbox" url={url}/>
</div>
Enter fullscreen mode Exit fullscreen mode

It appends a div with class imageDiv for each image. This div contains the image itself with specified url and the checkbox, to select it. This checkbox has a custom attribute named url, which later will be used by downloading function to identify, which URL to use to download the image.

If you run this right now for the same list of images, as on the previous screenshot, the page should display something like the following:

Raw images list with checkboxes

Here you can see that right after the header, with the "Select all" checkbox and "Download" button, there is a list of images with checkboxes to select each of them manually.

This is a full code of the page.js file, used to receive and display this list:

chrome.runtime.onMessage
    .addListener((message,sender,sendResponse) => { 
        addImagesToContainer(message)
        sendResponse("OK");
    });

/**
 * Function that used to display an UI to display a list 
 * of images
 * @param {} urls - Array of image URLs
 */
function addImagesToContainer(urls) {
    if (!urls || !urls.length) {
        return;
    }
    const container = document.querySelector(".container");
    urls.forEach(url => addImageNode(container, url))
}

/**
 * Function dynamically add a DIV with image and checkbox to 
 * select it to the container DIV
 * @param {*} container - DOM node of a container div 
 * @param {*} url - URL of image 
 */
function addImageNode(container, url) {
    const div = document.createElement("div");
    div.className = "imageDiv";
    const img = document.createElement("img");
    img.src = url;
    div.appendChild(img);
    const checkbox = document.createElement("input");
    checkbox.type = "checkbox";
    checkbox.setAttribute("url",url);            
    div.appendChild(checkbox);
    container.appendChild(div)
}
Enter fullscreen mode Exit fullscreen mode

In this step, we can select each image manually. Now, it's time to make the "Select All" checkbox work, to select/unselect all of them at once.

Implement Select All function

If return to the page.html layout, you'll see that the "Select All" checkbox is an input field with the selectAll id. So, we need to react to user clicks on it. When the user switches it on, all image checkboxes should switch on. When the user switches it off, all image checkboxes also should switch off. In other words, we should listen to the "onChange" event of the "#selectAll" checkbox, and in a handler of this event, set a "checked" status of all checkboxes to be the same as the status of the "Select All" checkbox. This is how it could be implemented in the page.js script:

document.getElementById("selectAll")
        .addEventListener("change", (event) => {
    const items = document.querySelectorAll(".container input");
    for (let item of items) {
        item.checked = event.target.checked;
    };
});
Enter fullscreen mode Exit fullscreen mode

The listening function receives an instance of the onChange event as an event function argument. This instance has a link to the "Select All" node itself in the target parameter, which we can use to determine the current status of this checkbox.

Then, we select all "input" fields inside div with a container class, e.g. all image checkboxes, because there are no other input fields inside this container.

Then, we set the checked status to each of these checkboxes to the status of the "Select All" checkbox. So, each time the user changes the status of that checkbox, all other checkboxes reflect this change.

Now, if you run the extension again, you can select the images to download either manually, or automatically.

The only step left in this section is to download selected images. To do this, we need to make the Download button work.

Implement Download function

After the user selected the images, it should press the Download button, which should run the onClick event listener of this button. The Download button can be identified by the downloadBtn ID. So, we can connect the listener function to this button, using this ID. This function should do three things:

  • Get URLs of all selected images,
  • Download them and compress them to a ZIP archive
  • Prompt the user to download this archive.

Let's define a shape of this function:

document.getElementById("downloadBtn")
        .addEventListener("click", async() => {
            try {
                const urls = getSelectedUrls();
                const archive = await createArchive(urls);
                downloadArchive(archive);
            } catch (err) {
                alert(err.message)
            }
        })

function getSelectedUrls() {
    // TODO: Get all image checkboxes which are checked,
    // extract image URL from each of them and return
    // these URLs as an array
}

async function createArchive(urls) {
    // TODO: Create an empty ZIP archive, then, using 
    // the array of `urls`, download each image, put it 
    // as a file to the ZIP archive and return that ZIP
    // archive
}

function downloadArchive(archive) {
    // TODO: Create an <a> tag
    // with link to an `archive` and automatically
    // click this link. This way, the browser will show
    // the "Save File" dialog window to save the archive
}
Enter fullscreen mode Exit fullscreen mode

The listener runs exactly the actions, defined above one by one.

I put the whole listener body to try/catch block, to implement a uniform way to handle all errors that can happen on any step. If an exception is thrown during processing the list of URLs or compressing the files, this error will be intercepted and displayed as an alert.

Also, part of the actions, that this function will do are asynchronous and return promises. I use the async/await approach to resolve promises, instead of then/catch, to make code easier and cleaner. If you are not familiar with this modern approach, look for a simple clarification here: https://javascript.info/async-await. That is why, to be able to resolve promises using await, the listener function is defined as async(), the same as createArchive function.

Get selected image URLs

getSelectedUrls() function should query all image checkboxes inside .container div, then filter them to keep only checked and then, extract url attribute of these checkboxes. As a result, this function should return an array of these URLs. This is how this function could look:

function getSelectedUrls() {
    const urls = 
        Array.from(document.querySelectorAll(".container input"))
             .filter(item=>item.checked)
             .map(item=>item.getAttribute("url"));
    if (!urls || !urls.length) {
        throw new Error("Please, select at least one image");
    }
    return urls;
}
Enter fullscreen mode Exit fullscreen mode

In addition, it throws an exception if there are no selected checkboxes. Then, this exception is properly handled in the upstream function.

Download images by URLs

The createArchive function uses urls argument to download image files for each url. To download a file from the Internet, need to execute a GET HTTP request to an address of this file. There are many ways for this from Javascript, but the most uniform and modern is by using a fetch() function. This function can be simple or complex. Depending on the kind of request you need to execute, you can construct very specific request objects to pass to that function and then analyze the responses returned. In a simple form, it requires to specify an URL to request and returns a promise with Response object:

response = await fetch(url);
Enter fullscreen mode Exit fullscreen mode

This form we will use for Image Grabber. The full description of the fetch function and its API can find in official docs: https://www.javascripttutorial.net/javascript-fetch-api/.

The function call above will either resolve to the response object or throw an exception in case of problems. The response is an HTTP Response object, which contains the raw received content and various properties and methods, that allow dealing with it. A reference to it you can find in the official docs as well: https://developer.mozilla.org/en-US/docs/Web/API/Response.

This object contains methods to get content in different forms, depending on what is expected to receive. For example response.text() converts the response to a text string, response.json() converts it into a plain Javascript object. However, we need to get binary data of an image, to save it to a file. The type of object, that is usually used to work with binary data in Javascript is Blob - Binary Large Object. The method to get the response content as blob is response.blob().

Now let's implement a part of createArchive function to download the images as Blob objects:

async function createArchive(urls) {
    for (let index in urls) {
        const url = urls[index];
        try {
            const response = await fetch(url);
            const blob = await response.blob();
            console.log(blob);
        } catch (err) {
            console.error(err);
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

In this function, we go over each item of the selected urls array, download each of them to response then, convert the response to blob. Finally, just log each blob to a console.

A blob is an object, which contains the binary data of the file itself and also, some properties of this data, that can be important, in particular:

  • type - The type of file. This is a MIME-type of content - https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types. Depending on MIME-type we can check is it really an image or not. We will need to filter files by their mime types and leave only image/jpeg, image/png, or image/gif. We will do that later, in the next section.

  • size - The size of the image in bytes. This parameter is also important, because if the size is 0, or less than 0, then there is no sense to save this image to a file.

The reference with all parameters and methods of Blob objects you can find here: https://developer.mozilla.org/en-US/docs/Web/API/Blob .

If you read this, you will not find a name or file name property. Blob is only about content, it does not know the name of the file, because the content, returned by the fetch() could be not a file. However, we need to have the names of the images somehow. In the next section, we will create a utility function that will be used to construct a file name, knowing only blob.

Determine file names for images

To put files to the archive, we need to specify a file name for each file. Also, to open these files as images later we need to know an extension for each file. To handle this task, we will define a utility function with the following syntax:

function checkAndGetFileName(index, blob)
Enter fullscreen mode Exit fullscreen mode

Where index is an index of item from urls array and blob is a BLOB object with a content of a file.

To obtain a name of the file we will use just an index of an URL in the input array. We will not use the URL itself, because it can be weird and include various timestamps and other garbage. So, file names will be like '1.jpeg', '2.png', and so on.

To obtain an extension of the file, we will use a MIME-type of blob object of this file, which is stored in blob.type parameter.

In addition, this function will not only construct file name but also check the blob to have the correct size and MIME-type. It will return a file name only if it has a positive size and correct image MIME-type. The correct MIME types for images look like: image/jpeg, image/png or image/gif in which the first part is a word image and the second part is an extension of the image.

So, the function will parse a MIME-type and will return a filename with extension only if the mime-type begins with image. The name of the file is the index and the extension of the file is the second part of its MIME-type:

This is how the function could look:

function checkAndGetFileName(index, blob) {
    let name = parseInt(index)+1;
    const [type, extension] = blob.type.split("/");
    if (type != "image" || blob.size <= 0) {
        throw Error("Incorrect content");
    }
    return name+"."+extension;
}
Enter fullscreen mode Exit fullscreen mode

Now, when we have names of images and their binary content, nothing can stop us from just putting this to a ZIP archive.

Create a ZIP archive

ZIP is one of the most commonly used formats to compress and archive data. If you compress files by ZIP and send it somewhere, you can be confident on about 100% that receiving party will be able to open it. This format was created and released by PKWare company in 1989: https://en.wikipedia.org/wiki/ZIP_(file_format). Here you can find not only history but also a structure of ZIP file and algorithm description, which can be used to implement binary data compression and decompression using this method. However, here we will not reinvent the wheel, because it's already implemented for all or almost all programming languages, including Javascript. We will just use the existing external library - JSZip. You can find it here: https://stuk.github.io/jszip/.

So, we need to download a JSZip library script and include it in page.html, before page.js. The direct download link is the following: http://github.com/Stuk/jszip/zipball/master. It will download an archive with all source code and release versions. This is a big archive, but you really need only a single file from it: dist/jszip.min.js.

Create a lib folder inside the extension path, extract this file to it, and include this script to the page.html, before page.js:

<!DOCTYPE html>
<html>
    <head>
        <title>Image Grabber</title>
    </head>
    <body>
        <div class="header">
            <div>
                <input type="checkbox" id="selectAll"/>&nbsp;
                <span>Select all</span>
            </div>
            <span>Image Grabber</span>
            <button id="downloadBtn">Download</button>    
        </div>
        <div class="container">      
        </div>
        <script src="/lib/jszip.min.js"></script>
        <script src="/page.js"></script>        
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

When it is included, it creates a global JSZip class, that can be used to construct ZIP archives and add content to them. This process can be described by the following code:

const zip = new JSZip();
zip.file(filename1, blob1);
zip.file(filename2, blob2);
.
.
.
zip.file(filenameN, blobN);
const blob = await zip.generateAsync({type:'blob'});
Enter fullscreen mode Exit fullscreen mode

First, it creates an empty zip object. Then, it starts adding files to it. File defined by name, and blob with binary content of this file. Finally, the generateAsync method is used to generate a ZIP archive from previously added files. In this case, it returns generated archive as a blob, because we already know what is BLOB and how to work with it. However, you can learn JSZip API documentation for other options: https://stuk.github.io/jszip/documentation/api_jszip.html.

Now we can integrate this code to createArchive function to create an archive from all image files and return a BLOB of this archive:

async function createArchive(urls) {
    const zip = new JSZip();
    for (let index in urls) {
        try {
            const url = urls[index];
            const response = await fetch(url);
            const blob = await response.blob();
            zip.file(checkAndGetFileName(index, blob),blob);
        } catch (err) {
            console.error(err);
        }
    };
    return await zip.generateAsync({type:'blob'});
}

function checkAndGetFileName(index, blob) {
    let name = parseInt(index)+1;
    [type, extension] = blob.type.split("/");
    if (type != "image" || blob.size <= 0) {
        throw Error("Incorrect content");
    }
    return name+"."+extension;
}
Enter fullscreen mode Exit fullscreen mode

Here, when adding each image file to the zip, we use the previously created checkAndGetFileName function to generate a filename for this file.

Also, the body of the loop is placed to try/catch block, so any exception that is thrown by any line of code will be handled inside that loop. I decided to not stop the process in case of exceptions here, but just skip the file, which resulted in an exception and only show an error message to the console.

And finally, it returns generated BLOB with zip archive, which is ready to download.

Download a ZIP archive

Usually, when we want to invite users to download a file, we show them the link, pointing to this file, and ask them to click it to download this file. In this case, we need to have a link, which points to the BLOB of the archive. BLOB objects can be very big, that is why web browser stores them somewhere and, fortunately, there is a function in Javascript, which allow getting a link to a BLOB object:

window.URL.createObjectURL(blob)
Enter fullscreen mode Exit fullscreen mode

So, we can create a link to a blob of ZIP-archive. What is more, we can automatically click this link, to not ask users to do this, because they already clicked the "Download" button in the beginning.

Finally, this is how the downloadArchive function looks:

function downloadArchive(archive) {
    const link = document.createElement('a');
    link.href = URL.createObjectURL(archive);
    link.download = "images.zip";        
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);    
}
Enter fullscreen mode Exit fullscreen mode

This code dynamically creates an 'a' element and points it to the URL of the archive blob. Also, it sets the name of the downloaded file to images.zip. Then it injects this invisible link into a document and clicks it. This will trigger the browser to either show the "File Save" window or automatically save a file with the name of images.zip and the content of the ZIP archive. Finally, the function removes this link from a document, because we do not need it anymore after the click.

Code cleanup

This is the final step of the "Download" function implementation. Let's cleanup, comment, and memorize the whole code, which we created in page.js:

/**
 * Listener that receives a message with a list of image
 * URL's to display from popup.
 */
chrome.runtime.onMessage
    .addListener((message,sender,sendResponse) => { 
        addImagesToContainer(message)
        sendResponse("OK");
    });

/**
 * Function that used to display an UI to display a list 
 * of images
 * @param {} urls - Array of image URLs
 */
function addImagesToContainer(urls) {
    if (!urls || !urls.length) {
        return;
    }
    const container = document.querySelector(".container");
    urls.forEach(url => addImageNode(container, url))
}

/**
 * Function dynamically add a DIV with image and checkbox to 
 * select it to the container DIV
 * @param {*} container - DOM node of a container div 
 * @param {*} url - URL of image 
 */
function addImageNode(container, url) {
    const div = document.createElement("div");
    div.className = "imageDiv";
    const img = document.createElement("img");
    img.src = url;
    div.appendChild(img);
    const checkbox = document.createElement("input");
    checkbox.type = "checkbox";
    checkbox.setAttribute("url",url);            
    div.appendChild(checkbox);
    container.appendChild(div)
}

/**
 * The "Select All" checkbox "onChange" event listener
 * Used to check/uncheck all image checkboxes
 */
document.getElementById("selectAll")
         .addEventListener("change", (event) => {
    const items = document.querySelectorAll(".container input");
    for (let item of items) {
        item.checked = event.target.checked;
    };
});

/**
 * The "Download" button "onClick" event listener
 * Used to compress all selected images to a ZIP-archive 
 * and download this ZIP-archive
 */
document.getElementById("downloadBtn")
        .addEventListener("click", async() => {
            try {
                const urls = getSelectedUrls();
                const archive = await createArchive(urls);
                downloadArchive(archive);
            } catch (err) {
                alert(err.message)
            }
        })

/**
 * Function used to get URLs of all selected image
 * checkboxes
 * @returns Array of URL string 
 */
function getSelectedUrls() {
    const urls = 
        Array.from(document.querySelectorAll(".container input"))
             .filter(item=>item.checked)
             .map(item=>item.getAttribute("url"));
    if (!urls || !urls.length) {
        throw new Error("Please, select at least one image");
    }
    return urls;
}

/**
 * Function used to download all image files, identified 
 * by `urls`, and compress them to a ZIP
 * @param {} urls - list of URLs of files to download
 * @returns a BLOB of generated ZIP-archive
 */
async function createArchive(urls) {
    const zip = new JSZip();
    for (let index in urls) {
        try {
            const url = urls[index];
            const response = await fetch(url);
            const blob = await response.blob();
            zip.file(checkAndGetFileName(index, blob),blob);
        } catch (err) {
            console.error(err);
        }
    };
    return await zip.generateAsync({type:'blob'});
}

/**
 * Function used to return a file name for
 * image blob only if it has a correct image type
 * and positive size. Otherwise throws an exception.
 * @param {} index - An index of URL in an input
 * @param {*} blob - BLOB with a file content 
 * @returns 
 */
function checkAndGetFileName(index, blob) {
    let name = parseInt(index)+1;
    const [type, extension] = blob.type.split("/");
    if (type != "image" || blob.size <= 0) {
        throw Error("Incorrect content");
    }
    return name+"."+extension.split("+").shift();
}

/**
 * Triggers browser "Download file" action
 * using a content of a file, provided by 
 * "archive" parameter
 * @param {} archive - BLOB of file to download
 */
function downloadArchive(archive) {
    const link = document.createElement('a');
    link.href = URL.createObjectURL(archive);
    link.download = "images.zip";        
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);    
}
Enter fullscreen mode Exit fullscreen mode

Now, you can click the "GRAB NOW" button, then, either automatically or manually select the images to download, press the "Download" button and save a ZIP archive with these images:

Final functionaliy

However, it looks not perfect. It is almost impossible to use this in practice. Let's style this page properly.

Styling the extension page

At the current stage, all markup and functionality of the extension page are ready. All classes and IDs are defined in HTML. It's time to add CSS, to style it. Create a page.css file at the same folder with page.html and others and add this stylesheet to the page.html:

<!DOCTYPE html>
<html>
    <head>
        <title>Image Grabber</title>
        <link href="/page.css" rel="stylesheet" type="text/css"/>
    </head>
    <body>
        <div class="header">
            <div>
                <input type="checkbox" id="selectAll"/>&nbsp;
                <span>Select all</span>
            </div>
            <span>Image Grabber</span>
            <button id="downloadBtn">Download</button>    
        </div>
        <div class="container">      
        </div>
        <script src="/lib/jszip.min.js"></script>
        <script src="/page.js"></script>        
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Then add the following content to the page.css:

body {
    margin:0px;
    padding:0px;
    background-color: #ffffff;
}

.header {    
    display:flex;
    flex-wrap: wrap;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    width:100%;
    position: fixed;
    padding:10px;
    background: linear-gradient( #5bc4bc, #01a9e1);
    z-index:100;
    box-shadow: 0px 5px 5px #00222266;
}

.header > span {
    font-weight: bold;
    color: black;
    text-transform: uppercase;
    color: #ffffff;
    text-shadow: 3px 3px 3px #000000ff;
    font-size: 24px;
}

.header > div {
    display: flex;
    flex-direction: row;
    align-items: center;
    margin-right: 10px;
}

.header > div > span {
    font-weight: bold;
    color: #ffffff;
    font-size:16px;
    text-shadow: 3px 3px 3px #00000088;
}

.header input {
    width:20px;
    height:20px;
}

.header > button {
    color:white;
    background:linear-gradient(#01a9e1, #5bc4bc);
    border-width:0px;
    border-radius:5px;
    padding:10px;
    font-weight: bold;
    cursor:pointer;
    box-shadow: 2px 2px #00000066;
    margin-right: 20px;
    font-size:16px;
    text-shadow: 2px 2px 2px#00000088;
}

.header > button:hover {
    background:linear-gradient( #5bc4bc,#01a9e1);
    box-shadow: 2px 2px #00000066;
}

.container {
    display: flex;
    flex-wrap: wrap;
    flex-direction: row;
    justify-content: center;
    align-items: flex-start;
    padding-top: 70px;
}

.imageDiv {
    display:flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    position:relative;
    width:150px;
    height:150px;
    padding:10px;
    margin:10px;
    border-radius: 5px;
    background: linear-gradient(#01a9e1, #5bc4bc);
    box-shadow: 5px 5px 5px #00222266;
}

.imageDiv:hover {
    background: linear-gradient(#5bc4bc,#01a9e1);
    box-shadow: 10px 10px 10px #00222266;
}

.imageDiv img {
    max-width:100%;
    max-height:100%;
}

.imageDiv input {
    position:absolute;
    top:10px;
    right:10px;
    width:20px;
    height:20px;
}
Enter fullscreen mode Exit fullscreen mode

After body styling, it defines styling for the set of selectors of the content of .header div, and then, for the set of selectors of the content of .container div. The key part of this styling is using the Flexbox layout with the 'flex-wrap' option. It is used both for header and container. It makes the whole layout responsive. The components rearrange themselves properly on a screen of any size:

Styled page

Styled page.

You can read about using of Flexbox layout, for example, here: https://css-tricks.com/snippets/css/a-guide-to-flexbox/. Information about all other used CSS styles you can easily find in any CSS reference.

Publish and distribute the extension

Now the work is finished and the extension is ready for release. How to show it to other people? Send them this folder with files and explain how to install unpacked extension using chrome://extensions tab? Of course not, this is not a proper way to distribute Chrome extensions. The proper way is to publish the extension to the Chrome Web Store and send a link to a page, where it is published to everyone you want and publish this link on all your online resources.

For example, this is a link to an Image Reader extension, which I created and published recently:

https://chrome.google.com/webstore/detail/image-reader/acaljenpmopdeajikpkgbilhbkddjglh

This is how it looks on the Chrome Web Store:

Web Store Extension

People can read the description of the extension, see screenshots and finally press the Add to Chrome button to install it.

As you see here, to publish an extension, you need to provide not only the extension itself but also an image of extension, screenshots, description, specify a category of extension, and other parameters.

The rules of publishing change from time to time, that is why it's better to use the official Google website to see a guide on how to set up a Chrome Web Developer Account, upload the extension to it, and then publish it. This is the root of information in the official documentation: https://developer.chrome.com/docs/webstore/publish/. Google describes here everything you need to do and updates this page when the rules change.

I can specify a list of key points here to get started easily. (However, it's actual only today, maybe in a week or later something in a Google rules will change, so do not rely on this list too much, just use it as general info):

  • Archive your extension folder to a zip file

  • Register as a Chrome Web Store developer on this page: https://chrome.google.com/webstore/devconsole/ . You can use an existing Google account (for example, if you have an account used for a Gmail, it will work).

  • Pay one time $5 registration fee

  • Using Chrome Web Store Developer console, create a new product in it and upload the created ZIP archive to it.

  • Fill required fields in a product form with information about product name and description. Upload a product picture and screenshots of different sizes. This information can be variable, that is why I think that you will need to prepare it in a process of filling out this form.

  • It's not required to fill all fields in a single run. You can complete part of the form and press the "Save Draft" button. Then, return back, select your product and continue filling.

  • After all fields are completed, press the "Submit for Review" button, and, if the form is completed without mistakes, the extension will be sent to Google for review. The review can take time. The status of the review will be displayed on the products list.

  • You have to check from time to time the status of your submission because Google does not send any notifications by email about review progress.

  • After successful review, the status of the product will change to "Published" and it will be available on Google Chrome Web Store: https://chrome.google.com/webstore/. People will be able to find it and install it.

In the case of my extension on the screenshot above, the Google review took two days and it was published successfully. I hope the same will be with you, or even faster. Good luck!

Conclusion

Creating Google Chrome Extensions is an easy way to distribute your web application worldwide, using a global worldwide platform, that just works and does not require any support and promotion. This way you can easily deliver your online ideas almost at no cost. What is more, you can enrich the features of your existing websites with browser extensions to make your users feel more comfortable working with your online resources. For example, the extension, which I recently published, used to work with an online text recognition service - "Image Reader" (https://ir.germanov.dev). Using this service, you can get an image from any website, paste it to the interface and recognize a text on it. The browser extension for this service helps to send images from any browser tab to this service automatically. Without the extension, the user needs to make 5 mouse clicks to do that, but with extension, the same can be done in just two mouse clicks. This is a great productivity improvement. You can watch this video to see, how that extension helps to deliver images to the web service using the context menu:

I believe that you can find a lot of ways how to use web browser automation via extensions to increase the productivity and comfort level of your online users, to make their work with your online resources better, faster, and smarter. I hope that my tutorial opened the world of web browser extensions for you. However, I did not clarify even a few percent of the features, that exist in this area. Perhaps I will write more about this soon.

Full source code of the Image Grabber extension you can clone from my GitHub repository:

https://github.com/AndreyGermanov/image_grabber.

Please write if you have something to add or found bugs or what to improve.

Feel free to connect and follow me on social networks where I publish announcements about my new articles, similar to this one and other software development news:

LinkedIn: https://www.linkedin.com/in/andrey-germanov-dev/
Facebook: https://web.facebook.com/AndreyGermanovDev
Twitter: https://twitter.com/GermanovDev

My online services website: https://germanov.dev

Happy coding guys!

Latest comments (0)