Skip to content

[V4] Keep decoded palette colors exact when encoding - #3274

Merged
JimBobSquarePants merged 2 commits into
release/4.1.xfrom
js/v4-quantizer-exact-palette
Nov 13, 2026
Merged

JimBobSquarePants merged 2 commits into
release/4.1.xfrom
js/v4-quantizer-exact-palette

Conversation

@JimBobSquarePants

Copy link
Copy Markdown
Member

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

This PR fixes three faults that change colors when an encoder reuses a palette or a configured quantizer.

Coarse matching of decoded palettes

The PNG, GIF and icon encoders reuse a decoded palette without dithering, to keep the original colors. They used the default ColorMatchingMode.Coarse. The coarse cache keeps 5 bits of each channel, so palette colors that share a bucket map to one entry. For example, the red values 8 and 12 map to the same entry. These paths now use ColorMatchingMode.Exact:

  • PngEncoderCore: the palette from PngMetadata.ColorTable.
  • GifEncoderCore: the global color table, and the local color table with and without a reserved transparent index.
  • IconEncoderCore: the palette from the icon frame metadata.

GIF first frame

When the GIF encoder reuses the decoded global color table, the first frame quantized with the encoder options, not the options of the palette quantizer. So the first frame dithered against the decoded palette, although the comment says that it does not. The palette branch now replaces the options, so the first frame uses them.

BMP 1-, 2- and 4-bit options

These paths built new quantizer options from Dither and DitherScale only. They dropped ColorMatchingMode, TransparentColorMode and TransparencyThreshold of the configured quantizer. They now clone the quantizer options and set only MaxColors.

Tests

  • NearColorPalette: a two-color image whose colors share one coarse bucket. Only exact matching keeps both colors.
  • PNG, GIF (global and local table) and ICO round trips with a palette from the metadata. Each one checks exact pixels.
  • A 1-bit BMP with an exact PaletteQuantizer as the configured quantizer.

Reference images

The GifEncoder_CanDecode_AndEncode_Issue2866 reference frames are updated. The fixture uses local color tables, so coarse matching changed some colors before. I measured the mean RGB error against the decoded source on the 15 compared frames:

  • Frame 0 goes from 1.4124 to 0.0000.
  • Every other compared frame has less error.
  • The summed error goes from 22.99 to 10.39.

The tests pass on net10.0 and net8.0.

The PNG, GIF and icon encoders reuse a decoded palette without dithering to
keep the original colors. They used the default coarse color matching, which
maps colors that share a 5-bit bucket to one entry. They now use exact
matching.

The first GIF frame quantized with the encoder options instead of the options
of the decoded global palette, so it dithered against that palette. It now
uses the palette options.

The 1-, 2- and 4-bit BMP paths built new quantizer options from the dither
settings only. They dropped the color matching mode, the transparent color
mode and the transparency threshold of the configured quantizer. They now
clone the quantizer options and set only the color count.

The Issue2866 reference frames are updated. Against the decoded source, the
new output has less error on each compared frame, and frame 0 is exact.
@JimBobSquarePants JimBobSquarePants changed the title Keep decoded palette colors exact when encoding [V4] Keep decoded palette colors exact when encoding Nov 13, 2026
@JimBobSquarePants
JimBobSquarePants merged commit 29151c4 into release/4.1.x Nov 13, 2026
11 checks passed
@JimBobSquarePants
JimBobSquarePants deleted the js/v4-quantizer-exact-palette branch November 13, 2026 04:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant