{"slug": "compressing-a-photo-to-an-exact-file-size-in-the-browser-and-what-we-measured", "title": "Compressing a photo to an exact file size in the browser (and what we measured)", "summary": "The BasePic team built a browser-based tool that compresses photos to an exact user-specified file size by binary-searching JPEG quality and, when needed, downscaling by the square root of the size ratio. Testing 11 real photos across targets of 10–100 KB, they found that raising the quality floor to 0.6 scored worse on luminance SSIM in every browser, so they kept a 0.35 floor. They also measured a PHP GD encoder step: quality 90 keeps 4:4:4 color while 89 subsamples to 4:2:0, cutting a 900-pixel portrait from 219,057 to 164,296 bytes.", "body_md": "*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.*\n\nOnline 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.\n\n`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:\n\n```\n// Best JPEG quality that fits `target` bytes; shrink the pixels only when even q=0.35 is too big.\nasync function fitToSize(canvas, target) {\n  let cur = canvas, smallest = null;\n  for (let round = 0; round < 10; round++) {\n    const top = await toBlob(cur, 0.92);\n    if (top.size <= target) return top;            // already fits at high quality\n\n    let lo = 0.35, hi = 0.92;\n    const floor = await toBlob(cur, lo);\n    if (!smallest || floor.size < smallest.size) smallest = floor;\n    if (floor.size <= target) {\n      let best = floor;\n      for (let i = 0; i < 6; i++) {                 // binary search, ~0.01 precision\n        const q = (lo + hi) / 2;\n        const b = await toBlob(cur, q);\n        if (b.size <= target) { best = b; lo = q; } else hi = q;\n      }\n      return best;\n    }\n    // Even the floor is too big: scale by the square root of the size ratio, with a margin.\n    const k = Math.max(0.3, Math.min(0.92, Math.sqrt(target / floor.size) * 0.97));\n    if (cur.width * k < 16 || cur.height * k < 16) break;\n    cur = resize(cur, cur.width * k, cur.height * k);\n  }\n  return smallest;                                   // nothing fits: return the smallest we made\n}\n```\n\nThe square root is there because file size roughly follows the pixel count, and the pixel count follows the square of the scale.\n\nThat 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.\n\nThe 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.\n\nAt 20 KB, a quality floor of 0.6 scored **worse** in every browser:\n\nFloors 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.\n\nForms 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.\n\nOur 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.\n\nThe 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.\n\nIf you have a form whose limit this doesn't meet, we'd like to hear about it in the comments.", "url": "https://wpnews.pro/news/compressing-a-photo-to-an-exact-file-size-in-the-browser-and-what-we-measured", "canonical_source": "https://dev.to/basepic/compressing-a-photo-to-an-exact-file-size-in-the-browser-and-what-we-measured-3kf7", "published_at": "2026-10-25 05:36:25+00:00", "updated_at": "2026-10-25 05:42:28.124671+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-products"], "entities": ["BasePic", "Kodak", "PHP GD"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/compressing-a-photo-to-an-exact-file-size-in-the-browser-and-what-we-measured", "markdown": "https://wpnews.pro/news/compressing-a-photo-to-an-exact-file-size-in-the-browser-and-what-we-measured.md", "text": "https://wpnews.pro/news/compressing-a-photo-to-an-exact-file-size-in-the-browser-and-what-we-measured.txt", "jsonld": "https://wpnews.pro/news/compressing-a-photo-to-an-exact-file-size-in-the-browser-and-what-we-measured.jsonld"}}