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();
});
}
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
}
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"/>
<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:
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)
createProperties
is an object with parameters, that tell Chrome, which tab to open and how. In particular, it has theurl
parameter, that will be used to specify which page to open in the tabcallback
is a function that will be called after the tab is created. This function has a single argumenttab
, that contains an object of the created tab, which, among others, contains anid
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
});
}
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:
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}, ...
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)
-
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});
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});
}
);
}
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"/>
<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.
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 argumentresponseObject
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});
});
}
);
}
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);
}
);
}
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));
}
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 ofurls
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):
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)
}
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 thecontainer
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 thecontainer
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>
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:
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)
}
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;
};
});
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
}
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;
}
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);
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);
}
};
}
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
, orimage/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)
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;
}
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"/>
<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:
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:
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:
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:
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
:
/**
* 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:
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"/>
<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
:
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:
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:
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!
Top comments (0)