Building a Browser-Based Passport Photo Maker
Passport photos have strict government specifications — exact dimensions, specific background colors, and precise head positioning. Here's how to build a tool that handles all of this entirely in the browser.
The Challenge
Each country has different requirements:
| Country | Size (mm) | Size (px at 300 DPI) | Background |
|---|---|---|---|
| US | 51×51 | 600×600 | White |
| UK/EU | 35×45 | 413×531 | Light gray |
| India | 35×45 | 413×531 | White |
| Canada | 50×70 | 591×827 | White |
| China | 33×48 | 390×567 | White |
| Japan | 35×45 | 413×531 | White |
| Australia | 35×45 | 413×531 | White |
| France | 35×45 | 413×531 | Light gray |
| Germany | 35×45 | 413×531 | Light gray |
| UAE | 43×55 | 508×650 | White |
Loading and Displaying the Image
First, load the user's image and display it with an interactive crop overlay:
js
function loadImage(file) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(img);
img.src = URL.createObjectURL(file);
});
}
Interactive Crop Window
The crop window is an absolutely positioned div overlaid on the image. Users can drag it to reposition and resize using corner handles:
class CropWindow {
constructor(container, image, preset) {
this.container = container;
this.image = image;
this.preset = preset;
this.x = 0;
this.y = 0;
this.w = 200;
this.h = 260;
this.initHandles();
this.initKeyboard();
}
initHandles() {
// 4 corner handles for resize
const handles = ['nw', 'ne', 'sw', 'se'];
handles.forEach(pos => {
const handle = document.createElement('div');
handle.className = `crop-handle ${pos}`;
handle.addEventListener('pointerdown', (e) => this.startResize(e, pos));
this.container.appendChild(handle);
});
}
initKeyboard() {
document.addEventListener('keydown', (e) => {
const step = e.shiftKey ? 10 : 2;
switch(e.key) {
case 'ArrowUp': this.y = Math.max(0, this.y - step); break;
case 'ArrowDown': this.y = Math.min(this.maxY, this.y + step); break;
case 'ArrowLeft': this.x = Math.max(0, this.x - step); break;
case 'ArrowRight': this.x = Math.min(this.maxX, this.x + step); break;
}
this.render();
});
}
}
Background Replacement
To change the background color, we scan edge pixels and replace similar colors:
function replaceBackground(canvas, targetColor, tolerance = 30) {
const ctx = canvas.getContext('2d');
const data = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = data.data;
// Sample background color from corners
const bg = sampleCornerPixels(pixels, canvas.width, canvas.height);
for (let i = 0; i < pixels.length; i += 4) {
const diff = colorDistance(pixels, bg, i);
if (diff < tolerance) {
pixels[i] = targetColor.r; // R
pixels[i + 1] = targetColor.g; // G
pixels[i + 2] = targetColor.b; // B
}
}
ctx.putImageData(data, 0, 0);
}
Print Layout Generation
For users who want to print at a photo lab, we need to arrange multiple copies on a 4×6 inch (1024×1536 px at 300 DPI) print sheet:
function generatePrintLayout(singlePhoto, preset, sheetWidth = 1500, sheetHeight = 1000) {
const canvas = document.createElement('canvas');
canvas.width = sheetWidth;
canvas.height = sheetHeight;
const ctx = canvas.getContext('2d');
// Fill with white background
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, sheetWidth, sheetHeight);
// Calculate how many photos fit
const cols = Math.floor(sheetWidth / (preset.outW + 20));
const rows = Math.floor(sheetHeight / (preset.outH + 20));
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const x = col * (preset.outW + 20) + 10;
const y = row * (preset.outH + 20) + 10;
ctx.drawImage(singlePhoto, x, y, preset.outW, preset.outH);
}
}
return canvas;
}
Full Tool
The ToolBox Image Passport Photo Maker (https://toolboximage.com/tools/passport/) implements all of this with 10 country presets, 4 background colors, arrow key fine positioning, and both single photo + print layout downloads. All client-side, zero uploads.
Try it at toolboximage.com/tools/passport/ (https://toolboximage.com/tools/passport/)
Top comments (2)
The country preset table is genuinely useful, half the pain of passport photos is nobody knowing the exact spec, so baking those in is the real feature. Where I'd expect the background swap to struggle is any hair or edge that's close to the background color, since a flat corner-sample plus a distance threshold will either leave a halo or start eating into the person. If you ever want to push it further, running the color check in a space like LAB instead of raw RGB matches how people actually see "close enough" colors and tends to cut those halos a lot. For a simple white-wall photo though, the corner-sampling trick is exactly the right amount of clever.
This is really cool! I'm curious how you