diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index cf9ceca0e57b..aae5007b55c8 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -1731,39 +1731,43 @@ def _writeImg(self, data, id, smask=None): 'Subtype': Name('Image'), 'Width': width, 'Height': height, - 'ColorSpace': Name({1: 'DeviceGray', - 3: 'DeviceRGB'}[color_channels]), + 'ColorSpace': Name({1: 'DeviceGray', 3: 'DeviceRGB'}[color_channels]), 'BitsPerComponent': 8} if smask: obj['SMask'] = smask if mpl.rcParams['pdf.compression']: if data.shape[-1] == 1: data = data.squeeze(axis=-1) + png = {'Predictor': 10, 'Colors': color_channels, 'Columns': width} img = Image.fromarray(data) img_colors = img.getcolors(maxcolors=256) if color_channels == 3 and img_colors is not None: - # Convert to indexed color if there are 256 colors or fewer - # This can significantly reduce the file size + # Convert to indexed color if there are 256 colors or fewer. This can + # significantly reduce the file size. num_colors = len(img_colors) - # These constants were converted to IntEnums and deprecated in - # Pillow 9.2 - dither = getattr(Image, 'Dither', Image).NONE - pmode = getattr(Image, 'Palette', Image).ADAPTIVE - img = img.convert( - mode='P', dither=dither, palette=pmode, colors=num_colors - ) + palette = np.array([comp for _, color in img_colors for comp in color], + dtype=np.uint8) + palette24 = ((palette[0::3].astype(np.uint32) << 16) | + (palette[1::3].astype(np.uint32) << 8) | + palette[2::3]) + rgb24 = ((data[:, :, 0].astype(np.uint32) << 16) | + (data[:, :, 1].astype(np.uint32) << 8) | + data[:, :, 2]) + indices = np.argsort(palette24).astype(np.uint8) + rgb8 = indices[np.searchsorted(palette24, rgb24, sorter=indices)] + img = Image.fromarray(rgb8, mode='P') + img.putpalette(palette) png_data, bit_depth, palette = self._writePng(img) if bit_depth is None or palette is None: raise RuntimeError("invalid PNG header") - palette = palette[:num_colors * 3] # Trim padding - obj['ColorSpace'] = Verbatim( - b'[/Indexed /DeviceRGB %d %s]' - % (num_colors - 1, pdfRepr(palette))) + palette = palette[:num_colors * 3] # Trim padding; remove for Pillow>=9 + obj['ColorSpace'] = [Name('Indexed'), Name('DeviceRGB'), + num_colors - 1, palette] obj['BitsPerComponent'] = bit_depth - color_channels = 1 + png['Colors'] = 1 + png['BitsPerComponent'] = bit_depth else: png_data, _, _ = self._writePng(img) - png = {'Predictor': 10, 'Colors': color_channels, 'Columns': width} else: png = None self.beginStream( diff --git a/lib/matplotlib/tests/baseline_images/test_agg_filter/agg_filter_alpha.pdf b/lib/matplotlib/tests/baseline_images/test_agg_filter/agg_filter_alpha.pdf index 46275f180bc4..3d9d77f1a8ec 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_agg_filter/agg_filter_alpha.pdf and b/lib/matplotlib/tests/baseline_images/test_agg_filter/agg_filter_alpha.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/imshow.pdf b/lib/matplotlib/tests/baseline_images/test_axes/imshow.pdf index 875868fff1e7..183b072fc312 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/imshow.pdf and b/lib/matplotlib/tests/baseline_images/test_axes/imshow.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.pdf b/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.pdf index 5e2fd6190682..f4bbc73544a5 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.pdf and b/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pdf/grayscale_alpha.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pdf/grayscale_alpha.pdf index 6626c551355e..93e850ca8bdb 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_backend_pdf/grayscale_alpha.pdf and b/lib/matplotlib/tests/baseline_images/test_backend_pdf/grayscale_alpha.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/bbox_image_inverted.pdf b/lib/matplotlib/tests/baseline_images/test_image/bbox_image_inverted.pdf index df07fd91a9c6..e0baa115a6b3 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/bbox_image_inverted.pdf and b/lib/matplotlib/tests/baseline_images/test_image/bbox_image_inverted.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/figimage.pdf b/lib/matplotlib/tests/baseline_images/test_image/figimage.pdf index 53b98a11d5cb..83ed7cef5668 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/figimage.pdf and b/lib/matplotlib/tests/baseline_images/test_image/figimage.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/image_alpha.pdf b/lib/matplotlib/tests/baseline_images/test_image/image_alpha.pdf index 529788574bbf..f5923c481fa7 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/image_alpha.pdf and b/lib/matplotlib/tests/baseline_images/test_image/image_alpha.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/image_interps.pdf b/lib/matplotlib/tests/baseline_images/test_image/image_interps.pdf index d4ee5a70e014..4c0ecd558f42 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/image_interps.pdf and b/lib/matplotlib/tests/baseline_images/test_image/image_interps.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/image_placement.pdf b/lib/matplotlib/tests/baseline_images/test_image/image_placement.pdf index aa021a08e5b0..a86faf3b0f47 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/image_placement.pdf and b/lib/matplotlib/tests/baseline_images/test_image/image_placement.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/image_shift.pdf b/lib/matplotlib/tests/baseline_images/test_image/image_shift.pdf index 3c15129cb5c9..037a145712f9 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/image_shift.pdf and b/lib/matplotlib/tests/baseline_images/test_image/image_shift.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/imshow.pdf b/lib/matplotlib/tests/baseline_images/test_image/imshow.pdf index 4f1fb7db06cf..7e53255f2ebe 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/imshow.pdf and b/lib/matplotlib/tests/baseline_images/test_image/imshow.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.pdf b/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.pdf index 43dbea277522..1d14a9d2f60c 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.pdf and b/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/log_scale_image.pdf b/lib/matplotlib/tests/baseline_images/test_image/log_scale_image.pdf index d1d3ca14dcf7..b338fce6ee5a 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/log_scale_image.pdf and b/lib/matplotlib/tests/baseline_images/test_image/log_scale_image.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/no_interpolation_origin.pdf b/lib/matplotlib/tests/baseline_images/test_image/no_interpolation_origin.pdf index 2125fe7f2f01..9b0edaba007b 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/no_interpolation_origin.pdf and b/lib/matplotlib/tests/baseline_images/test_image/no_interpolation_origin.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/rotate_image.pdf b/lib/matplotlib/tests/baseline_images/test_image/rotate_image.pdf index 7df9f5462c22..e1da2d0cb2d5 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/rotate_image.pdf and b/lib/matplotlib/tests/baseline_images/test_image/rotate_image.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect1.pdf b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect1.pdf index 8adac0a0d262..45fd3b5b2b88 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect1.pdf and b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect1.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.pdf b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.pdf index f13892ba2bc5..1b29bdcd1fc3 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.pdf and b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout5.pdf b/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout5.pdf index bbe561de38e3..7132b252484f 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout5.pdf and b/lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout5.pdf differ diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index 0739f2198a4d..4e56e8a96286 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -131,6 +131,30 @@ def test_composite_image(): assert len(pdf._file._images) == 2 +def test_indexed_image(): + # An image with low color count should compress to a palette-indexed format. + pikepdf = pytest.importorskip('pikepdf') + + data = np.zeros((256, 1, 3), dtype=np.uint8) + data[:, 0, 0] = np.arange(256) # Maximum unique colours for an indexed image. + + rcParams['pdf.compression'] = True + fig = plt.figure() + fig.figimage(data, resize=True) + buf = io.BytesIO() + fig.savefig(buf, format='pdf', dpi='figure') + + with pikepdf.Pdf.open(buf) as pdf: + page, = pdf.pages + image, = page.images.values() + pdf_image = pikepdf.PdfImage(image) + assert pdf_image.indexed + pil_image = pdf_image.as_pil_image() + rgb = np.asarray(pil_image.convert('RGB')) + + np.testing.assert_array_equal(data, rgb) + + def test_savefig_metadata(monkeypatch): pikepdf = pytest.importorskip('pikepdf') monkeypatch.setenv('SOURCE_DATE_EPOCH', '0') diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 0d241d33b2c0..49b8570587fd 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -754,11 +754,7 @@ def test_log_scale_image(): ax.set(yscale='log') -# Increased tolerance is needed for PDF test to avoid failure. After the PDF -# backend was modified to use indexed color, there are ten pixels that differ -# due to how the subpixel calculation is done when converting the PDF files to -# PNG images. -@image_comparison(['rotate_image'], remove_text=True, tol=0.35) +@image_comparison(['rotate_image'], remove_text=True) def test_rotate_image(): delta = 0.25 x = y = np.arange(-3.0, 3.0, delta)