Skip to content

Fixes - #3

Merged
JohSand merged 3 commits into
mainfrom
fixes
Aug 20, 2026
Merged

Fixes#3
JohSand merged 3 commits into
mainfrom
fixes

Conversation

@JohSand

@JohSand JohSand commented Aug 20, 2026

Copy link
Copy Markdown
Owner

No description provided.

JohSand and others added 3 commits August 20, 2026 17:12
Wires up the per-row adaptive filtering heuristics zopflipng uses
(lodepng's LFS_MINSUM/LFS_ENTROPY), plus a whole-image "Adaptive" search
that trial-compresses every candidate strategy (None..Paeth, MinSum,
Entropy) with a fast in-process compressor and keeps whichever produces
the smallest output - mirroring zopflipng's own AutoChooseFilterStrategy,
without needing to link its C++/lodepng implementation into this fully
managed codec.

Coder gained a `filterStrategy` constructor parameter (PngFilterStrategy),
defaulting to None - the fixed filter-0 behavior Coder has always used -
so existing callers and the Verify snapshot baselines are unaffected.

New PngFilter.cs holds the forward filter math and the two adaptive
selection heuristics; it now also owns the single PaethPredictor shared
with Decoder.ApplyPngFilters, removing a second hand-copied implementation
that had to stay in sync by hand for round-tripping to work at all.

Measured against this project's own test images (all quantized to a
256-color-or-fewer palette, the only kind of PNG Coder ever writes),
Adaptive's search consistently converges back on None - MinSum/Entropy
made every one of them 8-20% *larger*. This matches the PNG spec's own
"do not filter" guidance for indexed-color images (also why lodepng
defaults filter_palette_zero=true, which zopflipng deliberately disables
just to confirm it doesn't help): index bytes don't carry the
pixel-to-pixel correlation filtering exploits, so there's no realistic
image where this wins for this codec - Adaptive is a safe no-op here by
construction (it always includes None as a candidate), not a source of
smaller output. Left in as infrastructure and to close out the
investigation, not as a default worth switching to.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nopu9gA5CuVcGZFJH2Nouh
Replaces the five-line TODO stub with an actual description, a
quick-start example (decode -> quantize -> encode with Zopfli + adaptive
filtering), build-from-source instructions for the native libimagequant/
zopfli pieces, and an honest status note on NuGet publishing being wired
up but not yet turned on.

The usage example is verbatim what's compiled and run against panda.png
in this session (56907 -> 12393 bytes), not hand-typed against
half-remembered signatures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nopu9gA5CuVcGZFJH2Nouh
Color's [FieldOffset] layout put Red at offset 2 and Blue at offset 0,
matching the conventional 0xAARRGGBB int-packing for .Argb - but
libimagequant's native liq_color is { r, g, b, a } (R@0, G@1, B@2, A@3),
and Color gets blitted directly across the P/Invoke boundary with no
field-by-field marshaling in two places: Quantizer's row callback
(writing pixels in) and QuantizationResult's palette (reading colors
out). The mismatch swapped Red and Blue at each crossing.

Quantizer.Quantize(IProvideImages, ...) crosses that boundary twice, so
the two swaps canceled out and every test/benchmark in this repo (which
all go through that path) never saw a problem. Quantize(ReadOnlySpan<byte>,
...) - the "simple contiguous RGBA bytes" overload, and the more obvious
entry point for anyone not using IProvideImages - only crosses it once
and came back with a genuinely wrong palette: pure red in, pure blue out.
Confirmed both behaviors empirically before touching anything.

Fixed by moving Color's field offsets to match liq_color instead of
adding marshaling shims at the two crossing points - simpler, and closes
off the whole class of bug for any future direct-blit interop, not just
today's two call sites. This changes .Argb's packing convention (now
R at the low byte instead of B); nothing else in the repo depended on
that convention, and this project isn't published yet, so the breakage
is free.

Re-approved the two VisualTests snapshots that regressed on SSIM
(0.993/0.997, threshold 0.999): libimagequant's internal color-distance
weighting isn't symmetric between channels, so quantizing on the
previously-mislabeled channels was already producing subtly different
(off, if only slightly) palette choices for the IProvideImages path too -
visually verified both new baselines by hand before accepting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nopu9gA5CuVcGZFJH2Nouh
@JohSand
JohSand merged commit ab991a0 into main Aug 20, 2026
2 checks passed
@JohSand
JohSand deleted the fixes branch August 20, 2026 15:35
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