ImageData

Reading and Writing Pixels with ImageData

getImageData(x, y, w, h) returns RGBA bytes, row by row; putImageData() writes them back, ignoring transform, alpha and compositing. Both use bitmap pixels. Reads are slow on a GPU canvas, so pass willReadFrequently: true to the first getContext() call (Baseline since September 2024).

Grayscale and inverted copies of the logo, computed byte by byteHTML
<canvas id="c" style="width:600px; height:62px"></canvas><script src="hidpi.js"></script>
<script>
  c.getContext('2d', { willReadFrequently: true });       // the first call fixes the options
  const ctx = hidpi(c), dpr = devicePixelRatio;
  const img = Object.assign(new Image(), { src: 'booknest-logo.png' });
  img.decode().then(() => {
    [0, 205, 410].forEach(x => ctx.drawImage(img, x, 0, 190, 60));
    const edit = (x, f) => {                               // f maps r, g, b to [r, g, b]
      const pixels = ctx.getImageData(x * dpr, 0, 190 * dpr, 60 * dpr), d = pixels.data;
      for (let i = 0; i < d.length; i += 4) d.set(f(d[i], d[i + 1], d[i + 2]), i);
      ctx.putImageData(pixels, x * dpr, 0);
    };
    edit(205, (r, g, b) => Array(3).fill(0.2126 * r + 0.7152 * g + 0.0722 * b));
    edit(410, (r, g, b) => [255 - r, 255 - g, 255 - b]);
    window.__done = true;
  });
</script>
Browser output of Listing 2.17
Browser output of 17

Luminance weights make green read lighter than blue, as the eye sees it. Drawing a cross-origin image without CORS taints the canvas, and reading it then throws a SecurityError: set img.crossOrigin = 'anonymous' and have the server send Access-Control-Allow-Origin.

getImageData and putImageData turn a cover grayscale and inverted, pixel by pixelHTMLLive
<!doctype html>
<style>
  body { margin: 0; padding: 8px; font: 13px system-ui, sans-serif; background: #fafaf7; }
  canvas { width: 100%; max-width: 600px; height: 190px; display: block; background: #fff; border: 1px solid #ddd; }
</style>
<canvas id="c"></canvas>
<script>
  const c = document.getElementById('c');
  // The first getContext() call fixes the options: ask for fast reads up front
  c.getContext('2d', { willReadFrequently: true });
  function hidpi(c, r = devicePixelRatio) {
    c.width = c.clientWidth * r;  c.height = c.clientHeight * r;
    const ctx = c.getContext('2d');  ctx.scale(r, r);  return ctx;
  }
  const ctx = hidpi(c), dpr = devicePixelRatio;

  // Draw one colourful cover
  function cover(x) {
    const g = ctx.createLinearGradient(x, 0, x + 180, 160);
    g.addColorStop(0, '#1f5f8b');  g.addColorStop(0.5, '#3f7d3a');  g.addColorStop(1, '#e09a10');
    ctx.fillStyle = g;  ctx.fillRect(x, 10, 180, 150);
    ctx.fillStyle = '#b5452f';  ctx.beginPath();  ctx.arc(x + 130, 60, 28, 0, 2 * Math.PI);  ctx.fill();
    ctx.fillStyle = '#fff3e0';  ctx.font = 'bold 20px Georgia';  ctx.fillText('The Quiet', x + 12, 120);
    ctx.fillText('Harbor', x + 12, 145);
  }
  [0, 205, 410].forEach(cover);

  // Read a region, map each pixel's r, g, b, write it back (bitmap pixels, not CSS pixels)
  function edit(x, f) {
    const pixels = ctx.getImageData(x * dpr, 10 * dpr, 180 * dpr, 150 * dpr), d = pixels.data;
    for (let i = 0; i < d.length; i += 4) d.set(f(d[i], d[i + 1], d[i + 2]), i);
    ctx.putImageData(pixels, x * dpr, 10 * dpr);
  }
  // Luminance weights: green reads lighter than blue, as the eye sees it
  edit(205, (r, g, b) => Array(3).fill(0.2126 * r + 0.7152 * g + 0.0722 * b));
  edit(410, (r, g, b) => [255 - r, 255 - g, 255 - b]);

  ctx.font = '12px sans-serif';  ctx.fillStyle = '#263238';
  ['original', 'grayscale (luminance)', 'inverted'].forEach((t, i) => ctx.fillText(t, i * 205, 178));
</script>