diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 18ed7c239e74..344315d54b88 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -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 @@ -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)) + 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 @@ -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) @@ -1312,16 +1388,20 @@ 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 @@ -1329,13 +1409,15 @@ def pstoeps(tmpfile, bbox=None, rotated=False): 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", ] diff --git a/lib/matplotlib/tests/baseline_images/test_backend_ps/type3.eps b/lib/matplotlib/tests/baseline_images/test_backend_ps/type3.eps new file mode 100644 index 000000000000..9c9645b47cf0 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_backend_ps/type3.eps @@ -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 diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index e303e51c84a4..09c953337d45 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -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") diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index 74af2a7fd570..a471c866a555 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -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):