Upload forms for visas, exams and job portals often say "photo must be under 100 KB". A phone photo is 3 to 5 MB, and the usual advice is to drag a quality slider until the number looks right. You can automate that in the browser with the canvas API, and the part that matters is knowing when to stop lowering quality.
Encode at a quality and read the size
canvas.toBlob(callback, "image/jpeg", quality) gives you a Blob. blob.size is the file size in bytes. That is all you need to measure.
function encode(bitmap, width, height, quality) {
const canvas = document.createElement("canvas")
canvas.width = width
canvas.height = height
const ctx = canvas.getContext("2d")
ctx.fillStyle = "#fff" // JPEG has no alpha; transparent PNGs turn black without this
ctx.fillRect(0, 0, width, height)
ctx.imageSmoothingQuality = "high"
ctx.drawImage(bitmap, 0, 0, width, height)
return new Promise((resolve) => canvas.toBlob(resolve, "image/jpeg", quality))
}
Load the bitmap with createImageBitmap(file, { imageOrientation: "from-image" }), otherwise phone photos come out sideways because the EXIF rotation is ignored.
Binary search on quality
File size grows with quality, so you can search for the highest quality that still fits:
let low = 0.3, high = 0.95, best = null
for (let i = 0; i < 6; i++) {
const q = (low + high) / 2
const blob = await encode(bitmap, w, h, q)
if (blob.size <= targetBytes) { best = blob; low = q } else { high = q }
}
Six rounds narrows the range to about 1% of quality, which is more precision than the file size needs. Count 1 KB as 1000 bytes, not 1024, so the file passes whether the form counts KB one way or the other.
Know when to stop
Here is the part the sliders do not tell you. Below roughly 30% JPEG quality a photo breaks into visible blocks and the colours shift. A file that is technically under the limit but looks like that gets rejected by a human reviewer or looks bad on an ID. So the search never goes below 0.3. First it encodes once at 0.3 for the current size. If that is still over the limit, it makes the picture smaller instead: width and height times 0.8, and try again. It tries up to 12 sizes, which is down to about a tenth of the original width. Only at the last size does it allow quality all the way down to 0.05, because a file that fits beats no file.
PNG has no quality setting, so for PNG only the dimensions change.
What I would do differently
- Offer a "keep exact width and height" switch for forms that demand fixed pixels, and then accept a lower quality or a missed target. The tool I built has one, and it tells the user when the target was missed.
- Test the search with a fake encoder. The function takes the encoder as a parameter, so unit tests can pass a function that returns a Blob of a size you choose, with no canvas involved.
The tool built on this, with nothing uploaded anywhere:
- Image compressor with a target size: https://www.toolsy-hub.com/tools/image-compressor
- Photo resizer for exact KB and pixel presets: https://www.toolsy-hub.com/tools/photo-signature-resizer
Google Analytics counts visits on the site; image files are never sent anywhere.
Top comments (0)