Build guides

Building a compressor you give a file size, not a quality slider

Every image compressor has a quality slider from 1 to 100. Nobody wants a quality of 63. They want a photo under 100 KB because a form refuses anything larger. This tool inverts the control: you state the outcome, and the engine searches for the parameters. That search is the single most reused piece of code on the site, and it knows nothing about images.

What it is built with

  • Canvas 2D and `createImageBitmap` for decoding and rescaling
  • `canvas.convertToBlob` / `toBlob` for JPEG and WebP encoding
  • OffscreenCanvas in a Web Worker where the browser supports it
  • No third-party image library: the browser already has the codecs

Step 1: decide what the user is actually asking for

A quality parameter is the encoder’s unit, not the user’s. The relationship between quality and file size depends on the image: a flat screenshot at quality 80 might be 40 KB while a detailed photograph at the same setting is 900 KB. Handing someone a slider is handing them a guessing game with a slow feedback loop.

So the interface takes a budget, and the code solves for the rest. The consequence is that the compressor has to encode the image several times, which is only tolerable because it happens locally with no upload between attempts.

Step 2: make the search know nothing about images

The engine is handed an `encode(params)` function and a byte budget, and returns the largest output that still fits. It has no idea whether it is driving a JPEG encoder or ffmpeg. That is what lets the video tool reuse it.

src/core/target-size/index.ts, the interface
export interface TargetSizeOptions {
  /** Codec bridge. Must be pure w.r.t. its params and honour `signal`. */
  encode: (params: EncodeParams, signal: AbortSignal) => Promise<Uint8Array>;
  /** Hard byte budget the output must fit under (e.g. 100 * 1024). */
  budgetBytes: number;
  searchSpace: {
    quality: Range;
    /** Omitted means never rescale. */
    scale?: Range;
  };
  strategy: 'binary' | 'analytic-then-verify';
  maxIterations?: number;
}

Step 4: report what it had to give up

A compressor that silently halves the dimensions of a photo to hit a budget has done something the user might not have accepted. The result carries a record of what was sacrificed, so the interface can say "scaled to 78%" instead of quietly handing back a smaller picture.

The same structure covers honest failure. If nothing in the search space fits, the engine returns the smallest output it managed with `withinBudget: false`, rather than throwing. The user gets the best available answer and an accurate statement that it did not fit.

Step 5: the payoff, on a completely different codec

Video cannot binary-search. A single encode of a two-minute clip takes long enough that a dozen attempts is not an option. But the required bitrate is calculable: a byte budget divided by a duration is a bitrate, so you can compute a first guess analytically and then verify it with one corrective re-encode.

That is a second strategy inside the same engine, selected by a string, sharing all the budget accounting and result reporting. Because the search was written without any knowledge of images, adding video meant adding a strategy rather than writing a second engine.

What went wrong

The most useful part of any build write-up, and the part usually left out.

  • Binary searching only the interior of the quality range silently fails for images whose only fit is at the minimum. Probe the boundaries first.
  • The search must be abortable. Each iteration is a full encode, and a user who changes the target halfway through should not wait for the old search to finish; every probe checks the abort signal before starting.
  • A batch of large photos will exhaust memory if every decode is held at once. Decoding, searching and releasing one image at a time is slower to write and is the difference between working and crashing on a phone.
  • Canvas silently returns a PNG when asked for a format it cannot encode. The output extension is therefore not evidence of the output format, and the tool checks the magic bytes rather than trusting the request.

Use the tool: Compress an image to an exact size · How YappyKit works