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

Skip to content

Commit 1c0b4f4

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 71de09a commit 1c0b4f4

File tree

4 files changed

+180
-22
lines changed

4 files changed

+180
-22
lines changed

lib/matplotlib/backends/backend_ps.py

Lines changed: 84 additions & 17 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,77 @@ 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+
font = get_font(font_path, hinting_factor=1)
139+
140+
preamble = """\
141+
%!PS-Adobe-3.0 Resource-Font
142+
%%Creator: Converted from TrueType to Type 3 by Matplotlib.
143+
20 dict begin
144+
/_d {{bind def}} bind def
145+
/_m {{moveto}} _d
146+
/_l {{lineto}} _d
147+
/_ce {{closepath eofill}} _d
148+
/_c {{curveto}} _d
149+
/_sc {{setcachedevice}} _d
150+
/_e {{exec}} _d
151+
/FontName /{font_name} def
152+
/PaintType 0 def
153+
/FontMatrix [{inv_units_per_em} 0 0 {inv_units_per_em} 0 0] def
154+
/FontBBox [{bbox}] def
155+
/FontType 3 def
156+
/Encoding [{encoding}] def
157+
/CharStrings {num_glyphs} dict dup begin
158+
/.notdef 0 def
159+
""".format(font_name=font.postscript_name,
160+
inv_units_per_em=1 / font.units_per_EM,
161+
bbox=" ".join(map(str, font.bbox)),
162+
encoding=" ".join("/{}".format(font.get_glyph_name(glyph_id))
163+
for glyph_id in glyph_ids),
164+
num_glyphs=len(glyph_ids) + 1)
165+
postamble = """
166+
end readonly def
167+
168+
/BuildGlyph {
169+
exch begin
170+
CharStrings exch
171+
2 copy known not {pop /.notdef} if
172+
true 3 1 roll get exec
173+
end
174+
}_d
175+
176+
/BuildChar {
177+
1 index /Encoding get exch get
178+
1 index /BuildGlyph get exec
179+
}_d
180+
181+
FontName currentdict end definefont pop
182+
"""
183+
184+
entries = []
185+
for glyph_id in glyph_ids:
186+
g = font.load_glyph(glyph_id, LOAD_NO_SCALE)
187+
v, c = font.get_path()
188+
entries.append(
189+
"/%(name)s{%(bbox)s _sc\n" % {
190+
"name": font.get_glyph_name(glyph_id),
191+
"bbox": " ".join(map(str, [g.horiAdvance, 0, *g.bbox])),
192+
}
193+
+ _path.convert_to_string(
194+
# Convert back to TrueType's internal units (1/64's).
195+
# (Other dimensions are already in these units.)
196+
Path(v * 64, c), None, None, False, None, 0,
197+
# No code for quad Beziers triggers auto-conversion to cubics.
198+
# Drop intermediate closepolys (relying on the outline
199+
# decomposer always explicitly moving to the closing point
200+
# first).
201+
[b"_m", b"_l", b"", b"_c", b""], True).decode("ascii")
202+
+ "_ce}_d"
203+
)
204+
205+
return preamble + "\n".join(entries) + postamble
206+
207+
137208
class RendererPS(_backend_pdf_ps.RendererPDFPSBase):
138209
"""
139210
The renderer handles all the drawing primitives using a graphics
@@ -932,22 +1003,18 @@ def print_figure_impl(fh):
9321003
# Can't use more than 255 chars from a single Type 3 font.
9331004
if len(glyph_ids) > 255:
9341005
fonttype = 42
935-
# The ttf to ps (subsetting) support doesn't work for
936-
# OpenType fonts that are Postscript inside (like the STIX
937-
# fonts). This will simply turn that off to avoid errors.
938-
if is_opentype_cff_font(font_path):
939-
raise RuntimeError(
940-
"OpenType CFF fonts can not be saved using "
941-
"the internal Postscript backend at this "
942-
"time; consider using the Cairo backend")
9431006
fh.flush()
944-
try:
945-
convert_ttf_to_ps(os.fsencode(font_path),
946-
fh, fonttype, glyph_ids)
947-
except RuntimeError:
948-
_log.warning("The PostScript backend does not "
949-
"currently support the selected font.")
950-
raise
1007+
if fonttype == 3:
1008+
fh.write(_font_to_ps_type3(font_path, glyph_ids))
1009+
else:
1010+
try:
1011+
convert_ttf_to_ps(os.fsencode(font_path),
1012+
fh, fonttype, glyph_ids)
1013+
except RuntimeError:
1014+
_log.warning(
1015+
"The PostScript backend does not currently "
1016+
"support the selected font.")
1017+
raise
9511018
print("end", file=fh)
9521019
print("%%EndProlog", file=fh)
9531020

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
%!PS-Adobe-3.0 EPSF-3.0
2+
%%BeginProlog
3+
/mpldict 8 dict def
4+
mpldict begin
5+
/m { moveto } bind def
6+
/l { lineto } bind def
7+
/r { rlineto } bind def
8+
/c { curveto } bind def
9+
/cl { closepath } bind def
10+
/box {
11+
m
12+
1 index 0 r
13+
0 exch r
14+
neg 0 r
15+
cl
16+
} bind def
17+
/clipbox {
18+
box
19+
clip
20+
newpath
21+
} bind def
22+
%!PS-Adobe-3.0 Resource-Font
23+
%%Creator: Converted from TrueType to Type 3 by Matplotlib.
24+
20 dict begin
25+
/_d {bind def} bind def
26+
/_m {moveto} _d
27+
/_l {lineto} _d
28+
/_ce {closepath eofill} _d
29+
/_c {curveto} _d
30+
/_sc {setcachedevice} _d
31+
/_e {exec} _d
32+
/FontName /DejaVuSans def
33+
/PaintType 0 def
34+
/FontMatrix [0.00048828125 0 0 0.00048828125 0 0] def
35+
/FontBBox [-2090 -948 3673 2524] def
36+
/FontType 3 def
37+
/Encoding [/I] def
38+
/CharStrings 2 dict dup begin
39+
/.notdef 0 def
40+
/I{604 0 201 0 403 1493 _sc
41+
201 1493 _m
42+
403 1493 _l
43+
403 0 _l
44+
201 0 _l
45+
201 1493 _l
46+
47+
_ce}_d
48+
end readonly def
49+
50+
/BuildGlyph {
51+
exch begin
52+
CharStrings exch
53+
2 copy known not {pop /.notdef} if
54+
true 3 1 roll get exec
55+
end
56+
}_d
57+
58+
/BuildChar {
59+
1 index /Encoding get exch get
60+
1 index /BuildGlyph get exec
61+
}_d
62+
63+
FontName currentdict end definefont pop
64+
end
65+
%%EndProlog
66+
mpldict begin
67+
18 180 translate
68+
576 432 0 0 clipbox
69+
gsave
70+
0 0 m
71+
576 0 l
72+
576 432 l
73+
0 432 l
74+
cl
75+
1.000 setgray
76+
fill
77+
grestore
78+
0.000 setgray
79+
/DejaVuSans findfont
80+
12.000 scalefont
81+
setfont
82+
gsave
83+
288.000000 216.000000 translate
84+
0.000000 rotate
85+
0.000000 0 m /I glyphshow
86+
grestore
87+
88+
end
89+
showpage

lib/matplotlib/tests/test_backend_ps.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,8 @@ def test_partial_usetex(caplog):
133133
plt.savefig(io.BytesIO(), format="ps")
134134
assert caplog.records and all("as if usetex=False" in record.getMessage()
135135
for record in caplog.records)
136+
137+
138+
@image_comparison(["type3.eps"])
139+
def test_type3_font():
140+
plt.figtext(.5, .5, "I")

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)