Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit be2e5d7

Browse files
authored
Merge pull request #17895 from mpetroff/pdf-indexed-color
Use indexed color for PNG images in PDF files when possible
2 parents 56ca51f + c9599a2 commit be2e5d7

File tree

2 files changed

+45
-12
lines changed

2 files changed

+45
-12
lines changed

lib/matplotlib/backends/backend_pdf.py

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1521,28 +1521,34 @@ def _unpack(self, im):
15211521
alpha = None
15221522
return rgb, alpha
15231523

1524-
def _writePng(self, data):
1524+
def _writePng(self, img):
15251525
"""
1526-
Write the image *data* into the pdf file using png
1526+
Write the image *img* into the pdf file using png
15271527
predictors with Flate compression.
15281528
"""
15291529
buffer = BytesIO()
1530-
if data.shape[-1] == 1:
1531-
data = data.squeeze(axis=-1)
1532-
Image.fromarray(data).save(buffer, format="png")
1530+
img.save(buffer, format="png")
15331531
buffer.seek(8)
1532+
png_data = b''
1533+
bit_depth = palette = None
15341534
while True:
15351535
length, type = struct.unpack(b'!L4s', buffer.read(8))
1536-
if type == b'IDAT':
1536+
if type in [b'IHDR', b'PLTE', b'IDAT']:
15371537
data = buffer.read(length)
15381538
if len(data) != length:
15391539
raise RuntimeError("truncated data")
1540-
self.currentstream.write(data)
1540+
if type == b'IHDR':
1541+
bit_depth = int(data[8])
1542+
elif type == b'PLTE':
1543+
palette = data
1544+
elif type == b'IDAT':
1545+
png_data += data
15411546
elif type == b'IEND':
15421547
break
15431548
else:
15441549
buffer.seek(length, 1)
15451550
buffer.seek(4, 1) # skip CRC
1551+
return png_data, bit_depth, palette
15461552

15471553
def _writeImg(self, data, id, smask=None):
15481554
"""
@@ -1551,17 +1557,40 @@ def _writeImg(self, data, id, smask=None):
15511557
(alpha channel) *smask*, which should be either None or a ``(height,
15521558
width, 1)`` array.
15531559
"""
1554-
height, width, colors = data.shape
1560+
height, width, color_channels = data.shape
15551561
obj = {'Type': Name('XObject'),
15561562
'Subtype': Name('Image'),
15571563
'Width': width,
15581564
'Height': height,
1559-
'ColorSpace': Name({1: 'DeviceGray', 3: 'DeviceRGB'}[colors]),
1565+
'ColorSpace': Name({1: 'DeviceGray',
1566+
3: 'DeviceRGB'}[color_channels]),
15601567
'BitsPerComponent': 8}
15611568
if smask:
15621569
obj['SMask'] = smask
15631570
if mpl.rcParams['pdf.compression']:
1564-
png = {'Predictor': 10, 'Colors': colors, 'Columns': width}
1571+
if data.shape[-1] == 1:
1572+
data = data.squeeze(axis=-1)
1573+
img = Image.fromarray(data)
1574+
img_colors = img.getcolors(maxcolors=256)
1575+
if color_channels == 3 and img_colors is not None:
1576+
# Convert to indexed color if there are 256 colors or fewer
1577+
# This can significantly reduce the file size
1578+
num_colors = len(img_colors)
1579+
img = img.convert(mode='P', dither=Image.NONE,
1580+
palette=Image.ADAPTIVE, colors=num_colors)
1581+
png_data, bit_depth, palette = self._writePng(img)
1582+
if bit_depth is None or palette is None:
1583+
raise RuntimeError("invalid PNG header")
1584+
palette = palette[:num_colors * 3] # Trim padding
1585+
palette = pdfRepr(palette)
1586+
obj['ColorSpace'] = Verbatim(b'[/Indexed /DeviceRGB '
1587+
+ str(num_colors - 1).encode()
1588+
+ b' ' + palette + b']')
1589+
obj['BitsPerComponent'] = bit_depth
1590+
color_channels = 1
1591+
else:
1592+
png_data, _, _ = self._writePng(img)
1593+
png = {'Predictor': 10, 'Colors': color_channels, 'Columns': width}
15651594
else:
15661595
png = None
15671596
self.beginStream(
@@ -1571,7 +1600,7 @@ def _writeImg(self, data, id, smask=None):
15711600
png=png
15721601
)
15731602
if png:
1574-
self._writePng(data)
1603+
self.currentstream.write(png_data)
15751604
else:
15761605
self.currentstream.write(data.tobytes())
15771606
self.endStream()

lib/matplotlib/tests/test_image.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -732,7 +732,11 @@ def test_log_scale_image():
732732
ax.set(yscale='log')
733733

734734

735-
@image_comparison(['rotate_image'], remove_text=True)
735+
# Increased tolerance is needed for PDF test to avoid failure. After the PDF
736+
# backend was modified to use indexed color, there are ten pixels that differ
737+
# due to how the subpixel calculation is done when converting the PDF files to
738+
# PNG images.
739+
@image_comparison(['rotate_image'], remove_text=True, tol=0.35)
736740
def test_rotate_image():
737741
delta = 0.25
738742
x = y = np.arange(-3.0, 3.0, delta)

0 commit comments

Comments
 (0)