From 145761cb2d346a2383b26ffbfe52c16634b1ca25 Mon Sep 17 00:00:00 2001 From: Luv Goel Date: Fri, 23 Oct 2026 08:55:07 +0530 Subject: [PATCH 1/2] convert: honor dither argument with ADAPTIVE palette (#5836) Image.convert("P", palette=ADAPTIVE, colors=N) built the palette via quantize(), which maps each pixel to the nearest palette entry without dithering, then returned early -- silently dropping the dither argument. Build the adaptive palette first, then re-convert with the requested dither so Floyd-Steinberg is applied. Add a regression test asserting NONE vs FLOYDSTEINBERG produce different histograms. --- Tests/test_image_convert.py | 17 +++++++++++++++++ src/PIL/Image.py | 8 +++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 6573e30ebe7..b522c5f1cc3 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -377,3 +377,20 @@ def test_matrix_identity() -> None: # Test list converted_im = im.convert("RGB", list(identity_matrix)) assert_image_equal(converted_im, im) + +def test_adaptive_convert_honors_dither(tmp_path: Path) -> None: + # Regression test for #5836: convert("P", palette=ADAPTIVE, colors=N, + # dither=...) must apply dithering, not just nearest-color mapping. + im = hopper("RGB") + + no_dither = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=8, dither=Image.Dither.NONE) + with_dither = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=8, dither=Image.Dither.FLOYDSTEINBERG) + + # Both should produce valid palette images with the same palette size. + assert no_dither.mode == "P" + assert with_dither.mode == "P" + assert len(no_dither.getpalette()) == len(with_dither.getpalette()) == 24 + + # Dithered and non-dithered results must differ. Histograms should spread + # differently because dithering distributes error across neighboring pixels. + assert no_dither.histogram() != with_dither.histogram() diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f8a89cbb2eb..f6a5a5b7190 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1204,7 +1204,13 @@ def convert_transparency( raise ValueError(msg) if mode == "P" and palette == Palette.ADAPTIVE: - im = self.im.quantize(colors) + # quantize() maps each pixel to the nearest palette entry without + # dithering, so it silently drops the dither argument. Build the + # adaptive palette first, then re-convert with dither so the + # dither argument is honored (see #5836). + adaptive = self.im.quantize(colors) + palette_im = self._new(adaptive) + im = self.im.convert(mode, dither, palette_im.im) new_im = self._new(im) from . import ImagePalette From 0e8fbbe7aca3c6e38585c0fe82e7e64e07928032 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 03:26:05 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_image_convert.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index b522c5f1cc3..eb8d515f3ea 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -378,13 +378,21 @@ def test_matrix_identity() -> None: converted_im = im.convert("RGB", list(identity_matrix)) assert_image_equal(converted_im, im) + def test_adaptive_convert_honors_dither(tmp_path: Path) -> None: # Regression test for #5836: convert("P", palette=ADAPTIVE, colors=N, # dither=...) must apply dithering, not just nearest-color mapping. im = hopper("RGB") - no_dither = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=8, dither=Image.Dither.NONE) - with_dither = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=8, dither=Image.Dither.FLOYDSTEINBERG) + no_dither = im.convert( + "P", palette=Image.Palette.ADAPTIVE, colors=8, dither=Image.Dither.NONE + ) + with_dither = im.convert( + "P", + palette=Image.Palette.ADAPTIVE, + colors=8, + dither=Image.Dither.FLOYDSTEINBERG, + ) # Both should produce valid palette images with the same palette size. assert no_dither.mode == "P"