@@ -479,7 +479,7 @@ def __init__(self, filename, metadata=None):
479479 self .pagesObject = self .reserveObject ('pages' )
480480 self .pageList = []
481481 self .fontObject = self .reserveObject ('fonts' )
482- self .alphaStateObject = self .reserveObject ('extended graphics states' )
482+ self ._extGStateObject = self .reserveObject ('extended graphics states' )
483483 self .hatchObject = self .reserveObject ('tiling patterns' )
484484 self .gouraudObject = self .reserveObject ('Gouraud triangles' )
485485 self .XObjectObject = self .reserveObject ('external objects' )
@@ -517,6 +517,9 @@ def __init__(self, filename, metadata=None):
517517
518518 self .alphaStates = {} # maps alpha values to graphics state objects
519519 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 = []
520523 # reproducible writeHatches needs an ordered dict:
521524 self .hatchPatterns = collections .OrderedDict ()
522525 self ._hatch_pattern_seq = (Name (f'H{ i } ' ) for i in itertools .count (1 ))
@@ -541,7 +544,7 @@ def __init__(self, filename, metadata=None):
541544 # ColorSpace Pattern Shading Properties
542545 resources = {'Font' : self .fontObject ,
543546 'XObject' : self .XObjectObject ,
544- 'ExtGState' : self .alphaStateObject ,
547+ 'ExtGState' : self ._extGStateObject ,
545548 'Pattern' : self .hatchObject ,
546549 'Shading' : self .gouraudObject ,
547550 'ProcSet' : procsets }
@@ -591,9 +594,8 @@ def finalize(self):
591594
592595 self .endStream ()
593596 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 ()
597599 self .writeHatches ()
598600 self .writeGouraudTriangles ()
599601 xobjects = {
@@ -1217,6 +1219,72 @@ def alphaState(self, alpha):
12171219 'CA' : alpha [0 ], 'ca' : alpha [1 ]})
12181220 return name
12191221
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+
12201288 def hatchPattern (self , hatch_style ):
12211289 # The colors may come in as numpy arrays, which aren't hashable
12221290 if hatch_style is not None :
@@ -1274,18 +1342,39 @@ def writeHatches(self):
12741342 self .writeObject (self .hatchObject , hatchDict )
12751343
12761344 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+ """
12771361 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
12801365
12811366 def writeGouraudTriangles (self ):
12821367 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 :
12851369 gouraudDict [name ] = ob
12861370 shape = points .shape
12871371 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
12891378 points_min = np .min (flat_points , axis = 0 ) - (1 << 8 )
12901379 points_max = np .max (flat_points , axis = 0 ) + (1 << 8 )
12911380 factor = 0xffffffff / (points_max - points_min )
@@ -1296,21 +1385,23 @@ def writeGouraudTriangles(self):
12961385 'BitsPerCoordinate' : 32 ,
12971386 'BitsPerComponent' : 8 ,
12981387 '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 ),
13041395 })
13051396
13061397 streamarr = np .empty (
13071398 (shape [0 ] * shape [1 ],),
13081399 dtype = [('flags' , 'u1' ),
13091400 ('points' , '>u4' , (2 ,)),
1310- ('colors' , 'u1' , (3 ,))])
1401+ ('colors' , 'u1' , (colordim ,))])
13111402 streamarr ['flags' ] = 0
13121403 streamarr ['points' ] = (flat_points - points_min ) * factor
1313- streamarr ['colors' ] = flat_colors [:, :3 ] * 255.0
1404+ streamarr ['colors' ] = flat_colors [:, :colordim ] * 255.0
13141405
13151406 self .write (streamarr .tostring ())
13161407 self .endStream ()
@@ -1806,20 +1897,43 @@ def draw_gouraud_triangle(self, gc, points, colors, trans):
18061897
18071898 def draw_gouraud_triangles (self , gc , points , colors , trans ):
18081899 assert len (points ) == len (colors )
1900+ if len (points ) == 0 :
1901+ return
18091902 assert points .ndim == 3
18101903 assert points .shape [1 ] == 3
18111904 assert points .shape [2 ] == 2
18121905 assert colors .ndim == 3
18131906 assert colors .shape [1 ] == 3
1814- assert colors .shape [2 ] == 4
1907+ assert colors .shape [2 ] in ( 1 , 4 )
18151908
18161909 shape = points .shape
18171910 points = points .reshape ((shape [0 ] * shape [1 ], 2 ))
18181911 tpoints = trans .transform (points )
18191912 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 )
18231937
18241938 def _setup_textpos (self , x , y , angle , oldx = 0 , oldy = 0 , oldangle = 0 ):
18251939 if angle == oldangle == 0 :
0 commit comments