[{"data":1,"prerenderedAt":48},["ShallowReactive",2],{"$fivtukUgQW-bbglAw5sguQmN8NECCDxm7iKx23MFmwF4":3},{"slug":4,"title":5,"description":6,"date":7,"tags":8,"tools":13,"draft":22,"html":23,"toc":24,"minutes":47},"compress-image-to-exact-kb","How to Get an Image Under an Exact KB Limit, and When You Can't","What \"20 KB\" means to a form, how ToolOnTap lowers quality and then pixel size to reach it, why PNG behaves differently, and what happens when it can't.","2026-10-13",[9,10,11,12],"image compression","jpeg","png","algorithms",[14,15,16,17,18,19,20,21],"compress-image","compress-image-to-10kb","compress-image-to-20kb","compress-image-to-30kb","compress-image-to-50kb","compress-image-to-100kb","compress-image-to-200kb","compress-image-to-500kb",false,"\u003Cp>An exam portal asks for your photo “in JPG format, maximum 20 KB”. Your phone photo is 3 MB. You try a random online compressor, it gives you 24 KB, and the portal still says no. I built ToolOnTap’s “compress to X KB” pages for that moment, and getting under a number reliably turned out to need more than a quality slider.\u003C\u002Fp>\n\u003Ch2 id=\"what-20-kb-means-to-a-form\">What “20 KB” means to a form\u003C\u002Fh2>\n\u003Cp>The first trap is the unit. Some sites count a kilobyte as 1,000 bytes, others as 1,024. For a 20 KB limit that’s either 20,000 or 20,480 bytes, and a file of 20,300 bytes passes one check and fails the other. You can’t tell from the form which one it uses.\u003C\u002Fp>\n\u003Cp>So the compressor aims under the stricter one:\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#57606a;--shiki-dark:#8b949e\">\u002F\u002F Sites count a KB as 1,000 or 1,024 bytes; aim under the stricter one so either limit is met\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">const\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> target\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> maxKB \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">*\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 1000\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The result list checks against the same number. On the fixed-target pages, a file that still ends up above it gets a warning, “Still above 20 KB”, so you don’t upload something that looks fine and then bounces.\u003C\u002Fp>\n\u003Ch2 id=\"the-first-pass-quality-then-a-little-size\">The first pass: quality, then a little size\u003C\u002Fh2>\n\u003Cp>The compression itself is done by browser-image-compression (version 2.0.2), an open-source library that loads only when you add a file. I call it with a byte target and at most 15 rounds. Reading its source, one call works like this:\u003C\u002Fp>\n\u003Col>\n\u003Cli>Draw the image on a canvas and save it once at full quality.\u003C\u002Fli>\n\u003Cli>If that’s already under the target, stop.\u003C\u002Fli>\n\u003Cli>Otherwise, each round multiplies the quality by 0.95 and shrinks the width and height by 5%, then saves again.\u003C\u002Fli>\n\u003Cli>Stop when the file fits or the rounds run out.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>After 15 rounds, the quality is down to about 46% of where it started (0.95¹⁵ ≈ 0.46) and each side is about 46% of the original. For most photos and the larger targets, that’s enough.\u003C\u002Fp>\n\u003Cp>At 10 or 20 KB it often isn’t. Lower quality saves less and less, because a JPG still has to describe every 8 × 8 block of pixels in the picture.\u003C\u002Fp>\n\u003Ch2 id=\"when-quality-isn-t-enough-shrink-the-picture\">When quality isn’t enough, shrink the picture\u003C\u002Fh2>\n\u003Cp>The bytes in a compressed photo come mostly from detail, and detail grows with the number of pixels. Halving the width and height leaves a quarter of the pixels, which usually saves more than lowering the quality further. So when the first pass misses, the tool estimates a smaller size and starts again from the original file:\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">export\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> function\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> nextMaxSide\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">side\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> number\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">size\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> number\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#b35309;--shiki-dark:#FFAB70\">target\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> number\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">)\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> number\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  const\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> estimate\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> side \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">*\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> Math.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">sqrt\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(target \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">\u002F\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> size) \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">*\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 0.9\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">  return\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> Math.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">max\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">1\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, Math.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">floor\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(Math.\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">min\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(side \u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#cb2431;--shiki-dark:#F97583\">*\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 0.9\u003C\u002Fspan>\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">, estimate)))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>If file size grows with side², then the side has to shrink by the square root of how far off the size is. A 2,000-pixel image that came out at 400 KB for a 100 KB target is 4 times too big, so the side should halve. The extra \u003Ccode>0.9\u003C\u002Fcode> aims a little lower, because the estimate is rough. The \u003Ccode>Math.min(side * 0.9, …)\u003C\u002Fcode> makes sure every retry shrinks by at least 10%, even when the file is only a few bytes over and the estimate says “almost no change”. For that 2,000-pixel image the next try is 900 pixels on the long side, a case that’s in the unit tests.\u003C\u002Fp>\n\u003Cp>There are up to 4 of these retries, each one a full run of the library with a lower \u003Ccode>maxWidthOrHeight\u003C\u002Fcode>. Each run starts from the original file, so quality loss doesn’t pile up from one try to the next. When the size had to drop, the file gets a note such as “Scaled down to 640 × 480 px to get closer to 20 KB”, so you can check it against the form’s pixel rules.\u003C\u002Fp>\n\u003Ch2 id=\"jpg-and-png-behave-differently\">JPG and PNG behave differently\u003C\u002Fh2>\n\u003Cp>JPG is lossy and has a quality setting, so the library can trade detail for bytes smoothly. PNG is lossless, and the browser’s canvas has no quality setting for it at all. The library handles PNG with its own encoder that reduces the number of colors instead. It starts at 4,096 colors and multiplies that by 0.85 each round, alongside the same 5% size cut.\u003C\u002Fp>\n\u003Cp>That works for logos, icons and screenshots, which have few colors to begin with. For photos it’s poor value. A photo has smooth gradients, and squeezing them into a small palette gives visible bands while saving less than JPG would. That’s why the two kinds of page differ:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\u003Cthead>\n\u003Ctr>\n\u003Cth>Page\u003C\u002Fth>\n\u003Cth>Output format\u003C\u002Fth>\n\u003Cth>Target\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>Compress images\u003C\u002Ftd>\n\u003Ctd>Same as the input (JPG stays JPG, PNG keeps transparency, WebP stays WebP; other formats become JPG)\u003C\u002Ftd>\n\u003Ctd>200 KB\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Compress image to 10, 20, 30, 50, 100, 200 or 500 KB\u003C\u002Ftd>\n\u003Ctd>Always JPG\u003C\u002Ftd>\n\u003Ctd>The number in the page name\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\u003Cp>The fixed-target pages exist because the forms that set such limits almost always want a photo, and almost always as a JPG. A PNG photo would rarely fit in 20 KB anyway. When a transparent PNG goes to JPG, the library paints the canvas white first, so see-through areas come out white.\u003C\u002Fp>\n\u003Ch2 id=\"when-the-target-is-impossible\">When the target is impossible\u003C\u002Fh2>\n\u003Cp>Some images can’t reach some targets with a face still recognisable in them. A detailed landscape at 10 KB is one, and so is a photo full of grain from a dark room. The tool is built to stop rather than hand you a postage stamp:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Retries stop after 4 attempts.\u003C\u002Fli>\n\u003Cli>They also stop if the next size would be under 64 pixels on the long side.\u003C\u002Fli>\n\u003Cli>If the file is still over the target at that point, you get the last file it made, with the “Still above 20 KB” warning and a hint to crop it or use a smaller photo.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Cropping is usually the fix. In a head-and-shoulders photo, the background and the clothes take up a good share of the bytes. Cut them away, and more of the 20 KB is spent on the face.\u003C\u002Fp>\n\u003Cp>At the other end, a file that’s already under the target comes back almost unchanged. If it ends up much smaller than the limit, the photo was simply efficient, and nothing was thrown away to get there.\u003C\u002Fp>\n\u003Ch2 id=\"a-few-things-worth-doing-before-you-compress\">A few things worth doing before you compress\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Cstrong>Resize first if the form gives pixel dimensions.\u003C\u002Fstrong> An image that’s already 600 × 600 needs much less compression than a 4,000-pixel photo squeezed to the same bytes, and it stays sharper.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Use daylight and a plain background.\u003C\u002Fstrong> Smooth areas cost almost nothing. Noise and patterns cost a lot.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Keep the original.\u003C\u002Fstrong> Compress a copy, because a compressed JPG can’t be turned back into the sharp one.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"try-it\">Try it\u003C\u002Fh2>\n\u003Cp>For a specific limit, use the matching page: \u003Ca href=\"\u002Fcompress-image-to-20kb\">Compress image to 20 KB\u003C\u002Fa>, \u003Ca href=\"\u002Fcompress-image-to-50kb\">50 KB\u003C\u002Fa>, \u003Ca href=\"\u002Fcompress-image-to-10kb\">10 KB\u003C\u002Fa>, \u003Ca href=\"\u002Fcompress-image-to-30kb\">30 KB\u003C\u002Fa>, \u003Ca href=\"\u002Fcompress-image-to-100kb\">100 KB\u003C\u002Fa>, \u003Ca href=\"\u002Fcompress-image-to-200kb\">200 KB\u003C\u002Fa> or \u003Ca href=\"\u002Fcompress-image-to-500kb\">500 KB\u003C\u002Fa>. To make images smaller while keeping their format, use \u003Ca href=\"\u002Fcompress-image\">Compress images\u003C\u002Fa>. Everything runs on your device. The other image tools are on the \u003Ca href=\"\u002Fimage-tools\">image tools\u003C\u002Fa> page.\u003C\u002Fp>\n",[25,29,32,35,38,41,44],{"id":26,"text":27,"level":28},"what-20-kb-means-to-a-form","What “20 KB” means to a form",2,{"id":30,"text":31,"level":28},"the-first-pass-quality-then-a-little-size","The first pass: quality, then a little size",{"id":33,"text":34,"level":28},"when-quality-isn-t-enough-shrink-the-picture","When quality isn’t enough, shrink the picture",{"id":36,"text":37,"level":28},"jpg-and-png-behave-differently","JPG and PNG behave differently",{"id":39,"text":40,"level":28},"when-the-target-is-impossible","When the target is impossible",{"id":42,"text":43,"level":28},"a-few-things-worth-doing-before-you-compress","A few things worth doing before you compress",{"id":45,"text":46,"level":28},"try-it","Try it",5,1792325800902]