@@ -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 ()
0 commit comments