paint-brush
How to Create a Google Chrome Extension: Image Grabberby@germanov
4,912 reads
4,912 reads

How to Create a Google Chrome Extension: Image Grabber

by Andrey GermanovDecember 1st, 2022
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

In this article, I will show, how to create a Chrome Extension from scratch. The extension which we will create today will use Chrome APIs to get access to the content of web pages and extract different information from them. The resulting extension will provide an interface to connect to a website, read all images from it, grab their absolute URLs and copy these URLs to a clipboard. This feature can be used for a wide range of browser automation tasks like scrapping required information from websites or automating web surfing, which can be useful to automate user interface testing. In the second part of this tutorial I will explain how to extend the interface of the extension to select and download grabbed images as a ZIP archive.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - How to Create a Google Chrome Extension: Image Grabber
Andrey Germanov HackerNoon profile picture

Introduction

Chrome Extensions are small programs, that can be installed on the Google Chrome web browser to enrich its features. Usually, to install a Chrome Extension, a user should open Chrome Web Store, find the required extension, and install it from there.


In this article, I will show, how to create a Chrome Extension from scratch. The extension which we will create today will use Chrome APIs to get access to the content of web pages, which opened in a web browser and extract different information from them. Using these APIs you can not only read content from web pages but also write content to them and interact with these pages, like, for example, automatically pressing buttons or following links. This feature can be used for a wide range of browser automation tasks like scrapping required information from websites or automating web surfing, which can be useful to automate user interface testing.


In this article, I will guide you through the process of building an extension named Image Grabber. The resulting extension will provide an interface to connect to a website, read all images from it, grab their absolute URLs and copy these URLs to a clipboard. During this process, you will know about foundational parts of the Google Chrome extension that can be reused to build an extension of any kind.


By the end of this article, you will build an extension that looks and works as shown in this video.

This is only the first part of this tutorial. In the second part, I will show how to extend the interface of the extension to select and download grabbed images as a ZIP archive and then explain how to publish the extension to Google Chrome WebStore.

Basic extension structure

Google Chrome Extension is a web application, that can contain any number of HTML pages, CSS stylesheets, JavaScript files, images, any other files and a manifest.json file in the same folder, which defines how this particular extension will look and work.

Minimal Chrome extension

Minimal Chrome Extension consists only of the manifest.json file. This is an example of a basic manifest.json file that can be used as a template when start creating any new Chrome extension:


{
    "name": "Image Grabber",
    "description": "Extract all images from current web page",
    "version": "1.0",
    "manifest_version": 3,
    "icons": {},
    "action": {},
    "permissions": [],
    "background":{}
}


The only required parameters are name, description, version and manifest_version. manifest_version should be equal to 3. Values of other parameters are up to you, they should clearly describe your extension and its version. In this example, I described the Image Grabber extension, which will extract links of all images from a current browser page.

You can see a full list of options that can be specified in the manifest.json file in the official documentation.


A folder with a single manifest.json file is a minimal runnable Chrome Extension that can be packaged, installed to Chrome, and distributed. This minimal extension will have a default look and will do nothing until we define other parameters: icons, action, permissions, and background.


So, let's create the image_grabber folder and put the manifest.json file with that default content. Then, let's just install this extension to Chrome.

Install Chrome Extension

When you develop an extension, it has a form of a folder with files. In Chrome extensions terms it is called unpacked extension. After you finish development, you will need to pack the extension folder to an archive with a .crx extension using the Chrome extensions manager. This archive then can be used to upload to Chrome Web Store from which users can install your extension to their browsers.


To test and debug extension during development, you can install unpacked extension to Chrome. To do this, type chrome://extensions in the browser's URL string to open the Chrome Extensions Manager.



To install and debug extensions during development turn on the Developer mode switch on the right side of the Extensions panel. It will show extensions management panel:



Then press the Load unpacked button and select a folder with the extension. Point it to our minimal image_grabber extension. Right after this, a panel for the Image Grabber extension will appear in a list of installed extensions:



The Image Grabber extension panel shows a unique ID, description, and version of the extension. Every time when changing the manifest.json file, you need to press the Reload icon on the extension panel to reload the updated extension:



To use the extension in the browser, you can find it in a list of Chrome installed extensions. To see this list, press the Extensions icon button



