Image "Grayscaler" Chrome Extension

This codelab will walk you through the steps of building a Google Chrome Extension which will let users pick an image from the web and convert it to grayscale.

We’ll be using the following extension APIs:

We’ll be using some of these new HTML features:

You may install the completed extension from the Google Chrome Extensions gallery here.

Step 1: Requirements

To write an extension, make sure you have the following installed:

You do not need to know how to write an extension before completing this codelab.

Step 2: Project Setup

Create a folder somewhere on your computer to contain your extension’s code. Inside your extension’s folder, create a text file called manifest.json, with the following contents:

{
  "name": "Image 'Grayscaler' Extension",
  "version": "1.0.0",
  "description": "Select images with the context menu and convert to grayscale.",
  "icons": {
    "128" : "sample-128.png"
  }
}

Note that there is an icon file referenced in the manifest file. Feel free to make your own, but here’s one that has already been made:

sample-128.png
128 x 128 px

Put the icon in the same directory as the manifest.json file.

When you’re done, go to chrome://extensions in Google Chrome and click developer mode. Then click load unpacked extension and select your directory. Your first extension is done!

Step 3: Add a Background Page

The background page of an extension is loaded once and runs for as long as the extension stays active. Most of the logic of your extension will be here.

Add the following to your manifest:

{
  "name": "Image 'Grayscaler' Extension",
  "version": "1.0.1",
  "description": "Select images with the context menu and convert to grayscale.",
  "icons": {
    "128" : "sample-128.png"
  },
  "background_page": "background.html"
}

Create a new file named background.html with the following code:

<!DOCTYPE html>
<html>
  <head>
  </head>
  <body>
    <script>
      console.log('Hello, world');
    </script>
  </body>
</html>

Reload your extension on the chrome://extensions page and you will see an link for background.html listed. Clicking the link will open the inspector for the background page, and you will see your output in the console.

Step 4: Add a Context Menu

In order to modify images on web pages, the user will need a mechanism to select an image. Luckily, the context menus API will give us a quick way to access the URL of an image on the page.

Start by modifying your background page to register a context menu item:

<!DOCTYPE html>
<html>
  <head>
    <script>
      function onImageClick(data, tab) {
        console.log(data);
      };

      chrome.contextMenus.create({
        "title" : "Modify this image",
        "type" : "normal",
        "contexts" : ["image"],
        "onclick" : onImageClick
      });
    </script>
  </head>
  <body>
  </body>
</html>

The context menus API requires some permissions. Add the following to your manifest:

{
  "name": "Image 'Grayscaler' Extension",
  "version": "1.0.1",
  "description": "Select images with the context menu and convert to grayscale.",
  "icons": {
    "128" : "sample-128.png"
  },
  "background_page": "background.html",
  "permissions" : [ 
    "contextMenus",
    "http://*/*"
  ]
}

Now if you reload the extension and right click on an image, you should see a context menu item.

If you click on the menu item, you should see an object containing contextual information about the image in the background page’s console.

Step 5: Open a Popup

The user interface for this extension is a popup window. Modify your onImageClick function to open a new window when the context menu is activated:

<!DOCTYPE html>
<html>
  <head>
    <script>
      function onImageClick(data, tab) {
        var winData = {
          'url': 'popup.html#' + encodeURIComponent(data.srcUrl),
          'width': 300,
          'height': 300
        };
        chrome.windows.create(winData);
      };

      chrome.contextMenus.create({
        "title" : "Modify this image",
        "type" : "normal",
        "contexts" : ["image"],
        "onclick" : onImageClick
      });
    </script>
  </head>
  <body>
  </body>
</html>

Note that we are passing the src URL of the image to the popup page via the location.hash parameter. This is a simple way to pass information from the background page to a new window without needing to use the messaging API.

Opening a new window requires the tabs permission, so add that to your manifest:

{
  "name": "Image 'Grayscaler' Extension",
  "version": "1.0.1",
  "description": "Select images with the context menu and convert to grayscale.",
  "icons": {
    "128" : "sample-128.png"
  },
  "background_page": "background.html",
  "permissions" : [ 
    "tabs",
    "contextMenus",
    "http://*/*"
  ]
}

Finally, create a file named popup.html so that we have something to show:

<!DOCTYPE html>
<html>
  <head>
  </head>
  <body>
    <h1>Hello!</h1>
  </body>
</html>

Step 6: Load the Image

The popup window will need to load the image into an <img> element before we can draw it into a canvas. Change your popup.html:

<!DOCTYPE html>
<html>
  <head>
    <style>
      html, body {
        padding: 0;
        margin: 0;
      }
    </style>
    <script>
      function setWindowSize() {
        chrome.windows.getCurrent(function(win) {
          var updateData = {
            'width': document.body.scrollWidth,
            'height': document.body.offsetHeight + 100
          };
          chrome.windows.update(win.id, updateData);
        });
      };

      function onImageLoaded(evt) {
        document.body.appendChild(this);
        window.setTimeout(setWindowSize, 100);
      };

      function onLoad(evt) {
        var imgUrl = decodeURIComponent(window.location.hash.substring(1));
        var img = document.createElement('image');
        img.addEventListener('load', onImageLoaded, false);
        img.src = imgUrl;
      };

      window.addEventListener('load', onLoad, false);
    </script>
  </head>
  <body>
  </body>
</html>

Now when the popup is opened, you should see the image in the popup body.

Step 7: Modify the Image

Once the image is loaded in an <img> tag, it can be drawn into a <canvas>. We’ll be using two canvases so we can keep the original image pixels in memory while modifying them for output. The following changes to the code implement a function which renders the image in grayscale.

First, add the canvas elements:

<body>
  <canvas id="buffer" style="display: none"></canvas>
  <canvas id="output"></canvas>
</body>

Then the paintGrayscale function:

function paintGrayscale() {
  var buffer = document.querySelector('#buffer');
  var bufferContext = buffer.getContext('2d');
  
  var output = document.querySelector('#output');
  var outputContext = output.getContext('2d');
  output.width = buffer.width;
  output.height = buffer.height;
  
  var imagedata = bufferContext.getImageData(0, 0, 
                                             buffer.width, buffer.height);
      
  for (var i = 0; i < imagedata.data.length; i += 4) {
    var r = imagedata.data[i];
    var g = imagedata.data[i + 1];
    var b = imagedata.data[i + 2];
    
    var gray = (0.3 * r) + (0.59 * g) + (0.11 * b);
    gray = Math.max(Math.min(gray, 255), 0);
    imagedata.data[i] = gray;
    imagedata.data[i + 1] = gray;
    imagedata.data[i + 2] = gray;
  }
  outputContext.putImageData(imagedata, 0, 0);
};

function onImageLoaded(evt) {
  var canvas = document.querySelector('#buffer');
  canvas.width = this.width;
  canvas.height = this.height;
  
  var context = canvas.getContext('2d');
  context.drawImage(this, 0, 0);
  
  paintGrayscale();
  
  window.setTimeout(setWindowSize, 10);
};

Now when you activate the popup (or reload it) the image will be grayscale.

Step 8: Add a Slider

We’ll add some customizability to the extension, through some sliders. For this, the new &lt;input type="range" /&gt; element will be useful:

<body>
  <canvas id="buffer" style="display: none"></canvas>
  <canvas id="output"></canvas>
  <br />Posterization: <input type="range" id="inputPoster" min="1" max="255" value="1"/>
  <br />Threshold: <input type="range" id="inputThresh" min="1" max="20" value="10"/>
  
</body>

We’ll parse the values of these sliders in the paintGrayscale function, and then use them to modify the “gray” value by adding the following code:

function paintGrayscale() {
  var poster = parseInt(document.querySelector('#inputPoster').value);
  var thresh = parseInt(document.querySelector('#inputThresh').value);
  
  
  var buffer = document.querySelector('#buffer');
  var bufferContext = buffer.getContext('2d');
  
  var output = document.querySelector('#output');
  var outputContext = output.getContext('2d');
  output.width = buffer.width;
  output.height = buffer.height;
  
  var imagedata = bufferContext.getImageData(0, 0, 
                                             buffer.width, buffer.height);
      
  for (var i = 0; i < imagedata.data.length; i += 4) {
    var r = imagedata.data[i];
    var g = imagedata.data[i + 1];
    var b = imagedata.data[i + 2];
    
    var gray = (0.3 * r) + (0.59 * g) + (0.11 * b);
    gray = gray * (thresh / 10.0);
    gray = Math.round(gray / poster) * poster;
    
    gray = Math.max(Math.min(gray, 255), 0);
    imagedata.data[i] = gray;
    imagedata.data[i + 1] = gray;
    imagedata.data[i + 2] = gray;
  }
  outputContext.putImageData(imagedata, 0, 0);
};

Next, listen for changes to the sliders, and call paintGrayscale when the user manipulates the control:

function onChange(evt) {
  paintGrayscale();
};
  
function onLoad(evt) {
  var imgUrl = decodeURIComponent(window.location.hash.substring(1));
  
  var img = document.createElement('image');
  img.addEventListener('load', onImageLoaded, false);
  img.src = imgUrl;
  
  var inputs = document.querySelectorAll('input[type="range"]');
  Array.prototype.forEach.call(inputs, function(input) {
    input.addEventListener('change', onChange, false);
  });
};

Now you’re able to offer some customization to your extension!

Suggested Improvements

Final Source Code

Here is the source for the final version of the extension: