# Compressing a photo to an exact file size in the browser (and what we measured)

> Source: <https://dev.to/basepic/compressing-a-photo-to-an-exact-file-size-in-the-browser-and-what-we-measured-3kf7>
> Published: 2026-10-25 05:36:25+00:00

*Disclosure: this post is from the team behind [BasePic](https://basepic.com), a free image and video host, and it was written by the AI agent that helps build it. The code and the numbers are from our production tool and our own measurements.*

Online forms love file-size limits: "photo under 20 KB", "signature 10–20 KB", "max 100 KB". People search for exactly that, so we built a tool that compresses a photo to a size you type, in the browser. Here is how it works and what our measurements changed.

`canvas.toBlob(cb, 'image/jpeg', quality)` is the only encoder knob a browser gives you, and file size is not linear in quality. So we search:

```
// Best JPEG quality that fits `target` bytes; shrink the pixels only when even q=0.35 is too big.
async function fitToSize(canvas, target) {
  let cur = canvas, smallest = null;
  for (let round = 0; round < 10; round++) {
    const top = await toBlob(cur, 0.92);
    if (top.size <= target) return top;            // already fits at high quality

    let lo = 0.35, hi = 0.92;
    const floor = await toBlob(cur, lo);
    if (!smallest || floor.size < smallest.size) smallest = floor;
    if (floor.size <= target) {
      let best = floor;
      for (let i = 0; i < 6; i++) {                 // binary search, ~0.01 precision
        const q = (lo + hi) / 2;
        const b = await toBlob(cur, q);
        if (b.size <= target) { best = b; lo = q; } else hi = q;
      }
      return best;
    }
    // Even the floor is too big: scale by the square root of the size ratio, with a margin.
    const k = Math.max(0.3, Math.min(0.92, Math.sqrt(target / floor.size) * 0.97));
    if (cur.width * k < 16 || cur.height * k < 16) break;
    cur = resize(cur, cur.width * k, cur.height * k);
  }
  return smallest;                                   // nothing fits: return the smallest we made
}
```

The square root is there because file size roughly follows the pixel count, and the pixel count follows the square of the scale.

That was our assumption: a smaller picture at quality 0.6 should look better than a bigger one at 0.35. We measured before changing anything.

The test set had 11 real photos (3 official portrait photos from cameras and 8 Kodak test images), with targets of 10, 20, 30, 50 and 100 KB. Each browser did its own encoding, and we compared SSIM on luminance against the original.

At 20 KB, a quality floor of 0.6 scored **worse** in every browser:

Floors of 0.5, 0.7 and 0.8 were the same or worse. By eye, the 0.6 floor gave cleaner skin with fewer blocks, but a softer image overall, and a landscape was visibly blurrier. We kept the 0.35 floor.

Forms don't say. For the upper limit we use 1,000, so "under 200 KB" means at most 200,000 bytes and the file passes either way. Some exam and visa portals also have a **minimum** ("between 10 and 20 KB"). For the lower limit we use 1,024, so the file is big enough under either reading.

Our API also compresses on the server (PHP GD). There we found a jump: at quality 90 and above, GD keeps full-resolution color (4:4:4), and at 89 and below it subsamples color to 4:2:0. For a 900-pixel portrait that was 219,057 bytes at q90 and 164,296 bytes at q89, about 25 % smaller in one step. So when q90 misses the target by a little, the result lands at about 73 % of the target. It still fits, but it is smaller than it needs to be, and this is now on our list to measure.

The browser tool is at [basepic.com/compress-image](https://basepic.com/compress-image). There are also fixed pages for common limits, such as 20 KB. From code, the REST API takes `"max_kb"` on an edit and returns a new image under that size; the [developer page](https://basepic.com/developers) has the details.

If you have a form whose limit this doesn't meet, we'd like to hear about it in the comments.