on the right side of the Chrome URL bar and find the 'Image Grabber' item in the dropdown list. You can also press the "Pin" icon button at the right side of the extension to place an icon of the extension to the browser toolbar on the same line with other common extensions:



After Pin the extension, its default icon will appear on the toolbar:



That's all. We installed the minimal working Chrome extension. However, it looks like a simple "I" symbol on a gray background and nothing happens when you click on it. Let's add other missing parts to the manifest.json to change this.

Add extension icons

The icons parameter in the manifest.json file has a format of Javascript object, which defines locations of icons of various sizes. Extension should have icons of different sizes: 16x16 px, 32x32 px, 48x48 px and 128x128 px. Icons are ".PNG" images that should be placed anywhere in the extension folder. Image files can have any names. I have created 4 icons of appropriate sizes in 16.png, 32.png, 48.png, and 128.png files and put them into the icons folder inside the extension root folder. Then, manifest.json should be pointed to these locations using the icons parameter in a way, as shown below:


{
    "name": "Image Grabber",
    "description": "Extract all images from current web page",
    "version": "1.0",
    "manifest_version": 3,
    "icons": {
        "16":"icons/16.png",
        "32":"icons/32.png",
        "48":"icons/48.png",
        "128":"icons/128.png"
    },
    "action": {},
    "permissions": [],
    "background":{}
}


Paths to icon files are specified as relative paths.


After this is done, press the Reload button on the Image Grabber extension panel on the chrome://extensions tab to apply changed manifest.json. As a result, you should see that the icon of the extension on the toolbar changed, as displayed below:


Now it looks better, but if you press this icon, nothing happens. Let's add actions to this extension.

Create the extension interface

An extension should do something, it should run some actions to have a sense. The extension allows to run actions in two ways:


  • In the background, when extension starts
  • From an interface of the extension, when a user interacts with it using buttons or other UI controls

The extension can use both options at the same time.


To run actions in the background, you have to create a JS script and specify its location in the background parameter of manifest.json. This script can define listeners for a wide range of browser events, for example: when the extension is installed, when a user opens/closes a tab in a browser when the user adds/removes a bookmark, and many others. Then this script will run in the background all the time and react to each of these events by running Javascript code from event handling functions.


For this extension, I will not use this feature, so the background parameter of manifest.json will be empty. It's included only to make the manifest.json file to be useful as a starting template for a Chrome extension of any kind, but in the Image Grabber extension, the only action is "Grab images" and it will run only from a user interface when the user explicitly press the "GRAB NOW" button.


To run actions from the interface, we need to define an interface. Interfaces for Chrome extensions are HTML pages, which can be combined with CSS stylesheets to style these pages, and Javascript files, which define actions to run when the user interacts with elements of that interface. The main interface is an interface, displayed when the user clicks on the extension icon on the toolbar and it should be defined in the action parameter of the manifest.json file. Depending on how the interface is defined, it can be opened as a new tab in the browser or displayed as a popup window below the extension button, when the user presses it.


The Image Grabber extension uses the second option. It displays a popup with a header and the "GRAB NOW" button. So, let's define this in the manifest.json:


{
    "name": "Image Grabber",
    "description": "Extract all images from current web page",
    "version": "1.0",
    "manifest_version": 3,
    "icons": {
        "16":"icons/16.png",
        "32":"icons/32.png",
        "48":"icons/48.png",
        "128":"icons/128.png"
    },
    "action": {
        "default_popup":"popup.html"
    },
    "permissions": [],
    "background":{}
}


So, as defined here, the main interface is a popup window and the content of this popup window should be in the popup.html file. This file is an ordinary HTML page. So, create the popup.html file in the extension folder with the following content:


<!DOCTYPE html>
<html>
    <head>
        <title>Image Grabber</title>
    </head>
    <body>
        <h1>Image Grabber</h1>
        <button id="grabBtn">GRAB NOW</button>
    </body>
</html>


This is a simple page with the "Image Grabber" header and the "GRAB NOW" button which has a "grabBtn" id.


Go to chrome://extensions to reload the Image Grabber extension. Now you can press the extension icon to see the popup window with the interface:


It works but looks not enough perfect. Let's style it using CSS. Create the following popup.css file in the extension folder:


