Skip to content

Build1 publisherNot yet confirmed elsewhere3 min readPublished

An exact byte budget is a search problem, and 18 encodes is the wrong way to run it

A browser image tool's writeup on binary-searching JPEG quality, and on PNG, where the quality-shaped proxy it searches cannot address 155 of the 255 available palette sizes.

The Engineer · Build desk

How we use AISend a correction

What happened

  • Encoders accept a quality parameter and not a byte count, so any tool that promises an exact file size has to search for it.
  • The obvious approach steps quality down from 90 in fives and takes the first output that fits, at a worst case of 18 full-pixel encodes.
  • The writeup replaces it with a binary search over quality 1 to 100, capped at eight iterations, that keeps the highest quality whose output fits.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • cost Every probe is billed to the person watching the spinner, which makes the iteration count a user-experience budget rather than a compute one.
  • constraint Because the monotonicity assumption is not guaranteed, the tool cannot honestly claim it found the maximum quality that fits, only the smallest file it happened to encode.
  • capability Keeping the smallest result converts the impossible-target case from an error dialog into a usable deliverable, which is what the requester wanted anyway.
  • decision Anyone shipping byte budgets for PNG has to accept that hitting the number means throwing away colours, so a size request quietly becomes a lossy edit on a format sold as lossless.

The arithmetic is what makes the case here, not the elegance. Run a 12 megapixel photo through the step-down loop and the worst case is 18 full passes over 12 million pixels, about 216 megapixels of encoding work [20]. The eight-iteration binary search bounds the same job at 96 megapixels [21], a difference of 120 megapixels that nobody sits and watches [22]. The naive loop pays that premium for a worse answer: in the writeup's own example it returns 30 KB against a 100 KB target [8], leaving 70 percent of the requested budget unspent [23].

The cap of eight is not padding either. log2(100) is roughly 6.64, so seven probes collapse a 100-value range to one candidate, and the eighth is spare capacity while still staying well under half the linear scan [10].

The part worth stealing is the three lines that record the smallest output on every iteration, whether or not it fits [11]. They do double duty. If the target is below what quality 1 produces, `best` stays null and a naive version throws [12]; with smallest tracked, the answer becomes the closest achievable file plus a note that the number was not reached [11]. They also cover the case where a higher JPEG quality compresses marginally smaller because the quantization tables changed between levels, which can send the search into the wrong half [24]. The author's framing is that you never detect the violation, you just stop trusting the search to have seen the smallest output [13]. Cheaper than a monotonicity guard, and weaker: what you get is the smallest file you happened to encode, not evidence that no smaller one hides between probes.

PNG is where the design leaks. There is no fidelity dial, and compression level moves speed far more than size [14], so bytes have to come out of the palette, which is what pngquant does by quantizing 16.7 million colors to N and handing DEFLATE a smaller symbol set [15]. The code keeps a quality-shaped interface and maps it: round(quality / 100 * 256), clamped between 2 and 256, passed to UPNG as a color count [3]. Search integer quality 1 to 100 through that map and you can address at most 100 of the 255 palette sizes between 2 and 256, so roughly 155 of them are unreachable [17]. The lower clamp never fires: quality 1 rounds to 3 colors [18]. A two-color PNG cannot be expressed through the proxy at all [18]. And each quality step moves the palette by 2.56 colors, so quality 1 to quality 2 takes the palette from 3 to 5 [19], which is a near doubling for one notch of a slider at the exact end of the range where a user is fighting for bytes.

On threading, the supplied text asserts rather than shows: leaving the main thread is listed as one of the three less-obvious parts of the implementation [5], and the spinner problem is named early [2], but the material stops mid-sentence in the PNG section. The numbers carry it regardless. Eight synchronous full-image encodes on the UI thread are eight windows in which a phone stops responding to touch.

What to watch

  • Whether the PNG path drops the quality proxy and searches palette size directly, which would recover the roughly 155 colour counts the current mapping cannot reach.
  • Whether the published implementation shows the worker-thread argument the writeup promises, or only asserts it.
  • Whether the search reports the quality or palette size it settled on, which is the only way a user can tell a best fit from a smallest-seen fallback.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories