@@ -1521,28 +1521,34 @@ def _unpack(self, im):
1521
1521
alpha = None
1522
1522
return rgb , alpha
1523
1523
1524
- def _writePng (self , data ):
1524
+ def _writePng (self , img ):
1525
1525
"""
1526
- Write the image *data * into the pdf file using png
1526
+ Write the image *img * into the pdf file using png
1527
1527
predictors with Flate compression.
1528
1528
"""
1529
1529
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" )
1533
1531
buffer .seek (8 )
1532
+ png_data = b''
1533
+ bit_depth = palette = None
1534
1534
while True :
1535
1535
length , type = struct .unpack (b'!L4s' , buffer .read (8 ))
1536
- if type == b' IDAT' :
1536
+ if type in [ b'IHDR' , b'PLTE' , b' IDAT'] :
1537
1537
data = buffer .read (length )
1538
1538
if len (data ) != length :
1539
1539
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
1541
1546
elif type == b'IEND' :
1542
1547
break
1543
1548
else :
1544
1549
buffer .seek (length , 1 )
1545
1550
buffer .seek (4 , 1 ) # skip CRC
1551
+ return png_data , bit_depth , palette
1546
1552
1547
1553
def _writeImg (self , data , id , smask = None ):
1548
1554
"""
@@ -1551,17 +1557,40 @@ def _writeImg(self, data, id, smask=None):
1551
1557
(alpha channel) *smask*, which should be either None or a ``(height,
1552
1558
width, 1)`` array.
1553
1559
"""
1554
- height , width , colors = data .shape
1560
+ height , width , color_channels = data .shape
1555
1561
obj = {'Type' : Name ('XObject' ),
1556
1562
'Subtype' : Name ('Image' ),
1557
1563
'Width' : width ,
1558
1564
'Height' : height ,
1559
- 'ColorSpace' : Name ({1 : 'DeviceGray' , 3 : 'DeviceRGB' }[colors ]),
1565
+ 'ColorSpace' : Name ({1 : 'DeviceGray' ,
1566
+ 3 : 'DeviceRGB' }[color_channels ]),
1560
1567
'BitsPerComponent' : 8 }
1561
1568
if smask :
1562
1569
obj ['SMask' ] = smask
1563
1570
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 }
1565
1594
else :
1566
1595
png = None
1567
1596
self .beginStream (
@@ -1571,7 +1600,7 @@ def _writeImg(self, data, id, smask=None):
1571
1600
png = png
1572
1601
)
1573
1602
if png :
1574
- self ._writePng ( data )
1603
+ self .currentstream . write ( png_data )
1575
1604
else :
1576
1605
self .currentstream .write (data .tobytes ())
1577
1606
self .endStream ()
0 commit comments