body {
    text-align:center;
    width:200px;
}

button {
    width:100%;
    color:white;
    background:linear-gradient(#01a9e1, #5bc4bc);
    border-width:0px;
    border-radius:20px;
    padding:5px;
    font-weight: bold;
    cursor:pointer;
}


This CSS defines that the body should have a width of 200px. This way the size of the popup window should be defined for a Chrome extension. If not defined, then the extension will use a minimum size required to display the content.


Then, add this popup.css stylesheet to the header of the popup.html page:


<!DOCTYPE html>
<html>
    <head>
        <title>Image Grabber</title>
        <link rel="stylesheet" type="text/css" href="popup.css"/>
    </head>
    <body>
        <h1>Image Grabber</h1>
        <button id="grabBtn">GRAB NOW</button>
    </body>
</html>


So, when all this is in place, you can click on the extension icon again to see the styled popup window:



As you could notice, you do not need to reload extension every time when modify HTML or any other file. You have to reload the extension only when change the manifest.json.


Now, to make our UI complete, let's add a Javascript code to react on the "GRAB NOW" button click event. Here is one important note, Chrome does not allow to have any inline Javascript in HTML pages of extensions. All Javascript code should be defined only in separate .js files.


That is why create a popup.js file in the extensions folder with the following placeholder code:


const grabBtn = document.getElementById("grabBtn");
grabBtn.addEventListener("click",() => {    
    alert("CLICKED");
})

and include this script file to the popup.html page:

<!DOCTYPE html>
<html>
    <head>
        <title>Image Grabber</title>
        <link rel="stylesheet" type="text/css" href="popup.css"/>
    </head>
    <body>
        <h1>Image Grabber</h1>
        <button id="grabBtn">GRAB NOW</button>
        <script src="popup.js"></script>
    </body>
</html>


This code adds the onClick event listener to a button with grabBtn ID. Now, if you open the extension popup and click the "GRAB NOW" button, it should display an alert box with "CLICKED" text.


Finally, we have a complete layout of an extension with a styled interface and event handling script for it.


At the current stage, this is an extension, that can be used as a base template to start building a wide range of Chrome extensions, based on a popup user interface.


Now let's implement a "business logic" of this concrete extension - the onClick handler for the "GRAB NOW" button to get a list of image URLs from the current browser page and copy it to a clipboard.

Implement the "GRAB NOW" function

Using Javascript in extension you can do everything that you can do using Javascript on a website: open other HTML pages from current one, make requests to a remote servers, upload data from extension to the remote locations and whatever else. But in addition to this, if this script executed in a chrome extension, you can use Chrome browser APIs to communicate with the browser objects: to read from them and to change them. Most of Google Chrome APIs available through chrome namespace. In particular, for Image Grabber extension we will use the following APIs:


  • chrome.tabs - Chrome Tabs API. It will be used to access an active tab of the Chrome browser.
  • chrome.scripting - Chrome Scripting API. It will be used to inject and execute JavaScript code on a web page, that opened in the active browser tab.

Obtain required permissions

By default, for security reasons, Chrome does not permit access to all available APIs. The extension should declare, which permissions it requires in the permissions parameter of the manifest.json. There are many permissions that exist, all they described in the official documentation here:

https://developer.chrome.com/docs/extensions/mv3/declare_permissions/. For Image Grabber we need two permissions with the following names:


  • activeTab - to obtain access to the active tab of a browser

  • scripting - to obtain access to the Chrome Scripting API to inject and execute JavaScript scripts in different places of the Chrome browser.


To obtain those permissions, need to add their names to the permissions array parameter of the manifest.json:


{
    "name": "Image Grabber",
    "description": "Extract all images from current web page",
    "version": "1.0",
    "manifest_version": 3,
    "icons": {
        "16":"icons/16.png",
        "32":"icons/32.png",
        "48":"icons/48.png",
        "128":"icons/128.png"
    },
    "action": {
        "default_popup":"popup.html",
    },
    "permissions": ["scripting", "activeTab"],
    "background":{}
}


and reload the extension on chrome://extensions panel.


This is a final manifest.json for this project. Now, it has all required parts: icons, link to the main popup interface, and the permissions, that this interface requires.

Get information about the active browser tab

To query information about browser tabs, we will use the chrome.tabs.query function, which has the following signature:


chrome.tabs.query(queryObject,callback)


  • The queryObject is a Javascript object with parameters that define search criteria for browser tabs, which we need to get.
  • The callback - is a function, that is called after the query is complete. This function is executed with a single parameter tabs, which is an array of found tabs, that meet specified search criteria. Each element of the tabs array is aTab object. The Tab object describes the found tab and contains a unique ID of the tab, its title, and other information.


Here I will not completely describe queryObject format and the returned Tab object. You can find this information in a chrome.tabs API reference here:

https://developer.chrome.com/docs/extensions/reference/tabs/.


For the purpose of the Image Grabber extension, we need to query the tab which is active. The query to search this kind of tab is {active: true}.


Let's write a code to get information about the active tab to the "GRAB NOW" button onClick handler:


const grabBtn = document.getElementById("grabBtn");
grabBtn.addEventListener("click",() => {    
    chrome.tabs.query({active: true}, (tabs) => {
        const tab = tabs[0];
        if (tab) {
            alert(tab.id)
        } else {
            alert("There are no active tabs")
        }
    })
})


This code executes a query to get all tabs that are active. After the query is finished, it calls a callback with an array of found tabs in the tabs argument. Only one tab can be active, so we can assume that this is the first and only item of the tabs array. If the active tab exists, we show an ID of that tab in an alert box (we will replace this alert with reasonable code in the next section). However, if there are no active tabs, we alert the user about that.


Now, if you open the extension and press the "GRAB NOW" button, it should show an alert window with a numeric ID of the active tab.


In the next section, we will use this ID to manipulate the content of a web page, displayed on that tab.

Grab images from the current page

The extension can communicate with open pages of the Chrome browser using Chrome Scripting JavaScript API, located in the chrome.scripting namespace. In particular, we will use this API to inject a script to a web page of the current tab, execute this script and return the result back to the extension. When running, it has access to all content of a web page, to which this script is injected.


The only function of chrome.scripting API which is used for this extension is executeScript. It has the following signature:


chrome.scripting.executeScript(injectSpec,callback)


injectSpec

This is an object of ScriptInjection type. It defines where and how to inject the script. target parameter of this object is used to specify "where" to inject the script - the ID of the browser tab to which the script should be injected. Then other parameters of this object define "how" to inject the script. The script can be injected as:


  • file or files - in this case, need to specify an array of Javascript files to inject. The files should exist in the extension folder.
  • function - in this case, need to specify a function to inject. The function should exist in the same (popup.js) file.


The script, which we need to inject will be used to get all images of a target page and return their URLs. This is a small script, so we will inject it as a function, located in the same popup.js file. So, injectSpec for this case will look like this:


{
    target:{ tabId: tab.id, allFrames: true },
    func: grabImages,
}, 


Here we use the id of the tab object, that we received in the previous step as a target to inject script to. Also, there is a allFrames option set, which tells that the injected script should be executed in each embedded frame of the target page if that page has embedded frames. As a script, we will inject a grabImages function which will be defined later.


callback

The injected function will do actions on a target web page and on all embedded frames of this page (each frame is a separate page as well) and will return the result. After this happens, the extension will execute the callback function with returned results as an argument. An argument of the function is an array of objects of InjectionResult type for each frame. Each object contains the "result" property, which is an actual result, that the grabImages function returns.


Now, let's join all parts together:


const grabBtn = document.getElementById("grabBtn");
grabBtn.addEventListener("click",() => {    
    chrome.tabs.query({active: true}, function(tabs) {
        var tab = tabs[0];
        if (tab) {
            chrome.scripting.executeScript(
                {
                    target:{tabId: tab.id, allFrames: true},
                    func:grabImages
                },
                onResult
            )
        } else {
            alert("There are no active tabs")
        }
    })
})

function grabImages() {
    // TODO - Query all images on a target web page
    // and return an array of their URLs
}

function onResult(frames) {
    // TODO - Combine returned arrays of image URLs,
    // join them to a single string, delimited by 
    // carriage return symbol and copy to a clipboard
}


Then, this is how the grabImages function is implemented:


/**
 * Executed on a remote browser page to grab all images
 * and return their URLs
 * 
 *  @return Array of image URLs
 */
function grabImages() {
    const images = document.querySelectorAll("img");
    return Array.from(images).map(image=>image.src);    
}


This function will run on a target web page, so, the document, specified inside it is a document DOM node of a target web page. This function queries a list of all img nodes from a document, then, converts this list to an array and returns an array of URLs (image.src) of these images. This is a very raw and simple function, so as homework you can customize it: apply different filters to this list, cleanup URLS, by removing "query" strings from them, and so on, to make a resulting list look perfect.


After this function is executed in each frame of the target web page, result arrays will be combined and sent to the onResult callback function, which could look 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 the remote end 
    // and could not return results
    if (!frames || !frames.length) { 
        alert("Could not retrieve images from specified page");
        return;
    }
    // Combine arrays of the 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();
          });
}


