diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 6573e30ebe7..eb8d515f3ea 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -377,3 +377,28 @@ 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