@@ -479,7 +479,7 @@ def __init__(self, filename, metadata=None):
479
479
self .pagesObject = self .reserveObject ('pages' )
480
480
self .pageList = []
481
481
self .fontObject = self .reserveObject ('fonts' )
482
- self .alphaStateObject = self .reserveObject ('extended graphics states' )
482
+ self ._extGStateObject = self .reserveObject ('extended graphics states' )
483
483
self .hatchObject = self .reserveObject ('tiling patterns' )
484
484
self .gouraudObject = self .reserveObject ('Gouraud triangles' )
485
485
self .XObjectObject = self .reserveObject ('external objects' )
@@ -517,6 +517,9 @@ def __init__(self, filename, metadata=None):
517
517
518
518
self .alphaStates = {} # maps alpha values to graphics state objects
519
519
self ._alpha_state_seq = (Name (f'A{ i } ' ) for i in itertools .count (1 ))
520
+ self ._soft_mask_states = {}
521
+ self ._soft_mask_seq = (Name (f'SM{ i } ' ) for i in itertools .count (1 ))
522
+ self ._soft_mask_groups = []
520
523
# reproducible writeHatches needs an ordered dict:
521
524
self .hatchPatterns = collections .OrderedDict ()
522
525
self ._hatch_pattern_seq = (Name (f'H{ i } ' ) for i in itertools .count (1 ))
@@ -541,7 +544,7 @@ def __init__(self, filename, metadata=None):
541
544
# ColorSpace Pattern Shading Properties
542
545
resources = {'Font' : self .fontObject ,
543
546
'XObject' : self .XObjectObject ,
544
- 'ExtGState' : self .alphaStateObject ,
547
+ 'ExtGState' : self ._extGStateObject ,
545
548
'Pattern' : self .hatchObject ,
546
549
'Shading' : self .gouraudObject ,
547
550
'ProcSet' : procsets }
@@ -591,9 +594,8 @@ def finalize(self):
591
594
592
595
self .endStream ()
593
596
self .writeFonts ()
594
- self .writeObject (
595
- self .alphaStateObject ,
596
- {val [0 ]: val [1 ] for val in self .alphaStates .values ()})
597
+ self .writeExtGSTates ()
598
+ self ._write_soft_mask_groups ()
597
599
self .writeHatches ()
598
600
self .writeGouraudTriangles ()
599
601
xobjects = {
@@ -1217,6 +1219,72 @@ def alphaState(self, alpha):
1217
1219
'CA' : alpha [0 ], 'ca' : alpha [1 ]})
1218
1220
return name
1219
1221
1222
+ def _soft_mask_state (self , smask ):
1223
+ """Return an ExtGState that sets the soft mask to the given shading.
1224
+
1225
+ Parameters
1226
+ ----------
1227
+ smask : Reference
1228
+ Reference to a shading in DeviceGray color space, whose luminosity
1229
+ is to be used as the alpha channel.
1230
+
1231
+ Returns
1232
+ -------
1233
+ Name
1234
+ """
1235
+
1236
+ state = self ._soft_mask_states .get (smask , None )
1237
+ if state is not None :
1238
+ return state [0 ]
1239
+
1240
+ name = next (self ._soft_mask_seq )
1241
+ groupOb = self .reserveObject ('transparency group for soft mask' )
1242
+ self ._soft_mask_states [smask ] = (
1243
+ name ,
1244
+ {
1245
+ 'Type' : Name ('ExtGState' ),
1246
+ 'AIS' : False ,
1247
+ 'SMask' : {
1248
+ 'Type' : Name ('Mask' ),
1249
+ 'S' : Name ('Luminosity' ),
1250
+ 'BC' : [1 ],
1251
+ 'G' : groupOb
1252
+ }
1253
+ }
1254
+ )
1255
+ self ._soft_mask_groups .append ((
1256
+ groupOb ,
1257
+ {
1258
+ 'Type' : Name ('XObject' ),
1259
+ 'Subtype' : Name ('Form' ),
1260
+ 'FormType' : 1 ,
1261
+ 'Group' : {
1262
+ 'S' : Name ('Transparency' ),
1263
+ 'CS' : Name ('DeviceGray' )
1264
+ },
1265
+ 'Matrix' : [1 , 0 , 0 , 1 , 0 , 0 ],
1266
+ 'Resources' : {'Shading' : {'S' : smask }},
1267
+ 'BBox' : [0 , 0 , 1 , 1 ]
1268
+ },
1269
+ [Name ('S' ), Op .shading ]
1270
+ ))
1271
+ return name
1272
+
1273
+ def writeExtGSTates (self ):
1274
+ self .writeObject (
1275
+ self ._extGStateObject ,
1276
+ dict ([
1277
+ * self .alphaStates .values (),
1278
+ * self ._soft_mask_states .values ()
1279
+ ])
1280
+ )
1281
+
1282
+ def _write_soft_mask_groups (self ):
1283
+ for ob , attributes , content in self ._soft_mask_groups :
1284
+ self .beginStream (ob .id , None , attributes )
1285
+ self .output (* content )
1286
+ self .endStream ()
1287
+
1220
1288
def hatchPattern (self , hatch_style ):
1221
1289
# The colors may come in as numpy arrays, which aren't hashable
1222
1290
if hatch_style is not None :
@@ -1274,18 +1342,39 @@ def writeHatches(self):
1274
1342
self .writeObject (self .hatchObject , hatchDict )
1275
1343
1276
1344
def addGouraudTriangles (self , points , colors ):
1345
+ """Add a Gouraud triangle shading
1346
+
1347
+ Parameters
1348
+ ----------
1349
+ points : np.ndarray
1350
+ Triangle vertices, shape (n, 3, 2)
1351
+ where n = number of triangles, 3 = vertices, 2 = x, y.
1352
+ colors : np.ndarray
1353
+ Vertex colors, shape (n, 3, 1) or (n, 3, 4)
1354
+ as with points, but last dimension is either (gray,)
1355
+ or (r, g, b, alpha).
1356
+
1357
+ Returns
1358
+ -------
1359
+ Name, Reference
1360
+ """
1277
1361
name = Name ('GT%d' % len (self .gouraudTriangles ))
1278
- self .gouraudTriangles .append ((name , points , colors ))
1279
- return name
1362
+ ob = self .reserveObject (f'Gouraud triangle { name } ' )
1363
+ self .gouraudTriangles .append ((name , ob , points , colors ))
1364
+ return name , ob
1280
1365
1281
1366
def writeGouraudTriangles (self ):
1282
1367
gouraudDict = dict ()
1283
- for name , points , colors in self .gouraudTriangles :
1284
- ob = self .reserveObject ('Gouraud triangle' )
1368
+ for name , ob , points , colors in self .gouraudTriangles :
1285
1369
gouraudDict [name ] = ob
1286
1370
shape = points .shape
1287
1371
flat_points = points .reshape ((shape [0 ] * shape [1 ], 2 ))
1288
- flat_colors = colors .reshape ((shape [0 ] * shape [1 ], 4 ))
1372
+ colordim = colors .shape [2 ]
1373
+ assert colordim in (1 , 4 )
1374
+ flat_colors = colors .reshape ((shape [0 ] * shape [1 ], colordim ))
1375
+ if colordim == 4 :
1376
+ # strip the alpha channel
1377
+ colordim = 3
1289
1378
points_min = np .min (flat_points , axis = 0 ) - (1 << 8 )
1290
1379
points_max = np .max (flat_points , axis = 0 ) + (1 << 8 )
1291
1380
factor = 0xffffffff / (points_max - points_min )
@@ -1296,21 +1385,23 @@ def writeGouraudTriangles(self):
1296
1385
'BitsPerCoordinate' : 32 ,
1297
1386
'BitsPerComponent' : 8 ,
1298
1387
'BitsPerFlag' : 8 ,
1299
- 'ColorSpace' : Name ('DeviceRGB' ),
1300
- 'AntiAlias' : True ,
1301
- 'Decode' : [points_min [0 ], points_max [0 ],
1302
- points_min [1 ], points_max [1 ],
1303
- 0 , 1 , 0 , 1 , 0 , 1 ]
1388
+ 'ColorSpace' : Name (
1389
+ 'DeviceRGB' if colordim == 3 else 'DeviceGray'
1390
+ ),
1391
+ 'AntiAlias' : False ,
1392
+ 'Decode' : ([points_min [0 ], points_max [0 ],
1393
+ points_min [1 ], points_max [1 ]]
1394
+ + [0 , 1 ] * colordim ),
1304
1395
})
1305
1396
1306
1397
streamarr = np .empty (
1307
1398
(shape [0 ] * shape [1 ],),
1308
1399
dtype = [('flags' , 'u1' ),
1309
1400
('points' , '>u4' , (2 ,)),
1310
- ('colors' , 'u1' , (3 ,))])
1401
+ ('colors' , 'u1' , (colordim ,))])
1311
1402
streamarr ['flags' ] = 0
1312
1403
streamarr ['points' ] = (flat_points - points_min ) * factor
1313
- streamarr ['colors' ] = flat_colors [:, :3 ] * 255.0
1404
+ streamarr ['colors' ] = flat_colors [:, :colordim ] * 255.0
1314
1405
1315
1406
self .write (streamarr .tostring ())
1316
1407
self .endStream ()
@@ -1806,20 +1897,43 @@ def draw_gouraud_triangle(self, gc, points, colors, trans):
1806
1897
1807
1898
def draw_gouraud_triangles (self , gc , points , colors , trans ):
1808
1899
assert len (points ) == len (colors )
1900
+ if len (points ) == 0 :
1901
+ return
1809
1902
assert points .ndim == 3
1810
1903
assert points .shape [1 ] == 3
1811
1904
assert points .shape [2 ] == 2
1812
1905
assert colors .ndim == 3
1813
1906
assert colors .shape [1 ] == 3
1814
- assert colors .shape [2 ] == 4
1907
+ assert colors .shape [2 ] in ( 1 , 4 )
1815
1908
1816
1909
shape = points .shape
1817
1910
points = points .reshape ((shape [0 ] * shape [1 ], 2 ))
1818
1911
tpoints = trans .transform (points )
1819
1912
tpoints = tpoints .reshape (shape )
1820
- name = self .file .addGouraudTriangles (tpoints , colors )
1821
- self .check_gc (gc )
1822
- self .file .output (name , Op .shading )
1913
+ name , _ = self .file .addGouraudTriangles (tpoints , colors )
1914
+ output = self .file .output
1915
+
1916
+ if colors .shape [2 ] == 1 :
1917
+ # grayscale
1918
+ gc .set_alpha (1.0 )
1919
+ self .check_gc (gc )
1920
+ output (name , Op .shading )
1921
+ return
1922
+
1923
+ alpha = colors [0 , 0 , 3 ]
1924
+ if np .allclose (alpha , colors [:, :, 3 ]):
1925
+ # single alpha value
1926
+ gc .set_alpha (alpha )
1927
+ self .check_gc (gc )
1928
+ output (name , Op .shading )
1929
+ else :
1930
+ # varying alpha: use a soft mask
1931
+ alpha = colors [:, :, 3 ][:, :, None ]
1932
+ _ , smask_ob = self .file .addGouraudTriangles (tpoints , alpha )
1933
+ gstate = self .file ._soft_mask_state (smask_ob )
1934
+ output (Op .gsave , gstate , Op .setgstate ,
1935
+ name , Op .shading ,
1936
+ Op .grestore )
1823
1937
1824
1938
def _setup_textpos (self , x , y , angle , oldx = 0 , oldy = 0 , oldangle = 0 ):
1825
1939
if angle == oldangle == 0 :
0 commit comments