Not all tabs that opened in the browser are tabs with web pages inside. For example, a tab with a list of extensions, or a tab with browser settings are not tabs with web pages. If you try to run a script with the document object on these tabs it will fail and return nothing. That is why at the beginning of the onResult function we check the result and continue only if it exists. Then we combine arrays of image URLs returned for each frame to a single array by using map/reduce combination and then, use window.navigator.clipboard API to copy joined to string array to a clipboard. writeText function is asynchronous, so we have to wait until it finishes by resolving a promise, that it returns. And when it is resolved, we close the popup window of the extension.

I have explained only a single function of Chrome scripting API and only in the context of the Image Grabber extension. You can see the full documentation for Chrome Scripting API to clarify all missing parts: https://developer.chrome.com/docs/extensions/reference/scripting/ .

Code cleanup

The last thing that I would do with the code, that handles the "GRAB NOW" onClick event, is to extract a code that does chrome.scripting to a separate function:


const grabBtn = document.getElementById("grabBtn");
grabBtn.addEventListener("click",() => {    
    // Get active browser tab
    chrome.tabs.query({active: true}, function(tabs) {
        var tab = tabs[0];
        if (tab) {
            execScript(tab);
        } else {
            alert("There are no active tabs")
        }
    })
})

/**
 * Function executes a grabImages() function on a web page,
 * opened on specified tab
 * @param tab - A tab to execute script on
 */
