Canvas 2D Context

Part 7. Image Data

Image is only pixel deep
Rewritten proverb

Sampled Images

CanvasRenderingContext2D and ImageData APIs expose a number of methods and properties enabling the developer to handle bitmap data directly. Low-level pixels manipulation is based on the concept of the sampled image - a one-dimensional array of pixel-related data. A pixel is defined as a single grid point consisting of a set of samples (in other terminology - color components). There are four samples per pixel, and each sample is encoded as an 8-bit value. Samples hold information about red, green, blue and alpha characteristics of a pixel. Below is a sequence of three pixels. Any pixel is represented by four samples.

R

G

B

A

R

G

B

A

R

G

B

A

A red, green, or blue sample holds a value in the range 0..255. Using total 24 bits per pixel enables the reproduction of 16777216 colors. Transparency level is defined by the A (alpha) value which is in the same range (0..255). Canvas implementations presume the use of 8-bit unsigned integers to represent the raw image data. All samples constitute an instance of Uint8ClampedArray. There are three main operations with the image data and the underlying array:

  • getting a "snapshot" of the canvas or its fragment and using the returned object which is an instance of the ImageData interface;
  • creating a block of the image data by specifying red, green, blue and alpha channels for the image as well as its dimensions;
  • putting the created or previously obtained image data onto the canvas plane.

Getting Image Data

To get an instance of the ImageData, the getImageData(sx, sy, sw, sh) method is used. The methods returns a rectangular grid of pixels with the following characteristics: width (the number of columns in the pixels grid), height (the number of rows in the grid) and data (Uint8ClampedArray object holding values for samples). For demonstration purposes, the code below uses an opaque rectangle filled with the red color. The width and height of the obtained image is 100, so there are 10000 pixels in the image (100 * 100). Each sample is an intersection of a pixel and color or alpha channel, so 10000 pixels presume the existence of 40000 samples (10000 pixels * 4 samples for each pixel). In each 4 successive samples the first three are colors, and the fourth is alpha. Samples are interleaved pixel by pixel: samples 0..3 describe the first pixel, samples 4..7 - the second one, etc. This order is reflected in the Uint8ClampedArray instance:

context.fillStyle="red"; // red is 255, green and blue are 0, and alpha is 1.0
context.fillRect(10, 10, 100, 100); // opaque red rectangle is filled

var imageData=context.getImageData(10, 10, 100, 100); // "snapshot" of the same area is taken

console.log("width:" + imageData.width); // returns 100
console.log("height:" + imageData.height); // returns 100

var samplesData=imageData.data; // Uint8ClampedArray instance
console.log("number of image samples: "+samplesData.length); // returns 40000

console.log("first pixel characteristics");
console.log("red: "+samplesData[0]); // red sample; returns 255
console.log("green: "+samplesData[1]); // green sample; returns 0
console.log("blue: "+samplesData[2]); // blue sample; returns 0
console.log("alpha: "+samplesData[3]); // alpha value; returns 255 (fully opaque)

console.log("second pixel characteristics"); // the same characteristics for the second pixel
console.log("red: "+samplesData[4]); // 255
console.log("green: "+samplesData[5]); // 0
console.log("blue: "+samplesData[6]); // 0
console.log("alpha: "+samplesData[7]); // 255

console.log("last pixel characteristics"); // the same characteristics for the last pixel
console.log("red: "+samplesData[39996]); // 255
console.log("green: "+samplesData[39997]); // 0
console.log("blue: "+samplesData[39998]); // 0
console.log("alpha: "+samplesData[39999]); // 255

Creating Image Data

Now that we know how to address any sample in the image data we can create images programmatically by using the createImageData(sw, sh) method and filling the Uint8ClampedArray instance with new values. The code below builds the same red rectangle "from scratch":

var imageData=context.createImageData(100, 100); // width=100, height=100
var samplesData=imageData.data; // Uint8ClampedArray instance
console.log("number of image samples: "+samplesData.length); // returns 40000

console.log("first pixel characteristics"); // so far all pixels are transparent black
console.log("red: "+samplesData[0]); // red sample; returns 0
console.log("green: "+samplesData[1]); // green sample; 0
console.log("blue: "+samplesData[2]); // blue sample; 0
console.log("alpha: "+samplesData[3]); // alpha value; 0: image is transparent

// filling the array with the new values for samples
var counter;
for(counter=0;counter<samplesData.length;counter+=4) {
 samplesData[counter]=255; // setting red sample
 samplesData[counter+1]=0; // setting green sample
 samplesData[counter+2]=0; // setting blue sample
 samplesData[counter+3]=255; // setting alpha sample
}

// this time new values are logged out; each pixel is opaque red
console.log("first pixel characteristics");
console.log("red: "+samplesData[0]); // 255
console.log("green: "+samplesData[1]); // 0
console.log("blue: "+samplesData[2]); // 0
console.log("alpha: "+samplesData[3]); // 255

Putting Image Data

The final objective of image data manipulation is its rendering on the canvas plane. To produce a visible result of in-memory pixels operations the putImageData(imageData, dx, dy) method is used. The example below is the result of the following image data conversions:

  • getting image data and putting the obtained instance again on the canvas plane (two red rectangles);
  • creating image data programmatically and putting it onto the canvas (semi-transparent blue rectangle);
  • creating image data from the previously defined data (lime rectangle derived from the semi-transparent blue one);
  • creating the image data containing all the canvas plane and putting it back on the canvas with arguments changing its size and upper left point (a smaller copy of the canvas displaying four rectangles).

Let's examine each operation more attentively. The second red rectangle is painted as a result of putting the image data obtained from the first rectangle:

context.fillRect(10, 10, 100, 100);
var imageData=context.getImageData(10, 10, 100, 100);
context.putImageData(imageData, 130, 10);

The semi-transparent blue rectangle is the result of putting the image data created programmatically:

var newImageData=context.createImageData(100, 100);
var samplesData=newImageData.data; // all pixels are transparent black
var counter;
for(counter=0;counter<samplesData.length;counter+=4) {
 samplesData[counter]=0;
 samplesData[counter+1]=0;
 samplesData[counter+2]=255;
 samplesData[counter+3]=128;
}
context.putImageData(newImageData, 10, 130);

The green rectangle is based on the image data derived from already existing image data created above:

var derivedImageData=context.createImageData(newImageData);

An attempt to draw the derived image immediately will not make the image visible: the createImageData(imageData) method returns an ImageData instance with the dimensions inherited from the original image, but all the samples in the derived image are cleared to transparent black. We'll have to redefine samples to display the derived image:

var samples=derivedImageData.data;
var counter;
for(counter=0;counter<samples.length;counter+=4){
 samples[counter]=0;
 samples[counter+1]=255;
 samples[counter+2]=0;
 samples[counter+3]=255;
}
context.putImageData(derivedImageData, 130, 130); // lime opaque rectangle is displayed

The last operation creates a "snapshot" of the whole canvas. The image data is then placed at the point (10, 260). This time subsidiary arguments are utilized - dirtyX, dirtyY, dirtyWidth and dirtyHeight. As a result, the final image data is rendered as a smaller copy of the original canvas.

var totalCanvasData=context.getImageData(0, 0, 500, 500);
context.putImageData(totalCanvasData,10, 260, 20, 20, 200, 200);

Summary: Image Data

getting image data
context.getImageData(sx, sy, swidth, sheight);

creating image data
context.createImageData(swidth, sheight);
context.createImageData(imageData);


putting image data onto the canvas
context.putImageData(imageData, dx, dy);
context.putImageData(imageData, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight);


ImageData API members
imageData.width
imageData.height
imageData.data