deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

canvas.toBlob ignores its quality argument for PNG, and palette quantization is the fix

A dev.to post explains that canvas.toBlob() silently ignores its quality argument for PNG output, and that real size savings come from palette quantization with UPNG.js rather than re-encoding.

canvas.toBlob ignores its quality argument for PNG, and palette quantization is the fix

A compressor that compressed nothing

A developer who runs a small browser-based file converter has described a quiet bug in the tool's PNG compression feature. The code drew an image onto a canvas and exported it with canvas.toBlob(callback, 'image/png', 0.8), passing 0.8 in the expectation that it controlled compression quality. The output came back roughly the same size as the input, and sometimes larger. As a post on dev.io... rather, as the post on dev.to explains, the quality argument is simply not used for PNG.

The quality argument only applies to lossy formats

According to the post, both toBlob() and toDataURL() honor the quality parameter only for lossy formats such as image/jpeg and image/webp. For image/png the browser discards it and emits whatever its built-in PNG encoder produces, with no control over row filters or the DEFLATE compression level. Re-encoding a PNG this way gains almost nothing when the source file was already written by a competent encoder.

Where PNG savings actually come from

The post breaks a PNG into three layers — a grid of pixels, a per-row filter, and DEFLATE compression on top — and identifies two realistic ways to shrink the file:

  • Lossless re-encoding: trying better filters and stronger DEFLATE settings. The pixel data is unchanged, but gains are modest, typically in the single digits to low double digits as a percentage.
  • Palette quantization: reducing a 32-bit RGBA image to an indexed palette of up to 256 colors while preserving alpha. This is lossy, but for screenshots, logos, icons, and other UI graphics the difference is usually invisible, and the size drop is far larger.

The author notes that services like TinyPNG owe most of their savings to the second strategy.

Doing it in the browser

The replacement approach uses UPNG.js, a compact PNG codec written by the creator of Photopea, together with pako for DEFLATE. The workflow loads the file into an Image element, draws it to a canvas, pulls raw pixels with getImageData(), and hands the buffer to UPNG.encode(), which accepts a color count: 0 for lossless output, 256 to quantize to a palette.

A few implementation details matter. getImageData() returns non-premultiplied RGBA, which is exactly what UPNG expects, so transparency survives the round trip. UPNG.encode() may take ownership of the buffer it receives, so the code passes a fresh copy (rgba.slice(0)) when it encodes twice. The final step compares all candidates and falls back to the original file whenever none is smaller, so the tool never returns output that grew.

The numbers

The author tested a deliberately difficult case: a 1000×700 synthetic image combining a noise-heavy gradient and a strip of transparency at the top. The original weighed 1.50 MB. Lossless re-encoding produced 1.43 MB, about 5 percent smaller. Quantizing to a 256-color palette produced 231 KB, an 85 percent reduction. The author stresses that these figures illustrate the gap between the two strategies rather than serving as a benchmark, and that real-world results will vary.

When not to quantize

Palette reduction can introduce banding in smooth gradients and photographs. The post recommends keeping a lossless mode for users who need pixel-exact output, converting photographs to JPEG or WebP instead — canvas.toBlob(cb, 'image/webp', 0.8) does respect the quality value, and WebP supports transparency — and scaling down images that are larger than they will ever be displayed. The author's interface ended up with two modes: one that tries both encodings and keeps the smaller result, and one that stays strictly lossless.

Why it matters

Because everything runs client-side, no image data ever leaves the user's machine — no upload, no server to trust, and nothing to delete afterwards. The author cites this as the original motivation for building the tool and a real advantage when handling screenshots with private information or unreleased designs. More broadly, the post is a useful reminder that browser APIs can fail without any signal: the quality argument is accepted rather than rejected for PNG, so the bug produces no error and no warning. Any web application that claims to compress PNGs through a canvas may be shipping a silent no-op, and quantization — offered alongside a lossless fallback — is where the actual savings live.

  • #png
  • #javascript
  • #canvas-api
  • #image-compression
  • #browser