How to Get an Image Under an Exact KB Limit, and When You Can't
An exam portal asks for your photo “in JPG format, maximum 20 KB”. Your phone photo is 3 MB. You try a random online compressor, it gives you 24 KB, and the portal still says no. I built ToolOnTap’s “compress to X KB” pages for that moment, and getting under a number reliably turned out to need more than a quality slider.
What “20 KB” means to a form
The first trap is the unit. Some sites count a kilobyte as 1,000 bytes, others as 1,024. For a 20 KB limit that’s either 20,000 or 20,480 bytes, and a file of 20,300 bytes passes one check and fails the other. You can’t tell from the form which one it uses.
So the compressor aims under the stricter one:
// Sites count a KB as 1,000 or 1,024 bytes; aim under the stricter one so either limit is met
const target = maxKB * 1000
The result list checks against the same number. On the fixed-target pages, a file that still ends up above it gets a warning, “Still above 20 KB”, so you don’t upload something that looks fine and then bounces.
The first pass: quality, then a little size
The compression itself is done by browser-image-compression (version 2.0.2), an open-source library that loads only when you add a file. I call it with a byte target and at most 15 rounds. Reading its source, one call works like this:
- Draw the image on a canvas and save it once at full quality.
- If that’s already under the target, stop.
- Otherwise, each round multiplies the quality by 0.95 and shrinks the width and height by 5%, then saves again.
- Stop when the file fits or the rounds run out.
After 15 rounds, the quality is down to about 46% of where it started (0.95¹⁵ ≈ 0.46) and each side is about 46% of the original. For most photos and the larger targets, that’s enough.
At 10 or 20 KB it often isn’t. Lower quality saves less and less, because a JPG still has to describe every 8 × 8 block of pixels in the picture.
When quality isn’t enough, shrink the picture
The bytes in a compressed photo come mostly from detail, and detail grows with the number of pixels. Halving the width and height leaves a quarter of the pixels, which usually saves more than lowering the quality further. So when the first pass misses, the tool estimates a smaller size and starts again from the original file:
export function nextMaxSide(side: number, size: number, target: number): number {
const estimate = side * Math.sqrt(target / size) * 0.9
return Math.max(1, Math.floor(Math.min(side * 0.9, estimate)))
}
If file size grows with side², then the side has to shrink by the square root of how far off the size is. A 2,000-pixel image that came out at 400 KB for a 100 KB target is 4 times too big, so the side should halve. The extra 0.9 aims a little lower, because the estimate is rough. The Math.min(side * 0.9, …) makes sure every retry shrinks by at least 10%, even when the file is only a few bytes over and the estimate says “almost no change”. For that 2,000-pixel image the next try is 900 pixels on the long side, a case that’s in the unit tests.
There are up to 4 of these retries, each one a full run of the library with a lower maxWidthOrHeight. Each run starts from the original file, so quality loss doesn’t pile up from one try to the next. When the size had to drop, the file gets a note such as “Scaled down to 640 × 480 px to get closer to 20 KB”, so you can check it against the form’s pixel rules.
JPG and PNG behave differently
JPG is lossy and has a quality setting, so the library can trade detail for bytes smoothly. PNG is lossless, and the browser’s canvas has no quality setting for it at all. The library handles PNG with its own encoder that reduces the number of colors instead. It starts at 4,096 colors and multiplies that by 0.85 each round, alongside the same 5% size cut.
That works for logos, icons and screenshots, which have few colors to begin with. For photos it’s poor value. A photo has smooth gradients, and squeezing them into a small palette gives visible bands while saving less than JPG would. That’s why the two kinds of page differ:
| Page | Output format | Target |
|---|---|---|
| Compress images | Same as the input (JPG stays JPG, PNG keeps transparency, WebP stays WebP; other formats become JPG) | 200 KB |
| Compress image to 10, 20, 30, 50, 100, 200 or 500 KB | Always JPG | The number in the page name |
The fixed-target pages exist because the forms that set such limits almost always want a photo, and almost always as a JPG. A PNG photo would rarely fit in 20 KB anyway. When a transparent PNG goes to JPG, the library paints the canvas white first, so see-through areas come out white.
When the target is impossible
Some images can’t reach some targets with a face still recognisable in them. A detailed landscape at 10 KB is one, and so is a photo full of grain from a dark room. The tool is built to stop rather than hand you a postage stamp:
- Retries stop after 4 attempts.
- They also stop if the next size would be under 64 pixels on the long side.
- If the file is still over the target at that point, you get the last file it made, with the “Still above 20 KB” warning and a hint to crop it or use a smaller photo.
Cropping is usually the fix. In a head-and-shoulders photo, the background and the clothes take up a good share of the bytes. Cut them away, and more of the 20 KB is spent on the face.
At the other end, a file that’s already under the target comes back almost unchanged. If it ends up much smaller than the limit, the photo was simply efficient, and nothing was thrown away to get there.
A few things worth doing before you compress
- Resize first if the form gives pixel dimensions. An image that’s already 600 × 600 needs much less compression than a 4,000-pixel photo squeezed to the same bytes, and it stays sharper.
- Use daylight and a plain background. Smooth areas cost almost nothing. Noise and patterns cost a lot.
- Keep the original. Compress a copy, because a compressed JPG can’t be turned back into the sharp one.
Try it
For a specific limit, use the matching page: Compress image to 20 KB, 50 KB, 10 KB, 30 KB, 100 KB, 200 KB or 500 KB. To make images smaller while keeping their format, use Compress images. Everything runs on your device. The other image tools are on the image tools page.