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

Skip to content

Move PostScript Type3 subsetting to pure python. #18370

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 107 additions & 25 deletions lib/matplotlib/backends/backend_ps.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
GraphicsContextBase, RendererBase)
from matplotlib.cbook import is_writable_file_like, file_requires_unicode
from matplotlib.font_manager import is_opentype_cff_font, get_font
from matplotlib.ft2font import LOAD_NO_HINTING
from matplotlib.font_manager import get_font
from matplotlib.ft2font import LOAD_NO_HINTING, LOAD_NO_SCALE
from matplotlib._ttconv import convert_ttf_to_ps
from matplotlib.mathtext import MathTextParser
from matplotlib._mathtext_data import uni2type1
Expand Down Expand Up @@ -134,6 +134,86 @@ def _move_path_to_path_or_stream(src, dst):
shutil.move(src, dst, copy_function=shutil.copyfile)


def _font_to_ps_type3(font_path, glyph_ids):
"""
Subset *glyph_ids* from the font at *font_path* into a Type 3 font.

Parameters
----------
font_path : path-like
Path to the font to be subsetted.
glyph_ids : list of int
The glyph indices to include in the subsetted font.

Returns
-------
str
The string representation of a Type 3 font, which can be included
verbatim into a PostScript file.
"""
font = get_font(font_path, hinting_factor=1)

preamble = """\
%!PS-Adobe-3.0 Resource-Font
%%Creator: Converted from TrueType to Type 3 by Matplotlib.
10 dict begin
/FontName /{font_name} def
/PaintType 0 def
/FontMatrix [{inv_units_per_em} 0 0 {inv_units_per_em} 0 0] def
/FontBBox [{bbox}] def
/FontType 3 def
/Encoding [{encoding}] def
/CharStrings {num_glyphs} dict dup begin
/.notdef 0 def
""".format(font_name=font.postscript_name,
inv_units_per_em=1 / font.units_per_EM,
bbox=" ".join(map(str, font.bbox)),
encoding=" ".join("/{}".format(font.get_glyph_name(glyph_id))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this safe, or do you need to escape any Postscript special values?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be safe per https://github.com/adobe-type-tools/agl-specification#6-assigning-glyph-names-in-new-fonts. We can always revisit if someone finds a font with a bad glyph name...

for glyph_id in glyph_ids),
num_glyphs=len(glyph_ids) + 1)
postamble = """
end readonly def

/BuildGlyph {
exch begin
CharStrings exch
2 copy known not {pop /.notdef} if
true 3 1 roll get exec
end
} d

/BuildChar {
1 index /Encoding get exch get
1 index /BuildGlyph get exec
} d

FontName currentdict end definefont pop
"""

entries = []
for glyph_id in glyph_ids:
g = font.load_glyph(glyph_id, LOAD_NO_SCALE)
v, c = font.get_path()
entries.append(
"/%(name)s{%(bbox)s sc\n" % {
"name": font.get_glyph_name(glyph_id),
"bbox": " ".join(map(str, [g.horiAdvance, 0, *g.bbox])),
}
+ _path.convert_to_string(
# Convert back to TrueType's internal units (1/64's).
# (Other dimensions are already in these units.)
Path(v * 64, c), None, None, False, None, 0,
# No code for quad Beziers triggers auto-conversion to cubics.
# Drop intermediate closepolys (relying on the outline
# decomposer always explicitly moving to the closing point
# first).
[b"m", b"l", b"", b"c", b""], True).decode("ascii")
+ "ce} d"
)

return preamble + "\n".join(entries) + postamble


class RendererPS(_backend_pdf_ps.RendererPDFPSBase):
"""
The renderer handles all the drawing primitives using a graphics
Expand Down Expand Up @@ -922,22 +1002,18 @@ def print_figure_impl(fh):
# Can't use more than 255 chars from a single Type 3 font.
if len(glyph_ids) > 255:
fonttype = 42
# The ttf to ps (subsetting) support doesn't work for
# OpenType fonts that are Postscript inside (like the STIX
# fonts). This will simply turn that off to avoid errors.
if is_opentype_cff_font(font_path):
raise RuntimeError(
"OpenType CFF fonts can not be saved using "
"the internal Postscript backend at this "
"time; consider using the Cairo backend")
fh.flush()
try:
convert_ttf_to_ps(os.fsencode(font_path),
fh, fonttype, glyph_ids)
except RuntimeError:
_log.warning("The PostScript backend does not "
"currently support the selected font.")
raise
if fonttype == 3:
fh.write(_font_to_ps_type3(font_path, glyph_ids))
else:
try:
convert_ttf_to_ps(os.fsencode(font_path),
fh, fonttype, glyph_ids)
except RuntimeError:
_log.warning(
"The PostScript backend does not currently "
"support the selected font.")
raise
print("end", file=fh)
print("%%EndProlog", file=fh)

Expand Down Expand Up @@ -1312,30 +1388,36 @@ def pstoeps(tmpfile, bbox=None, rotated=False):
# The usage comments use the notation of the operator summary
# in the PostScript Language reference manual.
psDefs = [
# name proc *d* -
"/d { bind def } bind def",
# x y *m* -
"/m { moveto } bind def",
"/m { moveto } d",
# x y *l* -
"/l { lineto } bind def",
"/l { lineto } d",
# x y *r* -
"/r { rlineto } bind def",
"/r { rlineto } d",
# x1 y1 x2 y2 x y *c* -
"/c { curveto } bind def",
# *closepath* -
"/cl { closepath } bind def",
"/c { curveto } d",
# *cl* -
"/cl { closepath } d",
# *ce* -
"/ce { closepath eofill } d",
# w h x y *box* -
"""/box {
m
1 index 0 r
0 exch r
neg 0 r
cl
} bind def""",
} d""",
# w h x y *clipbox* -
"""/clipbox {
box
clip
newpath
} bind def""",
} d""",
# wx wy llx lly urx ury *setcachedevice* -
"/sc { setcachedevice } d",
]


Expand Down
112 changes: 112 additions & 0 deletions lib/matplotlib/tests/baseline_images/test_backend_ps/type3.eps
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
%!PS-Adobe-3.0 EPSF-3.0
%%Orientation: portrait
%%BoundingBox: 18.0 180.0 594.0 612.0
%%EndComments
%%BeginProlog
/mpldict 11 dict def
mpldict begin
/d { bind def } bind def
/m { moveto } d
/l { lineto } d
/r { rlineto } d
/c { curveto } d
/cl { closepath } d
/ce { closepath eofill } d
/box {
m
1 index 0 r
0 exch r
neg 0 r
cl
} d
/clipbox {
box
clip
newpath
} d
/sc { setcachedevice } d
%!PS-Adobe-3.0 Resource-Font
%%Creator: Converted from TrueType to Type 3 by Matplotlib.
10 dict begin
/FontName /DejaVuSans def
/PaintType 0 def
/FontMatrix [0.00048828125 0 0 0.00048828125 0 0] def
/FontBBox [-2090 -948 3673 2524] def
/FontType 3 def
/Encoding [/I /J /slash] def
/CharStrings 4 dict dup begin
/.notdef 0 def
/I{604 0 201 0 403 1493 sc
201 1493 m
403 1493 l
403 0 l
201 0 l
201 1493 l

ce} d
/J{604 0 -106 -410 403 1493 sc
201 1493 m
403 1493 l
403 104 l
403 -76 369 -207 300 -288 c
232 -369 122 -410 -29 -410 c
-106 -410 l
-106 -240 l
-43 -240 l
46 -240 109 -215 146 -165 c
183 -115 201 -25 201 104 c
201 1493 l

ce} d
/slash{690 0 0 -190 690 1493 sc
520 1493 m
690 1493 l
170 -190 l
0 -190 l
520 1493 l

ce} d
end readonly def

/BuildGlyph {
exch begin
CharStrings exch
2 copy known not {pop /.notdef} if
true 3 1 roll get exec
end
} d

/BuildChar {
1 index /Encoding get exch get
1 index /BuildGlyph get exec
} d

FontName currentdict end definefont pop
end
%%EndProlog
mpldict begin
18 180 translate
576 432 0 0 clipbox
gsave
0 0 m
576 0 l
576 432 l
0 432 l
cl
1.000 setgray
fill
grestore
0.000 setgray
/DejaVuSans findfont
12.000 scalefont
setfont
gsave
288.000000 216.000000 translate
0.000000 rotate
0.000000 0 m /I glyphshow
3.539062 0 m /slash glyphshow
7.582031 0 m /J glyphshow
grestore

end
showpage
5 changes: 5 additions & 0 deletions lib/matplotlib/tests/test_backend_ps.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,8 @@ def test_useafm():
ax.set_axis_off()
ax.axhline(.5)
ax.text(.5, .5, "qk")


@image_comparison(["type3.eps"])
def test_type3_font():
plt.figtext(.5, .5, "I/J")
7 changes: 2 additions & 5 deletions lib/matplotlib/tests/test_font_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,8 @@ def test_find_ttc():

fig, ax = plt.subplots()
ax.text(.5, .5, "\N{KANGXI RADICAL DRAGON}", fontproperties=fp)
fig.savefig(BytesIO(), format="raw")
fig.savefig(BytesIO(), format="svg")
fig.savefig(BytesIO(), format="pdf")
with pytest.raises(RuntimeError):
fig.savefig(BytesIO(), format="ps")
for fmt in ["raw", "svg", "pdf", "ps"]:
fig.savefig(BytesIO(), format=fmt)


def test_find_invalid(tmpdir):
Expand Down