A passport photo upload fails for one of three reasons, and they are easy to confuse:

  1. The file size is over the cap (KB)
  2. The pixel dimensions are wrong (px)
  3. The framing is wrong (head size, background, glasses)

Most guides blend all three together, which is why people compress a photo until it
looks terrible and still get rejected — because the real problem was #3.

Here are the actual published numbers, then what each one means in practice.

The specs, from the sources

United States — DS-160 / DS-260 / Diversity Visa

Requirement Value
Format JPEG only
Dimensions Square, 600×600 minimum, 1200×1200 maximum
File size ≤ 240 KB
Compression ratio ≤ 20:1
Head height 50–69% of image height
Eye line 56–69% from the bottom
Background Plain white or off-white
Glasses Not allowed (medical exceptions only)
Recency Within 6 months

Source: U.S. Department of State — Photo Requirements

That "20:1 compression ratio" line is the one nobody reads. A photo pulled off WhatsApp
has often been compressed far past that, and the system can reject it even when the
pixel dimensions and byte size are perfect.

Schengen (35×45 mm ICAO format)

Requirement Value
Print size 35 × 45 mm
Face coverage 70–80% of the photo
Background Plain, light-coloured
Recency Within 6 months

Sources: Czech MFA,
France-Visas photo guidance

Note the conflict with the US: 70–80% face coverage for Schengen vs 50–69% head height
for the US
. A photo cropped for one is wrong for the other. This is the single most
common reason people redo a photo that "already met the requirements" — it met the
other country's requirements.

India — Passport Seva online upload

Requirement Value
Pixel dimensions 630 × 810 exactly
File size 10 KB – 250 KB
Format JPEG
Face coverage 80–85%

One caveat on the Indian numbers: unlike the US and Schengen figures, these come from
third-party photo-services documentation rather than a single official spec page. The
630×810 figure is consistent across several sources, but if your upload keeps failing,
check the instructions on the portal itself rather than trusting a blog (including this
one). The point that matters here still holds: if the rejection is about exact pixel
dimensions, compressing harder will never fix it.

What compression can and cannot fix

This is the part worth being blunt about.

Compression fixes: file size in KB. That's it.

Compression does not fix:

  • Wrong pixel dimensions
  • Wrong aspect ratio
  • Head too small or too large in frame
  • Grey background instead of white
  • Glasses
  • A photo older than 6 months

If your upload was rejected for any of those reasons, running it through a compressor
will produce a smaller file that gets rejected for the same reason.

I built a small browser tool for the first problem specifically, because it is the one
that is mechanical and easy to get wrong by hand: imgpress-c90.pages.dev
takes a target in KB and finds the highest JPEG/WebP quality that still fits under it.
Everything runs in the tab — no upload, which matters for a passport scan.

But it deliberately does not crop to 600×600 or 630×810, because cropping is where
portals differ and a wrong auto-crop is worse than no crop.

Getting under a small KB cap without destroying the photo

Portals that ask for 20–50 KB are usually old government systems with a database field
sized decades ago. The trap is that people compress by dragging a quality slider down
until the number fits, then wonder why the face looks like a watercolour.

The better technique is to treat the cap as a constraint and search for the best quality
that satisfies it, downscaling resolution only when quality alone cannot get there:

async function encodeUnderTarget(canvas, mime, targetBytes) { const top = await toBlob(canvas, mime, 0.95); if (top.size <= targetBytes) return top; // already fits let lo = 0.15, hi = 0.95, best = null; for (let i = 0; i < 6; i++) { const mid = (lo + hi) / 2; const blob = await toBlob(canvas, mime, mid); if (blob.size <= targetBytes) { best = blob; lo = mid; } else { hi = mid; } } return best; // null => downscale and retry } 

Two things I got wrong building this, in case you are writing something similar:

File size scales with the scale factor roughly linearly, not with area. I assumed
scale ≈ sqrt(target/actual) (area model) and it under-shrank every time, so the loop
crept toward the target one round at a time. Measuring it showed 0.423× dimensions →
0.420× bytes, and 0.652× → 0.646×. The exponent is about 1, so the correct step is just
scale ≈ target/actual. On a 12MP test image that cut the search from 43 encodes to 16.

A binary search never tests its own floor. If the lowest quality happens to fit
exactly at the boundary, six probes all miss it and the round is thrown away. Testing
q = 0.15 explicitly after the search fixed a whole wasted round.

One more thing that matters for ID photos: transparent PNGs composite onto black in
JPEG
. You have to paint the canvas white before drawing, or every transparent region
comes out black — which on a passport photo looks like a corrupted upload.

ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, w, h); ctx.drawImage(img, 0, 0, w, h); 

A checklist before you upload

  • [ ] Pixel dimensions match the portal's requirement exactly (not close)
  • [ ] Aspect ratio is right — square for the US, 7:9 for India's 630×810
  • [ ] File size is under the cap, and the compression ratio is not absurd (US: ≤20:1)
  • [ ] Background is plain white or off-white, no shadows
  • [ ] Head/facial coverage matches that country's rule, not another's
  • [ ] No glasses (US), no head covering except religious/medical
  • [ ] Taken within 6 months
  • [ ] You kept the original — do all compression from the original, never from an already-compressed copy

That last point is worth repeating. Stacking compressions is how a perfectly good photo
becomes unreadable: each pass discards information that was already marginal.

The compression part of this is here
if you want the mechanical fix. The framing and dimension parts you have to check against
your own consulate's page — they genuinely differ, and that is not something a tool should
guess at for you.