1212import  codecs 
1313import  os 
1414import  re 
15+ import  struct 
1516import  sys 
1617import  time 
1718import  warnings 
4344from  matplotlib .transforms  import  Affine2D , BboxBase 
4445from  matplotlib .path  import  Path 
4546from  matplotlib  import  _path 
47+ from  matplotlib  import  _png 
4648from  matplotlib  import  ttconv 
4749
4850# Overview 
8789
8890# TODOs: 
8991# 
90- # * image compression could be improved (PDF supports png-like compression) 
9192# * encoding of fonts, including mathtext fonts and unicode support 
9293# * TTF support has lots of small TODOs, e.g., how do you know if a font 
9394#   is serif/sans-serif, or symbolic/non-symbolic? 
@@ -334,11 +335,12 @@ class Stream(object):
334335    """ 
335336    __slots__  =  ('id' , 'len' , 'pdfFile' , 'file' , 'compressobj' , 'extra' , 'pos' )
336337
337-     def  __init__ (self , id , len , file , extra = None ):
338+     def  __init__ (self , id , len , file , extra = None ,  png = None ):
338339        """id: object id of stream; len: an unused Reference object for the 
339340        length of the stream, or None (to use a memory buffer); file: 
340341        a PdfFile; extra: a dictionary of extra key-value pairs to 
341-         include in the stream header """ 
342+         include in the stream header; png: if the data is already 
343+         png compressed, the decode parameters""" 
342344        self .id  =  id             # object id 
343345        self .len  =  len           # id of length object 
344346        self .pdfFile  =  file 
@@ -347,10 +349,13 @@ def __init__(self, id, len, file, extra=None):
347349        if  extra  is  None :
348350            self .extra  =  dict ()
349351        else :
350-             self .extra  =  extra 
352+             self .extra  =  extra .copy ()
353+         if  png  is  not   None :
354+             self .extra .update ({'Filter' :      Name ('FlateDecode' ),
355+                                'DecodeParms' : png })
351356
352357        self .pdfFile .recordXref (self .id )
353-         if  rcParams ['pdf.compression' ]:
358+         if  rcParams ['pdf.compression' ]  and   not   png :
354359            self .compressobj  =  zlib .compressobj (rcParams ['pdf.compression' ])
355360        if  self .len  is  None :
356361            self .file  =  BytesIO ()
@@ -583,9 +588,9 @@ def output(self, *data):
583588        self .write (fill ([pdfRepr (x ) for  x  in  data ]))
584589        self .write (b'\n ' )
585590
586-     def  beginStream (self , id , len , extra = None ):
591+     def  beginStream (self , id , len , extra = None ,  png = None ):
587592        assert  self .currentstream  is  None 
588-         self .currentstream  =  Stream (id , len , self , extra )
593+         self .currentstream  =  Stream (id , len , self , extra ,  png )
589594
590595    def  endStream (self ):
591596        if  self .currentstream  is  not   None :
@@ -1247,73 +1252,103 @@ def imageObject(self, image):
12471252        self .images [image ] =  (name , ob )
12481253        return  name 
12491254
1250-     def  _rgb (self , im ):
1251-         h , w , s  =  im .as_rgba_str ()
1255+     def  _unpack (self , im ):
1256+         """ 
1257+         Unpack the image object im into height, width, data, alpha, 
1258+         where data and alpha are HxWx3 (RGB) or HxWx1 (grayscale or alpha) 
1259+         arrays, except alpha is None if the image is fully opaque. 
1260+         """ 
12521261
1262+         h , w , s  =  im .as_rgba_str ()
12531263        rgba  =  np .fromstring (s , np .uint8 )
12541264        rgba .shape  =  (h , w , 4 )
12551265        rgba  =  rgba [::- 1 ]
1256-         rgb  =  rgba [:, :, :3 ]. tostring () 
1257-         a  =  rgba [:, :, 3 ]
1258-         if  np .all (a  ==  255 ):
1266+         rgb  =  rgba [:, :, :3 ]
1267+         alpha  =  rgba [:, :, 3 ][...,  None ]
1268+         if  np .all (alpha  ==  255 ):
12591269            alpha  =  None 
12601270        else :
1261-             alpha  =  a .tostring ()
1262-         return  h , w , rgb , alpha 
1263- 
1264-     def  _gray (self , im , rc = 0.3 , gc = 0.59 , bc = 0.11 ):
1265-         rgbat  =  im .as_rgba_str ()
1266-         rgba  =  np .fromstring (rgbat [2 ], np .uint8 )
1267-         rgba .shape  =  (rgbat [0 ], rgbat [1 ], 4 )
1268-         rgba  =  rgba [::- 1 ]
1269-         rgba_f  =  rgba .astype (np .float32 )
1270-         r  =  rgba_f [:, :, 0 ]
1271-         g  =  rgba_f [:, :, 1 ]
1272-         b  =  rgba_f [:, :, 2 ]
1273-         a  =  rgba [:, :, 3 ]
1274-         if  np .all (a  ==  255 ):
1275-             alpha  =  None 
1271+             alpha  =  np .array (alpha , order = 'C' )
1272+         if  im .is_grayscale :
1273+             r , g , b  =  rgb .astype (np .float32 ).transpose (2 , 0 , 1 )
1274+             gray  =  (0.3  *  r  +  0.59  *  g  +  0.11  *  b ).astype (np .uint8 )[..., None ]
1275+             return  h , w , gray , alpha 
12761276        else :
1277-             alpha  =  a .tostring ()
1278-         gray  =  (r * rc  +  g * gc  +  b * bc ).astype (np .uint8 ).tostring ()
1279-         return  rgbat [0 ], rgbat [1 ], gray , alpha 
1277+             rgb  =  np .array (rgb , order = 'C' )
1278+             return  h , w , rgb , alpha 
12801279
1281-     def  writeImages (self ):
1282-         for  img , pair  in  six .iteritems (self .images ):
1283-             if  img .is_grayscale :
1284-                 height , width , data , adata  =  self ._gray (img )
1280+     def  _writePng (self , data ):
1281+         """ 
1282+         Write the image *data* into the pdf file using png 
1283+         predictors with Flate compression. 
1284+         """ 
1285+ 
1286+         buffer  =  BytesIO ()
1287+         _png .write_png (data , buffer )
1288+         buffer .seek (8 )
1289+         written  =  0 
1290+         header  =  bytearray (8 )
1291+         while  True :
1292+             n  =  buffer .readinto (header )
1293+             assert  n  ==  8 
1294+             length , type  =  struct .unpack (b'!L4s' , bytes (header ))
1295+             if  type  ==  b'IDAT' :
1296+                 data  =  bytearray (length )
1297+                 n  =  buffer .readinto (data )
1298+                 assert  n  ==  length 
1299+                 self .currentstream .write (bytes (data ))
1300+                 written  +=  n 
1301+             elif  type  ==  b'IEND' :
1302+                 break 
12851303            else :
1286-                 height , width , data , adata  =  self ._rgb (img )
1304+                 buffer .seek (length , 1 )
1305+             buffer .seek (4 , 1 )   # skip CRC 
1306+ 
1307+     def  _writeImg (self , data , height , width , grayscale , id , smask = None ):
1308+         """ 
1309+         Write the image *data* of size *height* x *width*, as grayscale 
1310+         if *grayscale* is true and RGB otherwise, as pdf object *id* 
1311+         and with the soft mask (alpha channel) *smask*, which should be 
1312+         either None or a *height* x *width* x 1 array. 
1313+         """ 
12871314
1288-             colorspace  =  'DeviceGray'  if  img .is_grayscale  else  'DeviceRGB' 
1289-             obj  =  {'Type' : Name ('XObject' ),
1290-                    'Subtype' : Name ('Image' ),
1291-                    'Width' : width ,
1292-                    'Height' : height ,
1293-                    'ColorSpace' : Name (colorspace ),
1294-                    'BitsPerComponent' : 8 }
1315+         obj  =  {'Type' :             Name ('XObject' ),
1316+                'Subtype' :          Name ('Image' ),
1317+                'Width' :            width ,
1318+                'Height' :           height ,
1319+                'ColorSpace' :       Name ('DeviceGray'  if  grayscale 
1320+                                         else  'DeviceRGB' ),
1321+                'BitsPerComponent' : 8 }
1322+         if  smask :
1323+             obj ['SMask' ] =  smask 
1324+         if  rcParams ['pdf.compression' ]:
1325+             png  =  {'Predictor' : 10 ,
1326+                    'Colors' :    1  if  grayscale  else  3 ,
1327+                    'Columns' :   width }
1328+         else :
1329+             png  =  None 
1330+         self .beginStream (
1331+             id ,
1332+             self .reserveObject ('length of image stream' ),
1333+             obj ,
1334+             png = png 
1335+             )
1336+         if  png :
1337+             self ._writePng (data )
1338+         else :
1339+             self .currentstream .write (data .tostring ())
1340+         self .endStream ()
12951341
1342+     def  writeImages (self ):
1343+         for  img , pair  in  six .iteritems (self .images ):
1344+             height , width , data , adata  =  self ._unpack (img )
12961345            if  adata  is  not   None :
12971346                smaskObject  =  self .reserveObject ("smask" )
1298-                 self .beginStream (
1299-                     smaskObject .id ,
1300-                     self .reserveObject ('length of smask stream' ),
1301-                     {'Type' : Name ('XObject' ), 'Subtype' : Name ('Image' ),
1302-                      'Width' : width , 'Height' : height ,
1303-                      'ColorSpace' : Name ('DeviceGray' ), 'BitsPerComponent' : 8 })
1304-                 # TODO: predictors (i.e., output png) 
1305-                 self .currentstream .write (adata )
1306-                 self .endStream ()
1307-                 obj ['SMask' ] =  smaskObject 
1308- 
1309-             self .beginStream (
1310-                 pair [1 ].id ,
1311-                 self .reserveObject ('length of image stream' ),
1312-                 obj 
1313-                 )
1314-             # TODO: predictors (i.e., output png) 
1315-             self .currentstream .write (data )
1316-             self .endStream ()
1347+                 self ._writeImg (adata , height , width , True , smaskObject .id )
1348+             else :
1349+                 smaskObject  =  None 
1350+             self ._writeImg (data , height , width , img .is_grayscale ,
1351+                            pair [1 ].id , smaskObject )
13171352
13181353    def  markerObject (self , path , trans , fill , stroke , lw , joinstyle ,
13191354                     capstyle ):
0 commit comments