← 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) │
└───────────┘ └──────────┘ └───────────┘- Load image into Canvas
- Get pixel data as
Uint8Array(RGBA format) - Pass to Rust — mutate in-place
- 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