A passport photo rejection is one of the more frustrating pieces of paperwork in modern life. You take a photo, an online form tells you it needs to be 300 DPI, you run it through something that says "300 DPI", you upload it, and it comes back rejected for the same reason.
The reason is almost always that the form is not checking DPI at all. It is checking pixel dimensions. And the tool you used changed the number in a header without changing the number of pixels, because those are two entirely different things.
DPI is a division problem, not a stored quantity of detail
"300 DPI" means: if you print this at 300 dots per inch, it will be one inch wide.
That number is only meaningful when you also know the print size. Which means DPI can never be a property of a file on its own — it is always a statement about a file plus a physical size.
If you want a 2 × 2 inch photo at 300 DPI, you need:
2 in × 300 dpi = 600 pixels
2 in × 300 dpi = 600 pixels
600 × 600. That is the entire requirement. Nothing else matters.
Here are the numbers for the sizes that come up most often:
| Target size | Pixels at 300 DPI |
|---|---|
| 2 × 2 in (US passport) | 600 × 600 |
| 35 × 45 mm (Schengen visa) | 413 × 531 |
| 4 × 6 in print | 1200 × 1800 |
| 8 × 10 in print | 2400 × 3000 |
| A4 at print quality | 2480 × 3508 |
Note that the 35 × 45 mm case is not a round number. 35 mm is 1.378 inches; times 300 is 413.4. Every passport photo tool that gives you "413 × 531" is rounding down the same two divisions.
What the file actually stores
A JPEG has nowhere to put "this looks good at 300 DPI" in any absolute sense. What it stores is a hint for the printing software, and it lives in the JFIF header:
- units — 0 means "no units, aspect ratio only", 1 means dots per inch, 2 means dots per centimetre
- X density / Y density — the number, per unit
That is three fields, five bytes total, sitting near the start of the file. It is metadata in the purest sense: changing it does not touch a single pixel.
Which is exactly why the trick of "resave it at 300 DPI" produces a file that still gets rejected. If your photo is 800 pixels wide and you rewrite the header to say 300 DPI, you now have a file that claims to be 2.67 inches wide at print resolution. It is still 800 pixels. If the form wanted 600 × 600, you were already fine. If it wanted 1200 × 1800, you are still short and no header edit will help.
Reading it out of a JPEG
Most tools will not show you the raw value, so here is how to check it yourself. The JFIF APP0 segment starts with FF E0, and after the two length bytes comes the ASCII string JFIF\0:
async function readJpegDensity(file) {
const bytes = new Uint8Array(await file.arrayBuffer());
const view = new DataView(bytes.buffer);
if (view.getUint16(0) !== 0xffd8) return null; // not a JPEG
let off = 2;
while (off < bytes.length - 16) {
if (bytes[off] !== 0xff) return null;
const marker = bytes[off + 1];
const length = view.getUint16(off + 2);
// APP0 + "JFIF"
if (marker === 0xe0 && view.getUint32(off + 4) === 0x4a464946) {
const units = bytes[off + 11]; // 0 aspect, 1 dpi, 2 dpcm
const x = view.getUint16(off + 12);
const y = view.getUint16(off + 14);
return { units, x, y };
}
if (marker === 0xda) break; // start of scan: no JFIF segment
off += 2 + length;
}
return null;
}
If this returns null, or returns units: 0, the file has no DPI to speak of. Whatever software you opened it in invented a default — 72 and 96 are the usual guesses, and neither is stored anywhere.
The trap: canvas will not write it for you
This is the part that catches people building the tool rather than using it. Draw an image to a canvas, export it:
const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality: 0.92 });
The browser produces a valid JPEG. It does not produce one with a meaningful density value. Canvas has no API for setting DPI, and toBlob takes a type and a quality, nothing else. So any claim that a canvas-based tool outputs "300 DPI" is either wrong or doing something below the encoding layer.
I ran the reader above against two files while writing this — one a marketplace test fixture, one straight out of a browser canvas. Both came back identical:
{ units: 0, x: 1, y: 1 }
Not 72, not 96. A declared density of one dot per inch with no unit attached, which is the encoder saying "I have no opinion." Every piece of software that opens these files then invents its own default, and they do not agree with each other.
Doing it below the encoding layer is not hard, because you are editing five bytes:
async function setJpegDensity(blob, dpi) {
const bytes = new Uint8Array(await blob.arrayBuffer());
const view = new DataView(bytes.buffer);
let off = 2;
while (off < bytes.length - 16) {
if (bytes[off] !== 0xff) break;
const marker = bytes[off + 1];
const length = view.getUint16(off + 2);
if (marker === 0xe0 && view.getUint32(off + 4) === 0x4a464946) {
bytes[off + 11] = 1; // units = dots per inch
view.setUint16(off + 12, dpi);
view.setUint16(off + 14, dpi);
return new Blob([bytes], { type: 'image/jpeg' });
}
if (marker === 0xda) break;
off += 2 + length;
}
return blob; // no JFIF segment to patch
}
Two caveats worth knowing before you ship this. First, a PNG stores its resolution differently (a pHYs chunk, in pixels per metre), so this code is JPEG-only. Second, if the encoder omitted the JFIF segment entirely, there is nothing to patch in place and you would have to insert one — at which point you are writing a JPEG muxer and should reconsider the scope of your afternoon.
So what should you actually do
Work backwards from the print size:
- Decide the physical size the form wants.
- Multiply by the DPI it demands to get pixels.
- Resize to that pixel count. Crop to the required aspect ratio first, or you will end up stretching a face.
- If a downstream system insists on a DPI value in the header, write it after resizing, not instead of resizing.
Step 4 is optional. Step 3 is the one that gets your photo accepted.
And if the form still rejects it, the problem is almost never DPI. It is background colour, head size as a percentage of frame, glasses, shadow, or a file size ceiling measured in kilobytes. Those are all checkable in the tab, before you upload anything to anyone.
This is the reasoning behind ToolCaravan's ID photo tool — crop to a passport or visa spec, get the exact pixel dimensions, all in the browser. No upload, no account, no watermark.
Top comments (0)