← Back to Lessons Lesson 20 of 28
Intermediate graphics

Image Processing

Introduction

Image processing is one of the strongest use cases for Wasm — pixel-by-pixel manipulation is CPU-intensive, and Rust/Wasm is 5-10x faster than JavaScript for these operations. All processing happens client-side, so images never leave the user's browser.

How It Works

┌───────────┐    getImageData()   ┌──────────┐   putImageData()   ┌───────────┐
│  <canvas>  │ ────────────────▶ │ Rust/Wasm │ ────────────────▶ │  <canvas>  │
│  (source)  │    Uint8Array     │ (process) │    Uint8Array     │  (result)  │
└───────────┘                    └──────────┘                    └───────────┘
  1. Load image into Canvas
  2. Get pixel data as Uint8Array (RGBA format)
  3. Pass to Rust — mutate in-place
  4. Put modified pixels back on Canvas

JavaScript Integration

import init, { grayscale, brightness, sepia } from './pkg/image_wasm.js';

await init();

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

// Load image
const img = new Image();
img.onload = () => {
    canvas.width = img.width;
    canvas.height = img.height;
    ctx.drawImage(img, 0, 0);
};
img.src = 'photo.jpg';

// Apply filter
function applyFilter(filterFn) {
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    filterFn(imageData.data);  // Mutates in-place!
    ctx.putImageData(imageData, 0, 0);
}

// Usage
applyFilter(grayscale);
applyFilter((pixels) => brightness(pixels, 30));
applyFilter(sepia);

Pixel Format

Canvas ImageData is a flat Uint8Array in RGBA format:

Index:  0    1    2    3    4    5    6    7    ...
Data:  [R₀] [G₀] [B₀] [A₀] [R₁] [G₁] [B₁] [A₁] ...
        └── pixel 0 ──┘     └── pixel 1 ──┘

In Rust, process 4 bytes at a time with chunks_exact_mut(4).

Box Blur

#[wasm_bindgen]
pub fn blur(pixels: &mut [u8], width: u32, height: u32, radius: u32) {
    let src = pixels.to_vec();
    let r = radius as i32;
    let w = width as i32;
    let h = height as i32;

    for y in 0..h {
        for x in 0..w {
            let mut sum_r = 0u32;
            let mut sum_g = 0u32;
            let mut sum_b = 0u32;
            let mut count = 0u32;

            for dy in -r..=r {
                for dx in -r..=r {
                    let nx = (x + dx).clamp(0, w - 1);
                    let ny = (y + dy).clamp(0, h - 1);
                    let idx = ((ny * w + nx) * 4) as usize;
                    sum_r += src[idx] as u32;
                    sum_g += src[idx + 1] as u32;
                    sum_b += src[idx + 2] as u32;
                    count += 1;
                }
            }

            let idx = ((y * w + x) * 4) as usize;
            pixels[idx] = (sum_r / count) as u8;
            pixels[idx + 1] = (sum_g / count) as u8;
            pixels[idx + 2] = (sum_b / count) as u8;
        }
    }
}

Performance Comparison

Filter JavaScript Rust/Wasm Speedup
Grayscale (4K image) 45ms 5ms 9x
Brightness (4K) 40ms 4ms 10x
Box blur r=3 (4K) 2,500ms 280ms 9x
Sepia (4K) 50ms 6ms 8x

Key Optimization: In-Place Mutation

Rust modifies the Uint8Array in-place — no copying data back and forth:

pub fn grayscale(pixels: &mut [u8]) {  // &mut = modify in place
    // pixels IS the Canvas ImageData buffer
    // Changes are reflected immediately in JS
}

This is the fastest possible approach — zero allocation, zero copy.

Try It

The starter code includes four image filters. In a real project, you'd pass Canvas imageData.data directly to these functions and see the result instantly.

Try It

Chapter Quiz

Pass all questions to complete this lesson