Skip to content

Commit db2193c

Browse files
authored
Merge pull request #14414 from jkseppan/pdf-gouraud-alpha
FEATURE: Alpha channel in Gouraud triangles in the pdf backend
2 parents 1e40243 + 225b1bd commit db2193c

File tree

6 files changed

+172
-21
lines changed

6 files changed

+172
-21
lines changed

doc/api/next_api_changes/2019-06-05-JKS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ The following members of ``matplotlib.backends.backend_pdf.PdfFile`` were remove
88
- ``nextAlphaState``
99
- ``nextHatch``
1010
- ``nextImage``
11+
- ``alphaStateObject``
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Gouraud-shaded mesh alpha channels in the pdf backend
2+
-----------------------------------------------------
3+
4+
The pdf backend now supports an alpha channel in Gouraud-shaded
5+
triangle meshes.

lib/matplotlib/backends/backend_pdf.py

Lines changed: 135 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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:
Binary file not shown.
Loading

lib/matplotlib/tests/test_axes.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,37 @@ def test_pcolormesh():
11121112
ax3.pcolormesh(Qx, Qz, Z, shading="gouraud")
11131113

11141114

1115+
@image_comparison(['pcolormesh_alpha'], extensions=["png", "pdf"],
1116+
remove_text=True)
1117+
def test_pcolormesh_alpha():
1118+
n = 12
1119+
X, Y = np.meshgrid(
1120+
np.linspace(-1.5, 1.5, n),
1121+
np.linspace(-1.5, 1.5, n*2)
1122+
)
1123+
Qx = X
1124+
Qy = Y + np.sin(X)
1125+
Z = np.hypot(X, Y) / 5
1126+
Z = (Z - Z.min()) / Z.ptp()
1127+
vir = plt.get_cmap("viridis", 16)
1128+
# make another colormap with varying alpha
1129+
colors = vir(np.arange(16))
1130+
colors[:, 3] = 0.5 + 0.5*np.sin(np.arange(16))
1131+
cmap = mcolors.ListedColormap(colors)
1132+
1133+
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2)
1134+
for ax in ax1, ax2, ax3, ax4:
1135+
ax.add_patch(mpatches.Rectangle(
1136+
(0, -1.5), 1.5, 3, facecolor=[.7, .1, .1, .5], zorder=0
1137+
))
1138+
# ax1, ax2: constant alpha
1139+
ax1.pcolormesh(Qx, Qy, Z, cmap=vir, alpha=0.4, shading='flat', zorder=1)
1140+
ax2.pcolormesh(Qx, Qy, Z, cmap=vir, alpha=0.4, shading='gouraud', zorder=1)
1141+
# ax3, ax4: alpha from colormap
1142+
ax3.pcolormesh(Qx, Qy, Z, cmap=cmap, shading='flat', zorder=1)
1143+
ax4.pcolormesh(Qx, Qy, Z, cmap=cmap, shading='gouraud', zorder=1)
1144+
1145+
11151146
@image_comparison(['pcolormesh_datetime_axis.png'],
11161147
remove_text=False, style='mpl20')
11171148
def test_pcolormesh_datetime_axis():

0 commit comments

Comments
 (0)