12
12
import codecs
13
13
import os
14
14
import re
15
+ import struct
15
16
import sys
16
17
import time
17
18
import warnings
43
44
from matplotlib .transforms import Affine2D , BboxBase
44
45
from matplotlib .path import Path
45
46
from matplotlib import _path
47
+ from matplotlib import _png
46
48
from matplotlib import ttconv
47
49
48
50
# Overview
87
89
88
90
# TODOs:
89
91
#
90
- # * image compression could be improved (PDF supports png-like compression)
91
92
# * encoding of fonts, including mathtext fonts and unicode support
92
93
# * TTF support has lots of small TODOs, e.g., how do you know if a font
93
94
# is serif/sans-serif, or symbolic/non-symbolic?
@@ -334,11 +335,12 @@ class Stream(object):
334
335
"""
335
336
__slots__ = ('id' , 'len' , 'pdfFile' , 'file' , 'compressobj' , 'extra' , 'pos' )
336
337
337
- def __init__ (self , id , len , file , extra = None ):
338
+ def __init__ (self , id , len , file , extra = None , png = None ):
338
339
"""id: object id of stream; len: an unused Reference object for the
339
340
length of the stream, or None (to use a memory buffer); file:
340
341
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"""
342
344
self .id = id # object id
343
345
self .len = len # id of length object
344
346
self .pdfFile = file
@@ -347,10 +349,13 @@ def __init__(self, id, len, file, extra=None):
347
349
if extra is None :
348
350
self .extra = dict ()
349
351
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 })
351
356
352
357
self .pdfFile .recordXref (self .id )
353
- if rcParams ['pdf.compression' ]:
358
+ if rcParams ['pdf.compression' ] and not png :
354
359
self .compressobj = zlib .compressobj (rcParams ['pdf.compression' ])
355
360
if self .len is None :
356
361
self .file = BytesIO ()
@@ -583,9 +588,9 @@ def output(self, *data):
583
588
self .write (fill ([pdfRepr (x ) for x in data ]))
584
589
self .write (b'\n ' )
585
590
586
- def beginStream (self , id , len , extra = None ):
591
+ def beginStream (self , id , len , extra = None , png = None ):
587
592
assert self .currentstream is None
588
- self .currentstream = Stream (id , len , self , extra )
593
+ self .currentstream = Stream (id , len , self , extra , png )
589
594
590
595
def endStream (self ):
591
596
if self .currentstream is not None :
@@ -1247,73 +1252,103 @@ def imageObject(self, image):
1247
1252
self .images [image ] = (name , ob )
1248
1253
return name
1249
1254
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
+ """
1252
1261
1262
+ h , w , s = im .as_rgba_str ()
1253
1263
rgba = np .fromstring (s , np .uint8 )
1254
1264
rgba .shape = (h , w , 4 )
1255
1265
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 ):
1259
1269
alpha = None
1260
1270
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
1276
1276
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
1280
1279
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
1285
1303
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
+ """
1287
1314
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 ()
1295
1341
1342
+ def writeImages (self ):
1343
+ for img , pair in six .iteritems (self .images ):
1344
+ height , width , data , adata = self ._unpack (img )
1296
1345
if adata is not None :
1297
1346
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 )
1317
1352
1318
1353
def markerObject (self , path , trans , fill , stroke , lw , joinstyle ,
1319
1354
capstyle ):
0 commit comments