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