Canvas Bitmap Operations - bitblt in JavaScript
Written by Ian Elliot   
Thursday, 31 May 2018
Article Index
Canvas Bitmap Operations - bitblt in JavaScript
ImageData object
Security

 

Getting at the pixels

The drawImage method allows you to make the connection between a bitmap and the canvas object, but what about getting at the pixels of a bitmap?

You can do this quite easily with the help of the ImageData object.

You can create an ImageData object in three ways. The first two create ImageData objects from scratch with all pixels set to transparent black, i.e. R=0, G=0, B=0 and A=0.

The

createImageData(w,h)

method creates an ImageData object with the given width and height and

createImageData(IData)

method creates and ImageData object the same size as the ImageData object specified by IData.

The third method creates an ImageData object from the pixels in a specified area of a canvas object. The

getImageData(x,y,w.h)

method creates an ImageData object from the pixels in the rectangle  with top left-hand corner at x,y and width w and height h.

To manipulate the pixels in the ImageData object you make use of its data property which is a simple array of pixel values in the order RGBA for each pixel.

The first element of the array i.e.data[0] is the R value for the pixel in the top left hand corner. The pixels are stored in the array in row order with four elements to each pixel.  A few moment thought should convince you that the R value for the pixel at x,y in the rectangle of pixels is stored at

data[(x+y*w)*4]

We can use this to write a method that allows direct access to the color information for the pixel at x,y. We can then use these methods to manipulate the pixel data and then write the result to the canvas using the:

putImageData(ImageData,x,y);

method which renders the ImageData object with its top left hand corner at x,y. There is another more sophisticated putImageData method:

putImageData(ImageData,x,y,sx,sy,sw,sh)

which only transfers data from the ImageData object within the rectangle with top left-hand corner at sx,sy and width sw and height sh - however this method isn't implemented by all current browsers including Firefox 3.

This is more or less all we need to create and modify graphics working at the pixel level. Of course it would be a good idea to implement some slightly higher level methods and while this is easy it does raise the question of how best to package them to make them easy to use. One approach is to simply add them as ad-hoc methods to the ImageData object that you create.

For example:

var ImDat=ctx.createImageData(100,100);
ImDat.getPixel=function(x,y){
                var i=(x+y*this.width)*4;
                return {R:this.data[i],
                        G:this.data[i+1],
                        B:this.data[i+2],
                        A:this.data[i+3]

                       }
               }

this adds the getPixel method to the ImDat object which returns an object with the properties R, G, B and A for the pixel at x,y.

You can add a similar setPixel method:

ImDat.setPixel=function(x,y,c){
                var i=(x+y*this.width)*4;
                this.data[i]=c.R;
                this.data[i+1]=c.G;
                this.data[i+2]=c.B;
                this.data[i+3]=c.A;
              }

Which sets the pixel at x,y to the color specified by the RGBA properties of the c object.  Of course in a production system you would need to add checks that the parameters were of the correct type and that the values were all in the range 0 to 255. You could also write other methods to work with color defined in other ways - CSS colours, color in the range 0 to 1 and so on.

This approach has two main problems. The first is that you have to augment each instance of the ImageData object in the same way. You also have to update it with each of the new methods you want to add to it. One way of making this easier is to write an augmentation function:

function augmentImageData(o){
   o.getPixel=function(x,y){
      var i=(x+y*this.width)*4;
      return {R:this.data[i],
              G:this.data[i+1],
              B:this.data[i+2],
              A:this.data[i+3]

             }
   }
   o.setPixel=function(x,y,c){
      var i=(x+y*this.width)*4;
      this.data[i]=c.R;
      this.data[i+1]=c.G;
      this.data[i+2]=c.B;
      this.data[i+3]=c.A;
   }
}

This will add the two methods to any instance of the ImageData object.  (If you wanted to do the job properly you could even add a createAugmentedImageData method to the canvas object.)

So now we can simply write:

var ImDat=ctx.createImageData(100,100);
augmentImageData(ImDat);

and use the getPixel and setPixel methods on ImDat. For example following these two lines:

for(var x=0;x<100;x++){
  for (var y = 0; y < 100; y++) {
    ImDat.setPixel(x, x, {
                          R: 0,
                          G: 255,
                          B: 0,
                          A: 255});
  }
}

sets every pixel to green. To see this we can put the ImageData object to the canvas:

ctx.putImageData(ImDat,0,0);

Of course this is a complex way of drawing a green square and the canvas already has a perfectly easy to use way of doing the same job in the form of fillRectangle.

In general there is never much point in using an uninitialised ImageData object to draw regular shapes that could just as easily be drawn using the standard canvas methods. However that are some things are are easier to do directly in terms of pixels.

For example, to create a completely random background:

var ImDat=ctx.createImageData(300,300);
augmentImageData(ImDat);

for(var x=0;x<300;x++){
 for (var y = 0; y < 300; y++) {
  ImDat.setPixel(x, y, {
                  R: Math.floor(Math.random()*256),
                  G: Math.floor(Math.random()*256),
                  B: Math.floor(Math.random()*256),
                  A: 255});
  }
}

ctx.putImageData(ImDat,0,0);

 

random

 

A special effects filter

As an example of loading and modifying an existing image let's implement a simple "embossed" effect filter.

First we create a canvas object using the helper function introduced in the previous chapter:

function createCanvas(h, w) {
 var c = document.createElement("canvas");
 c.width = w;
 c.height = h;
 return c;
}

function draw(){
  var ctx =document.body.appendChild(
  createCanvas(400,400)).getContext("2d");

Now we have a canvas ready to use.

Next we load the image file to be processed:

var img=new Image();

and write the onload event handler which draws the image to the canvas:

img.onload = function(){
               ctx.drawImage(img,0,0,400,300);

Getting the ImageData is just a repeat of what we did in the previous example:

var ImDat=ctx.getImageData(0,0,400,300);
augmentImageData(ImDat);

At this point we can now process the image data using the augmented methods:

for(var x=0;x<400;x++){
  for (var y = 0; y < 300; y++) {
   var c1=ImDat.getPixel(x,y);
   var c2=ImDat.getPixel(x,y+3);
   var r=Math.abs(c1.R-c2.R)+128;
   var g=Math.abs(c1.G-c2.G)+128;
   var b=Math.abs(c1.B-c2.B)+128;
   var grey=(r+g+b)/3;
   ImDat.setPixel(x,y,
           {R:grey,G:grey,B:grey,A:c1.A});
  }
}

The two for loops simply scan through every pixel in the image and computes the difference between the pixel's color value and the color value of the pixel three to the right. The 128 is added to make the difference have an average of 128 rather than 0 and then the color values are converted to a grey level i.e. the average of the three color values. Finally the new pixel data is stored back in the ImageData object without changing the A value.

When the for loops come to an end all that remains is to put the pixel data back in the canvas or into a different canvas if you want to display both the input and the result:

 

  ctx.putImageData(ImDat,0,0);
 };

 img.src = "test.jpg";
}



Last Updated ( Thursday, 06 September 2018 )