Skip to content

feat(codecs): png:colors and png:dither, palette reduction through quantizr - #52

Merged
vlad-salone merged 2 commits into
mainfrom
feat/png-palette
Oct 4, 2026
Merged

vlad-salone merged 2 commits into
mainfrom
feat/png-palette

Conversation

@vlad-salone

Copy link
Copy Markdown
Collaborator

What and why

-x png:colors=<N> is here, what rimage had as --quantization: the first item of ROADMAP.md, built as ADR-0012 decided. All five of its action items are ticked.

# at most 256 colours, as pngquant writes them. lossy: --json says "lossless": false
sqzer logo.png -f png -x png:colors=256
# fewer colours, half the dithering
sqzer logo.png -f png -x png:colors=32 -x png:dither=50
png:colors=<2..=256>    `quantizr` reduces the image inside `OxipngEncoder`, which gets an indexed image.
                        an image that already has that few colours is written exactly, at its own depth
png:dither=<0..=100>    error diffusion, 100 by default. needs `png:colors`
Encoder::exact          a backend says when its options give samples up at a lossless target.
                        `oxipng` does for `png:colors` and `png:optimize_alpha=true`
Output::lossless        the result, read by the `--json` record and by the npm package
{quality}               renders `lossy` in a `--template` for such a run

A lossless target next to such an option is refused: exit 2 when -f names the format, at the image when the format is picked per image, in a dry run too.

$ sqzer in.webp -f png --lossless -x png:colors=16
error: lossless output contradicts 'png:colors=16', which gives samples up
  drop '--lossless' (or '--preset lossless'), or the option

Note: this breaks --lossless -x png:optimize_alpha=true, which 0.3 accepts. The option changes the colour under fully transparent pixels and was reported as lossless. Dropping --lossless writes the same file.

What the action items asked to be measured:

same bytes       the release CLI and the wasm32 package, six inputs at four settings: 24 files of 24
memory           5712 x 4284, -j 1, cgroup Memory peak: 344.4M for --lossless, 282.9M for png:colors=256.
                 the budget reserves 783 MB for one encode of that size, so nothing changes
npm package      8,082,215 to 8,124,452 bytes, 1,807,126 to 1,820,965 over brotli: 0.8 % more to download
golden scores    68.3 at 256 colours, -49.7 at 16, in both tables

The score at 16 is below zero and not broken: sixteen colours dithered over the 48 x 32 gradient are mostly noise to the metric. On a screenshot the same setting scores in the sixties. It is a regression check, as the webpx row is.

Where it departs from the record, each noted on its action item:

  • No clarify entry in deny.toml. cargo deny 0.20.2 reads the MIT text from the licence file by itself, and an entry with a wrong hash was ignored while the check stayed green, so it pinned nothing. deny.toml is untouched.
  • png:colors=off is accepted, since every listed option has to take its documented default.
  • Sqzer::lossy_options is new and public: it names the options an encoder is not exact under, so the facade, the CLI and the dry run refuse with the same words.

The test that exact is truthful runs over every option of every encoder with a lossless mode. Under the native tier it also holds webp:alpha_quality, webp:sharp_yuv and jxl:container to ADR-0012 D4, which had taken them from libwebp's documentation: all three keep every sample.

Note: a palette is not always smaller. A dithered 16-colour version of a smooth gradient compresses worse than the gradient does, because dithering defeats PNG's filters. The option does what it is asked and does not fall back.

New dependencies

quantizr 1.4.3, MIT, no dependencies of its own. Its manifest names a licence file, the MIT text, and no SPDX expression; cargo deny check passes under the current deny.toml.

Checklist

  • CHANGELOG.md has an entry under ## Unreleased, if behaviour changes
  • a new ADR has its row in docs/adr/README.md and its entry in .greptile/files.json, if there is one (ADR-0012, merged in docs(adr): ADR-0012, palette reduction for PNG through quantizr #51)
  • tests cover the change; golden scores are unchanged, or the PR says why they moved (two rows added, none moved)
  • cargo fmt, cargo clippy --all-features, cargo test, cargo doc and cargo deny check pass locally
cargo nextest run --workspace, native tier     442 passed
cargo nextest run --workspace, portable        386 passed
wasm-pack test --node                          16 passed
crates/sqzer-wasm/pack.sh                      sqzer@0.3.0 works from its tarball

🤖 Generated with Claude Code

…antizr

ADR-0012. `-x png:colors=<2..=256>` reduces a PNG to a palette inside
`OxipngEncoder`, `png:dither` sets the error diffusion, and an image that
already has that few colours is written exactly at its own depth.

`Encoder::exact` says when an option gives samples up at a lossless
target, `Output::lossless` carries the result into the CLI record and the
npm package, and a lossless target next to such an option is refused.
`png:optimize_alpha=true` is one of those options.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[High risk] Adds PNG palette quantization with new codec options and API.

The PR appears functionally safe to merge, but its README comment must satisfy the repository’s user-facing prose requirement first.

Summary

The PR adds PNG palette reduction and dithering, reports when encoder options give up samples, and refuses those options with an explicit lossless target.

  • It carries the result through CLI naming and JSON records and the wasm output.
  • The combined-options fix addresses the previously reported losslessness error.
  • One new README comment needs to follow the repository’s flag-formatting instruction.

Reviews (2) · Last reviewed commit: "fix(sqzer): options that are lossy only ..."

Comment thread crates/sqzer/src/lib.rs Resolved
Comment thread crates/sqzer-cli/src/job.rs Resolved
`lossy_options` asked `exact` about each option alone, so an encoder
that gives samples up only under a combination came back with nothing
named, and the output was reported lossless. When the encoder is not
exact and no single option is to blame, every option set is named.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vlad-salone
vlad-salone merged commit 7fa0007 into main Oct 4, 2026
23 checks passed
@vlad-salone
vlad-salone deleted the feat/png-palette branch October 4, 2026 11:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant