ShridhaBlog

4 min read

How to get a photo under 50 KB without ruining it

A way to shrink a picture to a file size limit in the browser: search for the best JPEG quality, shrink pixels only when needed, and test it without a real image.

Tech

Many Indian exam and job forms reject a photo or signature over 20 KB, 50 KB or 100 KB. Most people end up trying one online tool after another until a file fits. We built Reduce Image Size to KB to do the search properly, in the browser. This is how it works.

The idea#

For one picture at one size, a JPEG's file size goes up as quality goes up. That makes it a search problem: find the highest quality whose file still fits under the limit. A binary search does it in a handful of tries.

async function bestQuality(encode, scale, maxBytes, low) {
  const top = await encode(scale, 0.92)
  if (top.size <= maxBytes) return { blob: top, scale, quality: 0.92 }
  const floor = await encode(scale, low)
  if (floor.size > maxBytes) return null // even the lowest quality is too big at this size
  let lo = low, hi = 0.92, best = { blob: floor, scale, quality: low }
  for (let i = 0; i < 7; i++) {
    const mid = (lo + hi) / 2
    const blob = await encode(scale, mid)
    if (blob.size <= maxBytes) { best = { blob, scale, quality: mid }; lo = mid }
    else hi = mid
  }
  return best
}

Seven halvings narrow the quality range enough that the result lands close to the limit. On a large, noisy 2400 by 3200 photo we tried, the results were 19,921 bytes for a 20 KB limit, 48,258 for 50 KB and 99,765 for 100 KB: always under, and within a few percent.

When quality is not enough#

A 4 MB photo cannot reach 50 KB by lowering quality alone without turning into mush. Pixels are the other lever. So the search has a ladder of sizes, from full size down to 15%, and two rules:

  1. First, keep quality at 50% or better and step down the ladder until a size fits.
  2. Only if nothing fits that way, allow quality as low as 10%.
for (const low of [0.5, 0.1]) {
  for (const scale of SCALES) { // 1, 0.9, 0.8 ... 0.15
    const hit = await bestQuality(encode, scale, maxBytes, low)
    if (hit) return { ...hit, fits: true }
  }
}

The order matters. A slightly smaller picture at decent quality almost always looks better than a full-size picture crushed into blocks, and forms rarely care about the pixel size, only the file size.

Keep the search separate from the browser#

The search never touches a canvas. It takes an encode(scale, quality) function that returns a Blob, so the same code runs in a test with a fake encoder whose size grows with pixels and quality:

const fake = (fullBytes) => async (scale, quality) =>
  new Blob([new Uint8Array(Math.round(fullBytes * scale * scale * (0.05 + quality)))])

With that we test the behaviour we care about: a file that already fits is left alone at high quality; quality is lowered before pixels are; a huge image shrinks but keeps quality at 50% or better; the result is never over the limit when it says it fits; and when nothing can fit, it says so rather than pretending.

Details that bit us#

  • Kilobytes. Some systems count 1 KB as 1000 bytes and some as 1024. A file of 51,200 bytes is "50 KB" to one and 51.2 KB to the other, so a form using the first rule would reject it. We count 1000 bytes per KB, so the file passes either way.
  • Transparent PNGs. JPEG has no transparency, so the picture is drawn onto white first, otherwise transparent areas turn black.
  • Format support. canvas.toBlob quietly returns PNG if the browser cannot save the format you asked for. We check the returned type and show a clear message instead.
  • Browsers differ. Each browser has its own JPEG encoder, so the same quality gives slightly different sizes. That is fine, because the search measures the real size each time rather than predicting it.
  • Privacy. The whole thing runs in your browser. The photo is never sent anywhere, and saved copies do not carry camera details like location.

What it cannot do#

If a form wants an exact pixel size (say 200 by 230) as well as a file size, set the pixels first with an image resizer and then reduce the size. And a form that wants a minimum size as well as a maximum needs a check on the result: we make the file as large as fits, but a small original cannot be made bigger.

← All posts