feat(png)!: migrate PNG encode to gamut-png - #47
Merged
Conversation
PNG encoding moves from zune-png to gamut_png::PngEncoder. The 8-bit path keeps the existing 16->8 high-byte conversion into ImageRef<Rgb8>; the 16-bit path hands the RgbImage samples to ImageRef<Rgb16> directly, dropping the manual big-endian packing (gamut serialises 16-bit PNG itself). Decode stays on zune-png until the gamut decoder migration (#32) — encode-only swap, no dual encode backends. Metadata now embeds through the encoder itself — with_exif (bare TIFF from the gamut-exif builder), with_icc_profile, with_xmp — replacing the img-parts PNG chunk muxing on this path; the PNG path no longer pulls container-embed or zune-runtime. A new metadata round-trip test asserts eXIf/iCCP/iTXt chunk presence plus exact 16-bit pixel round-trip through the zune decoder. Config surface: ZunePngEncodeConfig -> PngEncodeConfig, exposing exactly gamut's encoder knobs (compression level Store/Fast/Default/Best, filter strategy None/Fixed/MinSumAbs/BruteForce, auto_reduce) — no invented options. EncodeOptions::PngZune -> PngGamut; encode codec id png/zune -> png/gamut. Feature graph: tier-4 collapses, png-encode = dep:gamut-png; png-encode-zune deleted; zune-png remains only behind png-decode-zune. Baseline vs zune (1024x1024 synthetic 16-bit gradient, release, avg of 5, default options; ref gamut#224): output size 6,302,493 -> 26,245 bytes (-99.6%; zune 0.5 emitted essentially uncompressed output), encode time 20.5ms -> 96.3ms (16-bit) and 2.6ms -> 47.3ms (8-bit) — the size win costs real filtering+DEFLATE time; Store/Fast levels are exposed for speed-first callers. Gradient content exaggerates the size ratio. BREAKING CHANGE: ZunePngEncodeConfig/EncodeOptions::PngZune renamed; PNG encode output bytes change (properly compressed).
This was referenced Jul 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #23.
Stacked on #46; merge order #42 → #44 → #45 → #46 → this.
What is Delivered
gamut_png::PngEncoder(decode stays zune-png — that is Migrate PNG decode to gamut-png #32). 8-bit keeps the existing 16→8 conversion intoImageRef<Rgb8>; 16-bit feeds samples straight toImageRef<Rgb16>, deleting the manual big-endian packing. No dual encode backends:png-encode-zuneis gone,png-encode = dep:gamut-png, zune-png remains only behindpng-decode-zune(grep-verified).with_exif(bare TIFF from the Migrate metadata stack to gamut-metadata #19 gamut-exif builder),with_icc_profile,with_xmp— replacing img-parts PNG chunk muxing on this path. The PNG path dropscontainer-embed/zune-runtimeentirely.PngEncodeConfigreplacesZunePngEncodeConfig, exposing exactly gamut's knobs (compression Store/Fast/Default/Best; filter None/Fixed/MinSumAbs/BruteForce;auto_reduce) — nothing invented, per the intentional-API rule.EncodeOptions::PngZune→PngGamut; codec idpng/zune→png/gamut.Bench vs the zune baseline (issue requirement; ref gamut#224)
1024×1024 synthetic 16-bit gradient, release, avg of 5, defaults:
zune 0.5 was emitting essentially uncompressed output; gamut's filtering + DEFLATE costs real time for a massive size win. Gradient content exaggerates the ratio;
Store/Fastare exposed for speed-first callers.Verification Done
Run independently after implementation:
cargo test --workspace384/0;--features full533/0 (fixtures ran);--features serde366/0; clippy workspace + full-features-D warningsboth 0 issues;--no-default-features --features pngstandalone builds clean; feature-graph grep confirms zune-png is decode-only. lefthook green.