Create a Google Chrome Extension Part 2: Image Grabber
Tags: javascript, webdev, tutorial, css
Published: 2022-02-12 15:16:35

# Table Of Contents

[Introduction](#introduction)
[Create and open a web page with a list of images](#create-and-open-a-web-page-with-a-list-of-images)
&nbsp;&nbsp;&nbsp;&nbsp;[Open a new tab with a local extension page](#open-a-new-tab-with-a-local-extension-page)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[Create a page.html](#create-a-page-html)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[Open a new browser tab](#open-a-new-browser-tab)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[Send image URLs data to the page](#send-image-urls-data-to-the-page)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[Receive image URLs data on the page](#receive-image-urls-data-on-the-page)
[Create Image Downloader interface](#create-image-downloader-interface)
&nbsp;&nbsp;&nbsp;&nbsp;[Create UI to display and select images](#create-ui-to-display-and-select-images)
&nbsp;&nbsp;&nbsp;&nbsp;[Implement Select All function](#implement-select-all-function)
&nbsp;&nbsp;&nbsp;&nbsp;[Implement Download function](#implement-download-function)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[Get selected image URLs](#get-selected-image-urls)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[Download images by URLs](#download-images-by-urls)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[Determine file names for images](#determine-file-names-for-images)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[Create a ZIP archive](#create-a-zip-archive)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;[Download a ZIP archive](#download-a-zip-archive)
[Code cleanup](#code-cleanup)
[Styling the extension page](#styling-the-extension-page)
[Publish and distribute the extension](#publish-and-distribute-the-extension)
[Conclusion](#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](Create a Google Chrome Extension Part 1: Image Grabber)

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 &lt;img&gt; 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.

{% youtube lUgfbmkc_5U %}

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:

```javascript
/**
 * 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();
          });
}
```

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:

```javascript
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
}
```

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:

```html
<!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>
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v40ot5mfv3bg34zajmlz.png)

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

```javascript
chrome.tabs.create(createProperties,callback)
```

* `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:

```javascript
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
    });
}
```
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r43cbgxmmgwc91c5ao7m.png)

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

```javascript
chrome.tabs.create({url: 'page.html', active: false}, ...
```

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:

```javascript
chrome.tabs.update(tabId,updateProperties,callback)
```

* `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:

```javascript
chrome.tabs.update(tab.id,{active:true});
```

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.

```javascript
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});
        }
    );
}
```

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:

```html
<!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>
```

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.

```javascript
chrome.tabs.sendMessage(tabId, message, responseFn)
```

* `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:

```javascript
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});
            });                            
        }
    );
}
```

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:

```javascript
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);
        }
    );
}
```

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

```javascript
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));
}

```
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l31ddpmxj7epffw4ioft.png)

## 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 &lt;div&gt;:

```javascript
/**
 * 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)
}
```

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:

```html
<div class="imageDiv">
    <img src={url}/>
    <input type="checkbox" url={url}/>
</div>
``` 
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z3qq7l08on2yqqzwl3wm.png)

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:

```javascript
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)
}
```
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:

```javascript
document.getElementById("selectAll")
        .addEventListener("change", (event) => {
    const items = document.querySelectorAll(".container input");
    for (let item of items) {
        item.checked = event.target.checked;
    };
});
```
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:

```javascript
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
}
```

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:

```javascript
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;
}
```
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:

```javascript
response = await fetch(url);
```

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:

```javascript
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);
        }
    };
}
```

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:

```javascript
function checkAndGetFileName(index, blob)
```

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:

```javascript
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;
}
```
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`: 

```html
<!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>
```

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:

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

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:

```javascript
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;
}
```

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:

```javascript
window.URL.createObjectURL(blob)
```

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:

```javascript
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);    
}
```
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`:

```javascript
/**
 * 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);    
}
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z3qq7l08on2yqqzwl3wm.png)

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

```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>
```
Then add the following content to the `page.css`:

```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;
}
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bno8z7ssdp66y0gsga2k.png)

![Styled page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iaifngkzbmsowkgj3oe3.png).

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9y4biifzv1k9qoj5ewvj.png)

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:

{% youtube LrnVEmgvnpg %}

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!