Ask five countries what a passport photo should be and you get five answers that look identical until you read the fine print. 35×45 mm. 2×2 inches. 33×48 mm. Head between 50% and 69% of the height. Background plain white, or off-white, but not cream.
The instinct is to build a form per country. That is the wrong shape. Every one of those rules reduces to a row in a table.
01The table
{ id:'us', label:'US passport', w:600, h:600 }, // 2×2 in @ 300dpi
{ id:'sc', label:'Schengen visa', w:413, h:531 }, // 35×45 mm @ 300dpi
{ id:'cn', label:'China visa', w:390, h:567 }, // 33×48 mm @ 300dpi
{ id:'ca', label:'Canada', w:591, h:827 } // 50×70 mm @ 300dpi
Width, height, and a label. That is the whole domain model. The UI reads it, the cropper reads it, the export reads it. Adding a country is one line, not a new component.
02The number people get wrong
A user uploads a 1200×1600 photo and asks whether it is big enough. The honest answer is: big enough for what?
What matters is not the source size, it is the size of the region left after you crop to the target ratio. Get that wrong and you tell people their photo is fine when it is 30% short.
var ratio = spec.w / spec.h;
var usableW, usableH;
if (W / H > ratio) { // source too wide — crop the sides
usableH = H;
usableW = Math.round(H * ratio);
} else { // source too tall — crop top and bottom
usableW = W;
usableH = Math.round(W / ratio);
}
var ok = usableW >= spec.w && usableH >= spec.h;
For a 1200×1600 source and a 413×531 target, the usable region is 1200 × 1543 — comfortably enough. For a 500×700 source it is 500 × 643 — also fine on width but only just. Two numbers that look similar produce very different verdicts, and only this arithmetic tells them apart.
03The file-size limit nobody plans for
Governments publish pixel dimensions. What actually rejects uploads is the byte limit — often 240 KB, sometimes 100 KB, occasionally 20 KB. A correctly sized JPEG at quality 94 can easily be 800 KB.
You cannot pick one quality and hope. You have to search:
var q = 0.94;
(function encode(){
canvas.toBlob(function(blob){
if (blob.size / 1024 > limit && q > 0.32) {
q -= 0.09;
encode(); // async recursion, not a loop
return;
}
save(blob);
}, 'image/jpeg', q);
})();
Nine iterations at most, each one cheaper than the last. The alternative — a hidden quality slider — pushes a problem the user cannot evaluate onto the user.
04The parts that are not data
Two requirements refuse to fit a table cleanly:
- Head size. "Head must be 50–69% of image height" is a proportion of the face, which means you need to know where the face is. A crop guide is the honest compromise: draw the band, let a human place the head inside it.
- Background. Sampling the outer band of pixels gives you an average luminance and a standard deviation. Bright and even is probably fine; dark or busy is probably not. It is a heuristic, so it should read like one — "looks good" and "likely rejected", never "compliant".
A tool that overstates its certainty is worse than no tool. People submit documents based on what it says.
Worth keeping
- Regulatory rules are rows in a table, not screens in a flow.
- The number that decides pass or fail is the post-crop region, not the source dimensions.
- Byte limits require a search, not a constant.
- Where you cannot compute, guide — and say plainly that it is a guide.