Skip to content

Commit dd15dc0

Browse files
authored
Merge pull request #18370 from anntzer/unttps
Move PostScript Type3 subsetting to pure python.
2 parents 825f518 + 4438b25 commit dd15dc0

File tree

4 files changed

+226
-30
lines changed

4 files changed

+226
-30
lines changed

lib/matplotlib/backends/backend_ps.py

+107-25
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
2626
GraphicsContextBase, RendererBase)
2727
from matplotlib.cbook import is_writable_file_like, file_requires_unicode
28-
from matplotlib.font_manager import is_opentype_cff_font, get_font
29-
from matplotlib.ft2font import LOAD_NO_HINTING
28+
from matplotlib.font_manager import get_font
29+
from matplotlib.ft2font import LOAD_NO_HINTING, LOAD_NO_SCALE
3030
from matplotlib._ttconv import convert_ttf_to_ps
3131
from matplotlib.mathtext import MathTextParser
3232
from matplotlib._mathtext_data import uni2type1
@@ -134,6 +134,86 @@ def _move_path_to_path_or_stream(src, dst):
134134
shutil.move(src, dst, copy_function=shutil.copyfile)
135135

136136

137+
def _font_to_ps_type3(font_path, glyph_ids):
138+
"""
139+
Subset *glyph_ids* from the font at *font_path* into a Type 3 font.
140+
141+
Parameters
142+
----------
143+
font_path : path-like
144+
Path to the font to be subsetted.
145+
glyph_ids : list of int
146+
The glyph indices to include in the subsetted font.
147+
148+
Returns
149+
-------
150+
str
151+
The string representation of a Type 3 font, which can be included
152+
verbatim into a PostScript file.
153+
"""
154+
font = get_font(font_path, hinting_factor=1)
155+
156+
preamble = """\
157+
%!PS-Adobe-3.0 Resource-Font
158+
%%Creator: Converted from TrueType to Type 3 by Matplotlib.
159+
10 dict begin
160+
/FontName /{font_name} def
161+
/PaintType 0 def
162+
/FontMatrix [{inv_units_per_em} 0 0 {inv_units_per_em} 0 0] def
163+
/FontBBox [{bbox}] def
164+
/FontType 3 def
165+
/Encoding [{encoding}] def
166+
/CharStrings {num_glyphs} dict dup begin
167+
/.notdef 0 def
168+
""".format(font_name=font.postscript_name,
169+
inv_units_per_em=1 / font.units_per_EM,
170+
bbox=" ".join(map(str, font.bbox)),
171+
encoding=" ".join("/{}".format(font.get_glyph_name(glyph_id))
172+
for glyph_id in glyph_ids),
173+
num_glyphs=len(glyph_ids) + 1)
174+
postamble = """
175+
end readonly def
176+
177+
/BuildGlyph {
178+
exch begin
179+
CharStrings exch
180+
2 copy known not {pop /.notdef} if
181+
true 3 1 roll get exec
182+
end
183+
} d
184+
185+
/BuildChar {
186+
1 index /Encoding get exch get
187+
1 index /BuildGlyph get exec
188+
} d
189+
190+
FontName currentdict end definefont pop
191+
"""
192+
193+
entries = []
194+
for glyph_id in glyph_ids:
195+
g = font.load_glyph(glyph_id, LOAD_NO_SCALE)
196+
v, c = font.get_path()
197+
entries.append(
198+
"/%(name)s{%(bbox)s sc\n" % {
199+
"name": font.get_glyph_name(glyph_id),
200+
"bbox": " ".join(map(str, [g.horiAdvance, 0, *g.bbox])),
201+
}
202+
+ _path.convert_to_string(
203+
# Convert back to TrueType's internal units (1/64's).
204+
# (Other dimensions are already in these units.)
205+
Path(v * 64, c), None, None, False, None, 0,
206+
# No code for quad Beziers triggers auto-conversion to cubics.
207+
# Drop intermediate closepolys (relying on the outline
208+
# decomposer always explicitly moving to the closing point
209+
# first).
210+
[b"m", b"l", b"", b"c", b""], True).decode("ascii")
211+
+ "ce} d"
212+
)
213+
214+
return preamble + "\n".join(entries) + postamble
215+
216+
137217
class RendererPS(_backend_pdf_ps.RendererPDFPSBase):
138218
"""
139219
The renderer handles all the drawing primitives using a graphics
@@ -922,22 +1002,18 @@ def print_figure_impl(fh):
9221002
# Can't use more than 255 chars from a single Type 3 font.
9231003
if len(glyph_ids) > 255:
9241004
fonttype = 42
925-
# The ttf to ps (subsetting) support doesn't work for
926-
# OpenType fonts that are Postscript inside (like the STIX
927-
# fonts). This will simply turn that off to avoid errors.
928-
if is_opentype_cff_font(font_path):
929-
raise RuntimeError(
930-
"OpenType CFF fonts can not be saved using "
931-
"the internal Postscript backend at this "
932-
"time; consider using the Cairo backend")
9331005
fh.flush()
934-
try:
935-
convert_ttf_to_ps(os.fsencode(font_path),
936-
fh, fonttype, glyph_ids)
937-
except RuntimeError:
938-
_log.warning("The PostScript backend does not "
939-
"currently support the selected font.")
940-
raise
1006+
if fonttype == 3:
1007+
fh.write(_font_to_ps_type3(font_path, glyph_ids))
1008+
else:
1009+
try:
1010+
convert_ttf_to_ps(os.fsencode(font_path),
1011+
fh, fonttype, glyph_ids)
1012+
except RuntimeError:
1013+
_log.warning(
1014+
"The PostScript backend does not currently "
1015+
"support the selected font.")
1016+
raise
9411017
print("end", file=fh)
9421018
print("%%EndProlog", file=fh)
9431019

@@ -1312,30 +1388,36 @@ def pstoeps(tmpfile, bbox=None, rotated=False):
13121388
# The usage comments use the notation of the operator summary
13131389
# in the PostScript Language reference manual.
13141390
psDefs = [
1391+
# name proc *d* -
1392+
"/d { bind def } bind def",
13151393
# x y *m* -
1316-
"/m { moveto } bind def",
1394+
"/m { moveto } d",
13171395
# x y *l* -
1318-
"/l { lineto } bind def",
1396+
"/l { lineto } d",
13191397
# x y *r* -
1320-
"/r { rlineto } bind def",
1398+
"/r { rlineto } d",
13211399
# x1 y1 x2 y2 x y *c* -
1322-
"/c { curveto } bind def",
1323-
# *closepath* -
1324-
"/cl { closepath } bind def",
1400+
"/c { curveto } d",
1401+
# *cl* -
1402+
"/cl { closepath } d",
1403+
# *ce* -
1404+
"/ce { closepath eofill } d",
13251405
# w h x y *box* -
13261406
"""/box {
13271407
m
13281408
1 index 0 r
13291409
0 exch r
13301410
neg 0 r
13311411
cl
1332-
} bind def""",
1412+
} d""",
13331413
# w h x y *clipbox* -
13341414
"""/clipbox {
13351415
box
13361416
clip
13371417
newpath
1338-
} bind def""",
1418+
} d""",
1419+
# wx wy llx lly urx ury *setcachedevice* -
1420+
"/sc { setcachedevice } d",
13391421
]
13401422

13411423

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
%!PS-Adobe-3.0 EPSF-3.0
2+
%%Orientation: portrait
3+
%%BoundingBox: 18.0 180.0 594.0 612.0
4+
%%EndComments
5+
%%BeginProlog
6+
/mpldict 11 dict def
7+
mpldict begin
8+
/d { bind def } bind def
9+
/m { moveto } d
10+
/l { lineto } d
11+
/r { rlineto } d
12+
/c { curveto } d
13+
/cl { closepath } d
14+
/ce { closepath eofill } d
15+
/box {
16+
m
17+
1 index 0 r
18+
0 exch r
19+
neg 0 r
20+
cl
21+
} d
22+
/clipbox {
23+
box
24+
clip
25+
newpath
26+
} d
27+
/sc { setcachedevice } d
28+
%!PS-Adobe-3.0 Resource-Font
29+
%%Creator: Converted from TrueType to Type 3 by Matplotlib.
30+
10 dict begin
31+
/FontName /DejaVuSans def
32+
/PaintType 0 def
33+
/FontMatrix [0.00048828125 0 0 0.00048828125 0 0] def
34+
/FontBBox [-2090 -948 3673 2524] def
35+
/FontType 3 def
36+
/Encoding [/I /J /slash] def
37+
/CharStrings 4 dict dup begin
38+
/.notdef 0 def
39+
/I{604 0 201 0 403 1493 sc
40+
201 1493 m
41+
403 1493 l
42+
403 0 l
43+
201 0 l
44+
201 1493 l
45+
46+
ce} d
47+
/J{604 0 -106 -410 403 1493 sc
48+
201 1493 m
49+
403 1493 l
50+
403 104 l
51+
403 -76 369 -207 300 -288 c
52+
232 -369 122 -410 -29 -410 c
53+
-106 -410 l
54+
-106 -240 l
55+
-43 -240 l
56+
46 -240 109 -215 146 -165 c
57+
183 -115 201 -25 201 104 c
58+
201 1493 l
59+
60+
ce} d
61+
/slash{690 0 0 -190 690 1493 sc
62+
520 1493 m
63+
690 1493 l
64+
170 -190 l
65+
0 -190 l
66+
520 1493 l
67+
68+
ce} d
69+
end readonly def
70+
71+
/BuildGlyph {
72+
exch begin
73+
CharStrings exch
74+
2 copy known not {pop /.notdef} if
75+
true 3 1 roll get exec
76+
end
77+
} d
78+
79+
/BuildChar {
80+
1 index /Encoding get exch get
81+
1 index /BuildGlyph get exec
82+
} d
83+
84+
FontName currentdict end definefont pop
85+
end
86+
%%EndProlog
87+
mpldict begin
88+
18 180 translate
89+
576 432 0 0 clipbox
90+
gsave
91+
0 0 m
92+
576 0 l
93+
576 432 l
94+
0 432 l
95+
cl
96+
1.000 setgray
97+
fill
98+
grestore
99+
0.000 setgray
100+
/DejaVuSans findfont
101+
12.000 scalefont
102+
setfont
103+
gsave
104+
288.000000 216.000000 translate
105+
0.000000 rotate
106+
0.000000 0 m /I glyphshow
107+
3.539062 0 m /slash glyphshow
108+
7.582031 0 m /J glyphshow
109+
grestore
110+
111+
end
112+
showpage

lib/matplotlib/tests/test_backend_ps.py

+5
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,8 @@ def test_useafm():
157157
ax.set_axis_off()
158158
ax.axhline(.5)
159159
ax.text(.5, .5, "qk")
160+
161+
162+
@image_comparison(["type3.eps"])
163+
def test_type3_font():
164+
plt.figtext(.5, .5, "I/J")

lib/matplotlib/tests/test_font_manager.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,8 @@ def test_find_ttc():
120120

121121
fig, ax = plt.subplots()
122122
ax.text(.5, .5, "\N{KANGXI RADICAL DRAGON}", fontproperties=fp)
123-
fig.savefig(BytesIO(), format="raw")
124-
fig.savefig(BytesIO(), format="svg")
125-
fig.savefig(BytesIO(), format="pdf")
126-
with pytest.raises(RuntimeError):
127-
fig.savefig(BytesIO(), format="ps")
123+
for fmt in ["raw", "svg", "pdf", "ps"]:
124+
fig.savefig(BytesIO(), format=fmt)
128125

129126

130127
def test_find_invalid(tmpdir):

0 commit comments

Comments
 (0)