There's a bug in an entire category of software, and once you see it you can't unsee it.
Government and university application portals ask for a photo like this:
*Photograph: 200×230 pixels, between 20 KB and 50 KB, JPEG only.
*
Read that carefully. It's not a maximum. It's a range.
Now go and look at what every image compressor on the internet actually does. imagecompressor, easyphoto, shrink.media, instasize, picssizer, fixmykb — they all give you one box: "target size", or "max KB". Some of the better ones (IMGonline, Img2Go, the desktop tool RIOT) will let you aim at an exact size, which is genuinely better.
But not one of them takes a range.
So you compress your photo, it comes out at 8 KB, and the portal rejects it — for being too small. And the error message says something like Invalid photograph, which tells you precisely nothing.
It gets worse when you follow the instructions
Here's the part that turns an annoyance into a trap.
The form also told you it wants 200×230 pixels. So you resize to exactly that. And at 200×230, a clean JPEG of a face is naturally about 10–15 KB. It falls under the 20 KB floor on its own, before you've compressed anything at all.
Now you're stuck in a genuinely impossible loop, because compressing is the wrong direction. You don't need the file smaller. You need it bigger. And there is no image tool anywhere that makes a file bigger, because "make this file bigger" has never been a thing anyone wanted — until a bureaucrat wrote a spec with a floor in it.
This, I'm now fairly sure, is the real reason behind a category of forum post I'd seen for years and never understood:
"I am facing problem in uploading my photo. The size, background, type — everything is perfect but as soon as I upload it, no notification pops and just everything clears up." — someone on Quora, about JEE Mains
Everything is perfect. That's the problem. They checked every rule they could see, and the one that's failing is the one nobody mentions.
So how do you make a JPEG bigger?
Two ways, and the order matters.
First, honestly: turn the quality up.
The usual approach to hitting a size target is to binary-search JPEG quality downward until you're under the ceiling. But if you search for the largest file that still fits under the max — rather than the smallest — you often clear the minimum for free, with better image quality as a bonus:
let lo = 0.05, hi = 1.0, best = null;
for (let i = 0; i < 10; i++) {
const mid = (lo + hi) / 2;
const blob = await encodeJpeg(canvas, mid);
if (blob.size <= maxBytes) {
best = blob; // keep it, and try to go BIGGER
lo = mid;
} else {
hi = mid;
}
}
That's it. Same binary search everyone writes, just optimising for the opposite end. It costs nothing and it fixes most cases, because the extra bytes are real image data — you're handing the portal a better photo, not a padded one.
Second, when the form's own rules contradict each other.
Sometimes even quality 1.0 at 200×230 can't reach 20 KB. The form is demanding small dimensions and a large minimum file size, and for this particular photo those two rules are simply incompatible. There is no honest image you can produce that satisfies both.
At that point you have to add bytes that aren't pixels. And JPEG has a place to put them: the COM (comment) segment, marker 0xFFFE. Every decoder on earth skips it.
function padJpegToMinimum(bytes, minBytes) {
const shortfall = minBytes - bytes.length;
if (shortfall <= 0) return bytes;
// COM segment: FF FE <2-byte big-endian length>
// The length counts itself, so payload = length - 2, capped at 0xFFFF.
const MAX_PAYLOAD = 0xffff - 2;
const segments = [];
let remaining = shortfall;
while (remaining > 0) {
const payload = Math.min(remaining > 4 ? remaining - 4 : 1, MAX_PAYLOAD);
const length = payload + 2;
segments.push(0xff, 0xfe, (length >> 8) & 0xff, length & 0xff);
for (let i = 0; i < payload; i++) segments.push(0x20); // spaces
remaining -= payload + 4;
}
// Insert straight after SOI (FF D8).
const out = new Uint8Array(bytes.length + segments.length);
out.set(bytes.subarray(0, 2), 0);
out.set(segments, 2);
out.set(bytes.subarray(2), 2 + segments.length);
return out;
}
Not one pixel changes. The image a human sees is bit-for-bit identical to the one at quality 1.0. You're satisfying a byte-count check, and nothing else.
Is that cheating? I'd argue no — but I also think you have to say you're doing it, so the tool prints a note on screen whenever it pads: "At 200×230 this photo is naturally smaller than the 20 KB minimum even at full quality, so we padded the file's metadata to clear it. Not one pixel was changed." If a tool quietly manipulates a file to pass a validation check and doesn't tell you, that's the bit I'd have a problem with.
How to be sure you didn't corrupt it
The test that gave me confidence walks the output's own segment markers to find the SOF (start of frame) header and reads the real dimensions back out:
function readJpegSize(buf) {
if (buf[0] !== 0xff || buf[1] !== 0xd8) throw new Error("not a JPEG");
let i = 2;
while (i < buf.length) {
if (buf[i] !== 0xff) throw new Error("desynced — segment lengths are wrong");
const marker = buf[i + 1];
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8) {
return { height: buf.readUInt16BE(i + 5), width: buf.readUInt16BE(i + 7) };
}
i += 2 + buf.readUInt16BE(i + 2);
}
throw new Error("no SOF marker");
}
To reach the SOF it has to correctly skip every segment you inserted. Get a single length byte wrong and the walk desynchronises and throws. A corrupt photo that passed a size check would be far worse than shipping nothing.
The other one: your file isn't the format you think it is
While I was in there, a second cause of the same silent failure turned up.
iPhones shoot HEIC by default. Most portals only accept JPEG. Fine — except that AirDrop, WhatsApp and Windows Photos will all cheerfully hand you a file called photo.jpg that is still HEIC inside.
So people are entirely certain their file is a JPEG. It says .jpg. And the upload fails silently.
Check the bytes, not the name. HEIC is ISO-BMFF: bytes 4–8 are the literal string ftyp, and the brand right after tells you the flavour:
async function isHeic(file) {
const head = new Uint8Array(await file.slice(0, 12).arrayBuffer());
if (String.fromCharCode(...head.subarray(4, 8)) !== "ftyp") return false;
const brand = String.fromCharCode(...head.subarray(8, 12));
return ["heic","heix","hevc","hevx","mif1","msf1"].includes(brand);
}
One more trap here, and I fell straight into it: createImageBitmap() decodes HEIC on Safari and iOS, and not on Chrome or Firefox. So my HEIC support worked perfectly when I tested it on an iPhone, and was completely broken for anyone who'd AirDropped the photo to a laptop first — which is exactly what people do when they're filling in a form. It shipped that way, advertised on the tin, for longer than I'd like.
The thing I decided not to build
Every site in this niche ships a table of per-exam presets: "SSC: 200×230px, 20–50 KB". It's the obvious feature. It's free SEO — one landing page per exam, dozens of pages.
I went to build it, and found that the presets contradict each other. For one railway exam, one site published 200×230px / 20–50 KB and another published 320×240px / 40–100 KB. They can't both be right. The official notifications frequently don't state fixed numbers at all — they tell candidates to follow the live upload form. And the specs change between recruitment cycles anyway.
A wrong number there doesn't give someone a worse photo. It gets their application rejected.
So I shipped no preset table, and the tool asks you to read the two numbers off your own form instead. It was the easiest forty pages I could have added to the site and I didn't take them, which I mention only because I think it's the interesting decision, not the noble one — if your data source is provably self-contradictory, "ship it anyway with a confident UI" is just lying with extra steps.
It runs in your browser, and you can check
All of the above is client-side. Not as a feature, but because an ID photo is exactly the file you shouldn't have to upload to a stranger's server in order to resize.
The trouble is that "we don't upload your files" is what every file-tool site says, including the ones that upload your files. It's unfalsifiable, so it convinces nobody who's actually worried.
So: turn off your Wi-Fi. It still works. A site that runs with the network off can't be secretly sending your documents anywhere, and that takes five seconds to check without trusting a word I've said. It's a service worker and about 230 KB of precached shell — the cheapest trust signal I've ever shipped.
The tool
It's free, there's no signup, and it does the whole rule rather than half of it: exact pixel dimensions, both ends of the KB range, HEIC in and JPEG out.
👉 https://docexp.com/tool/photo-upload-fixer
If you've ever had an upload rejected by a form that wouldn't tell you why, it was probably the minimum.
Top comments (0)