Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4438b25

Browse files
committed
Move PostScript Type3 subsetting to pure python.
... similarly to the change for pdf, but easier because there are no baseline images for which we need to provide bug-level backcompat :-) Drop the FontInfo metadata (which is explicitly optional in the PostScript spec) to avoid having to figure out the correct encoding (which can be quite obscure). Replace the implementation of the `_sc` command from `7 -1 roll{setcachedevice}{pop pop pop pop pop pop}ifelse` to a plain `setcachedevice` (as I cannot see any case where the "other" branch is taken). Drop the splitting of long commands using `exec` (`_e`) -- this is only needed for level-1 postscript, which has a small fixed stack size; we output level-2 postscript (per backend_version) and I guess level-1 is rarely in use nowadays anyways (probably the feature could be added back if there's really demand for it, but let's not get ahead of ourselves). Previously, some composite characters would be output in a "compressed" form (e.g., accented characters would be recorded as "draw the accent, then run the charproc for the unaccented character"). This is lost, but I'd guess outputting .ps.gz is better if compression really matters.
1 parent 825f518 commit 4438b25

File tree

4 files changed

+226
-30
lines changed

4 files changed

+226
-30
lines changed

lib/matplotlib/backends/backend_ps.py

Lines changed: 107 additions & 25 deletions
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

Lines changed: 112 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 2 additions & 5 deletions
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)