How to Compress an Image to an Exact File Size — And Why It's Harder Than You Think
Passport photos under 200 KB, CMS upload limits, email quotas — hitting an exact file size target requires binary search, not guesswork. Here's how.
Hitting an exact file size — a passport photo under 200 KB, a hero image under a strict CMS limit — is one of those tasks that sounds trivial but rarely is. You compress, overshoot by a few kilobytes, lower the quality, undershoot by twenty, raise it slightly, overshoot again. The problem is that JPEG and WebP compression ratios are non-linear: the relationship between quality and output size depends on the specific content of the image, not a predictable formula.
Why Quality Settings Don't Map Linearly to File Size
JPEG quality 80 for a photo of a clear blue sky might produce a 40 KB file. The same quality setting on a photo of dense forest foliage might produce a 400 KB file. The encoder is measuring image complexity — the amount of high-frequency detail — not respecting a size target. This is by design: the quality setting controls visual fidelity, not file size.
The Correct Approach: Binary Search on Quality
The reliable method for hitting a size target is a binary search over quality values. You set a target size, measure the output at quality 80, then step up or down based on whether you over- or undershot, repeating until you're within tolerance.
async function compressToTargetSize(
file: File,
targetBytes: number,
toleranceBytes = 5000,
format: 'image/jpeg' | 'image/webp' = 'image/jpeg',
): Promise<Blob> {
let lo = 1, hi = 100, bestBlob: Blob | null = null;
while (lo <= hi) {
const quality = Math.round((lo + hi) / 2);
const canvas = document.createElement('canvas');
const bitmap = await createImageBitmap(file);
canvas.width = bitmap.width;
canvas.height = bitmap.height;
canvas.getContext('2d')!.drawImage(bitmap, 0, 0);
const blob = await new Promise<Blob>((res) =>
canvas.toBlob((b) => res(b!), format, quality / 100),
);
if (blob.size <= targetBytes) {
bestBlob = blob;
lo = quality + 1; // try higher quality (larger file)
} else {
hi = quality - 1; // too large, reduce quality
}
if (Math.abs(blob.size - targetBytes) <= toleranceBytes) break;
}
return bestBlob ?? await new Promise<Blob>((res) =>
canvas.toBlob((b) => res(b!), format, lo / 100),
);
}Binary search converges in at most 7 iterations (log₂ 100 ≈ 6.6). This is far faster than manual trial-and-error and avoids the common mistake of incrementing quality by 5 and jumping over the optimal value.
Dimension Reduction: The Other Lever
If quality 1 still produces a file above your target, the image needs to be smaller in pixels. Halving the width and height reduces file size by roughly 75% at the same quality — far more effective than dropping quality to extreme lows.
- ●Calculate the scale factor:
scale = sqrt(targetBytes / currentBytes) - ●Apply the scale:
newWidth = currentWidth × scale - ●Then run the quality binary search on the downscaled image
Common Exact-Size Requirements
- ●UK passport photo: under 10 MB but at least 50 KB — usually resolution-driven, not compression
- ●US visa / DS-160: between 1 KB and 240 KB — quality 70–80 at 600×600px usually lands in range
- ●Indian passport / OCI: under 1 MB, min 10 KB — quality 85 at 1200×1200px
- ●Common CMS limits: WordPress default 8 MB, Webflow 25 MB, Shopify 20 MB per image
The quality slider in our image compressor shows a live size estimate as you drag — use it to zero in on your target without guesswork.
Ready to try it?
All tools run entirely in your browser — no uploads, no account required.
Compress Image