Treat head centring as a preference: clamp the crop to valid bounds, then test against the rotated image. That fixed our framing bug. A −1.1° rotation left roughly 10 px of blank space at the top corners of a 538 px-wide crop.
You can check your own photo against these rules for free in your browser on this page; the photo is never uploaded (disclosure: I built it).
Why did ready-made ID photos fail?
When I reviewed analytics on October 3–4, 2026, five people had uploaded a photo during the week. All five saw "likely to be rejected".
Three uploads, all on the Thailand e-Visa page, failed only "Whole head fits in the frame". Their background statistics, mean 254 and standard deviation 6, suggested ready-made studio ID photos with white backgrounds. That was a clue, not proof of where the photos came from.
pixtidy crops photos in the browser to an official spec. Our placement code had two problems: it treated vertical centring as mandatory, then treated the canvas boundary as the image boundary.
A tightly cropped photo exposed both.
How do you keep the whole head inside the crop?
- Separate the required head ratio from your preferred placement.
Thailand e-Visa photos require a head ratio of 70–80%, JPG up to 3 MB, and a height of at least 160 px. There is no published crown-margin or eye-line rule, so our code centred the head vertically.
At a preferred 75% head ratio, that centred crop could extend above the image. Studio ID photos often leave little space above the head. Rejecting the centred placement without trying another allowed position was the first bug.
- Calculate the allowed interval, then clamp your preference.
For crop height H, keeping the crown and chin inside requires:
y <= crownY
y >= chinY - H
These give you lo and hi. Intersect them with any crown-margin or eye-line ranges required by the spec. Then intersect with the image bounds:
function clampTop(
preferredY: number,
lo: number,
hi: number,
imageHeight: number,
H: number,
): number | undefined {
const lower = Math.max(lo, 0);
const upper = Math.min(hi, imageHeight - H);
if (lower > upper) return undefined;
return Math.max(lower, Math.min(preferredY, upper));
}
What does the rotated-image containment test check?
- Test the complete crop against actual image coverage.
Local measurements showed that the photo had been rotated by −1.1° to make the eyes level. Rotation left blank canvas corners. The first fix could put the crop at y = 0, inside the canvas rectangle but outside the real image.
For a 538 px-wide crop, the corner loss was roughly:
538 × sin(1.1°) ≈ 10 px
The head bounds constrain where you may place the crop. The rotated-image boundary decides whether image content covers that entire crop.
Why try 41 positions, and can the scan miss a fit?
- Search within the allowed interval when the clamped candidate fails.
The second fix tries 41 positions across [lo, hi], nearest to the preferred position first, and keeps the first that passes the rotated-image test.
Here, lo and hi already include the intersections above:
function scanTop(
lo: number,
hi: number,
preferredY: number,
insideRotatedImage: (y: number) => boolean,
): number | undefined {
if (lo > hi) return undefined;
const positions = Array.from(
{ length: 41 },
(_, i) => lo + ((hi - lo) * i) / 40,
);
positions.sort(
(a, b) => Math.abs(a - preferredY) - Math.abs(b - preferredY),
);
return positions.find(insideRotatedImage);
}
The measured test photo passed after the fix.
What changed in the live tool?
- Compare the same photo before and after, and preserve legitimate failures.
The live-tool test used a tightly cropped 568×757 photo with the head at 72%. Before the fix, framing failed. Afterward, it passed the Thailand e-Visa framing check.
It still failed the US visa framing check, correctly. The US range is 50–69%, and the head already occupied 72% of the photo.
Face measurements are not sent to analytics. I investigated with a console.log behind a window flag in a local development build, then removed it.
Two new unit tests were added: a tightly cropped photo, and a tilted photo with a custom in-bounds predicate. The existing "face far off-centre stays out of bounds" test still passes. The suite totals 363 tests.
Which edge cases should you keep testing?
Tight top margins: move the crop within the allowed interval before rejecting a preferred position.
Rotated corners: a canvas-bounded crop can still contain blank space. Test actual image coverage.
Incompatible framing: keep the 72% head failing under the US rule, and preserve the far-off-centre regression.
A tool's framing pass does not promise official acceptance.
Related: Checking passport photos against government specs in the browser with MediaPipe · Schengen visa photos are 35 45 mm: turning millimetre rules into browser checks
Disclosure: I built pixtidy.
Top comments (0)