How to Take Screenshots in Chrome Extension
Tags: javascript, webdev, tutorial, chrome
Published: 2021-06-07 16:04:35

_This article was originally published on my [personal blog](https://blog.shahednasser.com/how-to-take-screenshots-in-chrome-extension/)_

In this tutorial, we'll cover how to take a screenshot in a Chrome extension and save it on the user's machine. This tutorial requires some beginner skills in Javascript.

We'll create an extension that allows the user to take a screenshot just by clicking the icon of the toolbar. The user can choose to take a screenshot of the entire screen, just a window, or the current tab.

Note that this extension will be using Manifest V3\. I'll provide some hints about the differences between V3 and V2 throughout the tutorial, but if you want to know more about the differences between the two versions you can check out [this tutorial](https://blog.shahednasser.com/chrome-extension-tutorial-migrating-to-manifest-v3-from-v2/).

You can find the code for this tutorial on [this GitHub Repository](https://github.com/shahednasser/chrome-screenshot-tutorial).

---

## Creating The Extension

We will not get into details on how to create a Chrome Extension, as it is not the purpose. If you need to learn more details about it you can check out [this tutorial](https://blog.shahednasser.com/chrome-extension-tutorial-replace-images-in-any-website-with-pikachu/).

Create `manifest.json` in the root of your extension directory with the following content:

```json
{
    "name": "Screenshots",
    "version": "0.0.1",
    "description": "Take screenshots",
    "manifest_version": 3,
    "action": {
        "default_title": "Take a Screenshot"
    },
    "icons": {
        "16": "/assets/icon-16.png",
        "32": "/assets/icon-32.png",
        "48": "/assets/icon-48.png",
        "128": "/assets/icon-128.png"
    }
}
```

The icons we are using for this extension are by [BZZRICON Studio](https://iconscout.com/contributors/bzzricon) on [Iconscout](https://iconscout.com/).

For Manifest V2, make sure the `manifest_version` is set to 2:

```json
"manifest_version": 2
```

and make sure to replace`action`with`browser_action`:

```json
"browser_action": {
   "default_title": "Take a Screenshot"
}
```

Then, create a zip of, go to [chrome://extensions](chrome://extensions), enable Developer Mode from the top right if it isn't enabled, click "Load Unpacked" from the buttons on the left, and choose the directory of the extension. Our extension will be added successfully.

## Add Service Worker (or Background Script)

To detect when a user clicks on the extension's icon, we need to attach an event listener to `chrome.action.onClicked`. To do that, we need to add a service worker (or background script for V2).

To add a service worker, add the following in `manifest.json`:

```json
"background": {
	"service_worker": "background.js"
},
```

Or the following for V2:

```json
"background": {
	"scripts": ["background.js"],
	"persistent": false
},
```

Next, create `background.js` in the root of the extension with the following content:

```js
chrome.action.onClicked.addListener(function (tab) {
    
})
```

for V2 it should be the following:

```js
chrome.browserAction.onClicked.addListener(function (tab) {
    
})
```

Note that if you don't have the `action` key in `manifest.json`, you will not be able to add a listener to `onClicked`.

Next, we'll start the "take screenshot" process. To do that, we'll use the [Desktop Capture API](https://developer.chrome.com/docs/extensions/reference/desktopCapture/). In particular, we'll use the method `chrome.desktopCapture.chooseDesktopMedia` which takes 3 parameters: The first is an array of strings of capture sources, which can be "screen", "window", "tab", and "audio". The second parameter is the target tab which is optional, however, in some cases if the target tab is not passed Chrome crashes. The third parameter is a callback that returns the stream id which we'll use later to get a screenshot.

add the following inside the listener:

```js
chrome.desktopCapture.chooseDesktopMedia([
        "screen",
        "window",
        "tab"
    ], tab, (streamId) => {
        //check whether the user canceled the request or not
        if (streamId && streamId.length) {
            
        }
    })
```

Notice that we're passing in the first parameter "screen", "window", and "tab" as the allowed source types. The second parameter is the `tab` parameter passed to the listener, and the third is the callback function. We're checking if `streamId` is not empty since it will be `empty` if the user cancels the request.

Before we can use this though, we need to add some permissions in the `manifest.json`. Permissions allow the user to understand what the extension is doing and agree to it before it is installed in their browser.

Add the following to `manifest.json`:

```json
"permissions": [
    "desktopCapture",
    "tabs"
],
```

The reason we also need the `tabs` permission is because if we don't have the permission, the `tab` object passed to the `onClicked` event listener will not have the `url` parameter which is required for `chooseDesktopMedia` when passing that tab as a parameter.

So, if you reload the extension now and press the icon, you'll see that it will ask you what screen do you want to record and that's it. Next, we need to use the `streamId` to get the screenshot.

## Add Content Script

To obtain the stream from the `streamId`, we need to use [getUserMedia](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia). However, this is not available in the service worker. So, we need to create a content script that receives a message from the service worker with the stream id, then gets the screenshot from the stream.

To add a content script, add the following to `manifest.json`:

```json
"content_scripts": [
    {
    	"matches": ["<all_urls>"],
    	"js": ["content_script.js"]
    }
]
```

Then, create `content_script.js` in the root of the extension with the following content:

```js
chrome.runtime.onMessage.addListener((message, sender, senderResponse) => {
    if (message.name === 'stream' && message.streamId) {
        
    }
});
```

This code listens for the "onMessage" event and checks if the `message` received has a `name` property that's equal to `stream` and has a `streamId` property, then we'll obtain the stream and take a screenshot of it.

Inside the if, we'll use `getUserMedia` which returns a Promise that resolves to a [MediaStream](/p/f406e09d-a9cc-48a6-94b2-5ab3b4495ccd/MediaStream):

```js
let track, canvas
navigator.mediaDevices.getUserMedia({
    video: {
        mandatory: {
            chromeMediaSource: 'desktop',
            chromeMediaSourceId: message.streamId
        },
    }
}).then((stream) => {
    
})
```

Notice that the parameter we passed to `getUserMedia` takes an object of options. We're passing the `chromeMediaSource` which equals to `desktop`, and `chromeMediaSourceId` which equals to the stream Id we received.

Next, inside the callback function for the resolved promise, we'll get the [MediaStreamTrack](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack) and then capture a screenshot from it using the [ImageCapture](https://developer.mozilla.org/en-US/docs/Web/API/ImageCapture) API:

```js
track = stream.getVideoTracks()[0]
const imageCapture = new ImageCapture(track)
return imageCapture.grabFrame()
```

In the end, we're returning the value of `imageCapture.grabFrame` which returns a Promise that resolves to an ImageBitmap. Notice that we didn't use the `takePhoto` method of the `ImageCapture` API. The reason behind that is that there are known cases of a DOMException thrown using it and this is a [workaround](https://github.com/GoogleChromeLabs/imagecapture-polyfill/issues/15) for it.

Next, we'll attach another `then` method to handle the returned Promise from `imageCapture.grabFrame`. The callback function will stop the stream, create a canvas and draw the ImageBitmap in it, then get the Data Url of the canvas:

```js
.then((bitmap) => {
    track.stop();
    canvas = document.createElement('canvas');
    canvas.width = bitmap.width;
    canvas.height = bitmap.height;
    let context = canvas.getContext('2d');
    context.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height);
    return canvas.toDataURL();
})
```

Notice that it's important to set the width and height of the canvas to be equal to that of the `bitmap`. If we don't, the canvas height and width will default to `200px` and if the width or height of the bitmap is larger than that then the screenshot will be cropped.

In the end, we're returning `canvas.toDataUrl`. We'll attach a last `then` method that takes the URL returned as a parameter. This URL will be used to download the image on the user's device:

```js
.then((url) => {
    //TODO download the image from the URL
}).catch((err) => {
    alert("Could not take screenshot")
    senderResponse({success: false, message: err})
})
```

Notice that we also added `catch` to catch any errors. As you can see in the `catch` callback, we're calling the function `senderResponse`. This function is the one we'll pass from the service worker or background script to the content script when sending the message.

At the end of the `if` block we'll add the following:

```js
return true;
```

In an `onMessage` event listener if the listener returns true that means that we'll later return a response to the sender using the callback function they passed when sending the message.

## Download Screenshot

To download the screenshot, we'll use the [Downloads](https://developer.chrome.com/docs/extensions/reference/downloads) API. It provides a lot of methods to manage downloads like search, open, remove, and more.

Before we can use any of the methods, we need to add the `downloads` permission to the `permissions` array in `manifest.json`:

```json
"permissions": [
    "desktopCapture",
    "tabs",
    "downloads"
],
```

Now, we can use the methods of the Downloads API. We'll use the method `chrome.downloads.download` which takes an array of options as the first parameter and a callback function as the second.

However, this method cannot be called from the content script. We need to call it from the service worker/background script. So, when we get to the `TODO` part in our code earlier, we need to send a message to the service worker with the URL we want to download.

To send a message in an extension, we use the `chrome.runtime.sendMessage` which takes as a first parameter the message to send (which can be of any type), and an optional callback function as the second parameter, which is the function that the receiver of the message should call to deliver the response.

Add the following code in place of the `TODO` comment:

```js
.then((url) => {
    chrome.runtime.sendMessage({name: 'download', url}, (response) => {
        if (response.success) {
            alert("Screenshot saved");
        } else {
            alert("Could not save screenshot")
        }
        canvas.remove()
        senderResponse({success: true})
    })
})
```

Notice that we're sending the message `{name: 'download', url}` to the receiver. As the message is sent to every listener in the extension, it's good to include a message property in the message you're sending to be able to handle different messages. We're also sending the URL to download the image from.

Let's go back to our service worker now. First, let's send a message to the content script from `chooseDesktopMedia` callback function we earlier did:

```js
//check whether the user canceled the request or not
if (streamId && streamId.length) {
    setTimeout(() => {
        chrome.tabs.sendMessage(tab.id, {name: "stream", streamId}, (response) => console.log(response))
    }, 200)
}
```

Notice that to send a message to the content script we're using `chrome.tabs.sendMessage`. The difference between this one and `chrome.runtime.sendMessage` is that the former one sends the message to content scripts in a specific tab, whereas the first one sends the message to all scripts in the extension that listen to the `onMessage` handler.

Next, we'll add a listener to the `onMessage` event to receive the `download` message and download the file to the user's machine:

```js
chrome.runtime.onMessage.addListener((message, sender, senderResponse) => {
    if (message.name === 'download' && message.url) {
        chrome.downloads.download({
            filename: 'screenshot.png',
            url: message.url
        }, (downloadId) => {
            senderResponse({success: true})
        })

        return true;
    }
})
```

First, we're checking if the `name` property of the message is equal to `download` to make sure that the message received is the correct one. Then, we're downloading the file using `chrome.downloads.download`, passing it the options object that has two options here: `filename` which is the name of the file to download, and `url` which is the URL to download. In the callback of the `downloads` method we're calling the callback function passed by the sender.

Our extension is now ready. Go to [chrome://extensions](chrome://extensions)again and reload the extension. Then, go to any page, click the icon of the extension. You'll be prompted to choose either the entire screen, a window, or a tab. Once you choose, a screenshot will be taken and saved on your machine.

---

## Conclusion

In this tutorial, we learned how to a screenshot and a few of the concepts of a chrome extension shortly. If you want to learn more about Chrome extensions, make sure to check out the rest of my tutorials about [browser extensions](https://blog.shahednasser.com/tag/browser-extensions).

_If you would like to connect and talk more about this article or programming in general, you can find me on my Twitter account [@shahednasserr](https://twitter.com/shahednasserr)_