function execScript(tab) {
    // Execute a function on a page of the current browser tab
    // and process the result of execution
    chrome.scripting.executeScript(
        {
            target:{tabId: tab.id, allFrames: true},
            func:grabImages
        },
        onResult
    )
}


And the final content of popup.js is following:


const grabBtn = document.getElementById("grabBtn");
grabBtn.addEventListener("click",() => {    
    // Get active browser tab
    chrome.tabs.query({active: true}, function(tabs) {
        var tab = tabs[0];
        if (tab) {
            execScript(tab);
        } else {
            alert("There are no active tabs")
        }
    })
})

/**
 * Execute a grabImages() function on a web page,
 * opened on specified tab and on all frames of this page
 * @param tab - A tab to execute script on
 */
function execScript(tab) {
    // Execute a function on a page of the current browser tab
    // and process the result of execution
    chrome.scripting.executeScript(
        {
            target:{tabId: tab.id, allFrames: true},
            func:grabImages
        },
        onResult
    )
}

/**
 * Executed on a remote browser page to grab all images
 * and return their URLs
 * 
 *  @return Array of image URLs
 */
function grabImages() {
    const images = document.querySelectorAll("img");
    return Array.from(images).map(image=>image.src);    
}

/**
 * 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 from specified page");
        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();
          });
}

Conclusion

After this is done, you can open any browser web page with images, click on Image Grabber extension to open its popup interface and then click the "GRAB NOW" button. Then, paste the clipboard content to any text editor. It should paste a list of absolute URLs of all images from that web page.


You can clone and use the full source code of this extension from my GitHub repository: https://github.com/AndreyGermanov/image_grabber. However, I would recommend creating this extension from scratch while reading this article.


This is only the first part of the tutorial, related to this extension. In a second part, I will use this list of image URLs to build an additional interface for this extension, that will allow downloading all or selected images from this list as a single ZIP archive. This is definitely more useful than just having a list of URLs in the clipboard. Also, I will show how to package the completed extension and upload it to the Chrome Web Store which will make it available for anyone.


Feel free to connect and follow me on social networks where I publish announcements about my 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!


Also Published Here