From 89a7e19210fe1d283de0fe8d5c41a61042d71cff Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 16 Jan 2022 19:52:35 +0100 Subject: [PATCH] Improve usability of dviread.Text by third parties. dviread.Text specifies individual glyphs in parsed dvi files; this is used by the pdf and svg backends (via textpath, for svg) to parse the result of usetex-compiled strings and embed the right glyphs at the right places in the output file (as a reminder, agg currenly uses dvipng to rasterize the dvi file, and the ps backend relies on pstricks). Unfortunately, if third-party backends (e.g. mplcairo) want to do use the same strategy as pdf/svg, then they need to jump through a few hoops (e.g. understand PsfontsMap and find_tex_font) and use some private APIs (_parse_enc), as explained below. Try to improve the situation, by adding some methods on the Text class (which may later allow deprecating PsfontsMap and find_tex_font as public API). First, the actual font to be used is speficied via the `.font` attribute, which itself has a `.texname` which itself must be used as a key into `PsfontsMap(find_tex_font("pdftex.map"))` to get the actual font path (and some other info). Instead, just provide a `.font_path` property. pdftex.map can also require that the font be "slanted" or "extended" (that's some of the "other info), lift that as well to `.font_effects`. The `.font` attribute has a `.size`; also we can lift up to `.font_size`. Altogether, this will allow making `.font` private (later) -- except for the fact that Text is a namedtuple so deprecating fields is rather annoying. The trickiest part is actually specifying what glyph to select from the font. dvi just gives us an integer, whose interpretation depends again on pdftex.map. If pdftex.map specifies "no encoding" for the font, then the integer is the "native glyph index" of the font, and should be passed as-is to FreeType's FT_Load_Char (after selecting the native charmap). If pdftex.map does specify an encoding, then that's a file that needs to be separately loaded, parsed (with _parse_enc), and ultimately converts the index into a glyph name (which can be passed to FreeType's FT_Load_Glyph). Altogether, from the PoV of the end user, the "glyph specification" is thus either an int (a native charmap index) or a str (a glyph name); thus, provide `.glyph_name_or_index` which is that API. As an example of using that API, see the changes to textpath.py. --- lib/matplotlib/dviread.py | 63 +++++++++++++++++++++- lib/matplotlib/tests/test_usetex.py | 2 +- lib/matplotlib/textpath.py | 84 +++++++++++------------------ 3 files changed, 93 insertions(+), 56 deletions(-) diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 7f90a13f1086..28d80ed4dc47 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -58,10 +58,71 @@ # The marks on a page consist of text and boxes. A page also has dimensions. Page = namedtuple('Page', 'text boxes height width descent') -Text = namedtuple('Text', 'x y font glyph width') Box = namedtuple('Box', 'x y height width') +# Also a namedtuple, for backcompat. +class Text(namedtuple('Text', 'x y font glyph width')): + """ + A glyph in the dvi file. + + The *x* and *y* attributes directly position the glyph. The *font*, + *glyph*, and *width* attributes are kept public for back-compatibility, + but users wanting to draw the glyph themselves are encouraged to instead + load the font specified by `font_path` at `font_size`, warp it with the + effects specified by `font_effects`, and load the glyph specified by + `glyph_name_or_index`. + """ + + def _get_pdftexmap_entry(self): + return PsfontsMap(find_tex_file("pdftex.map"))[self.font.texname] + + @property + def font_path(self): + """The `~pathlib.Path` to the font for this glyph.""" + psfont = self._get_pdftexmap_entry() + if psfont.filename is None: + raise ValueError("No usable font file found for {} ({}); " + "the font may lack a Type-1 version" + .format(psfont.psname.decode("ascii"), + psfont.texname.decode("ascii"))) + return Path(psfont.filename) + + @property + def font_size(self): + """The font size.""" + return self.font.size + + @property + def font_effects(self): + """ + The "font effects" dict for this glyph. + + This dict contains the values for this glyph of SlantFont and + ExtendFont (if any), read off :file:`pdftex.map`. + """ + return self._get_pdftexmap_entry().effects + + @property + def glyph_name_or_index(self): + """ + Either the glyph name or the native charmap glyph index. + + If :file:`pdftex.map` specifies an encoding for this glyph's font, that + is a mapping of glyph indices to Adobe glyph names; use it to convert + dvi indices to glyph names. Callers can then convert glyph names to + glyph indices (with FT_Get_Name_Index/get_name_index), and load the + glyph using FT_Load_Glyph/load_glyph. + + If :file:`pdftex.map` specifies no encoding, the indices directly map + to the font's "native" charmap; glyphs should directly loaded using + FT_Load_Char/load_char after selecting the native charmap. + """ + entry = self._get_pdftexmap_entry() + return (_parse_enc(entry.encoding)[self.glyph] + if entry.encoding is not None else self.glyph) + + # Opcode argument parsing # # Each of the following functions takes a Dvi object and delta, diff --git a/lib/matplotlib/tests/test_usetex.py b/lib/matplotlib/tests/test_usetex.py index b726f9c26cfb..9133d5165c2e 100644 --- a/lib/matplotlib/tests/test_usetex.py +++ b/lib/matplotlib/tests/test_usetex.py @@ -131,7 +131,7 @@ def test_missing_psfont(fmt, monkeypatch): monkeypatch.setattr( dviread.PsfontsMap, '__getitem__', lambda self, k: dviread.PsFont( - texname='texfont', psname='Some Font', + texname=b'texfont', psname=b'Some Font', effects=None, encoding=None, filename=None)) mpl.rcParams['text.usetex'] = True fig, ax = plt.subplots() diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index 49be4a577352..7364aa304fa8 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -1,5 +1,4 @@ from collections import OrderedDict -import functools import logging import urllib.parse @@ -243,25 +242,29 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, # Gather font information and do some setup for combining # characters into strings. - for x1, y1, dvifont, glyph, width in page.text: - font, enc = self._get_ps_font_and_encoding(dvifont.texname) - char_id = self._get_char_id(font, glyph) - + for text in page.text: + font = get_font(text.font_path) + char_id = self._get_char_id(font, text.glyph) if char_id not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) - # See comments in _get_ps_font_and_encoding. - if enc is not None: - index = font.get_name_index(enc[glyph]) + glyph_name_or_index = text.glyph_name_or_index + if isinstance(glyph_name_or_index, str): + index = font.get_name_index(glyph_name_or_index) font.load_glyph(index, flags=LOAD_TARGET_LIGHT) - else: - font.load_char(glyph, flags=LOAD_TARGET_LIGHT) + elif isinstance(glyph_name_or_index, int): + self._select_native_charmap(font) + font.load_char( + glyph_name_or_index, flags=LOAD_TARGET_LIGHT) + else: # Should not occur. + raise TypeError(f"Glyph spec of unexpected type: " + f"{glyph_name_or_index!r}") glyph_map_new[char_id] = font.get_path() glyph_ids.append(char_id) - xpositions.append(x1) - ypositions.append(y1) - sizes.append(dvifont.size / self.FONT_SCALE) + xpositions.append(text.x) + ypositions.append(text.y) + sizes.append(text.font_size / self.FONT_SCALE) myrects = [] @@ -277,48 +280,21 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, glyph_map_new, myrects) @staticmethod - @functools.lru_cache(50) - def _get_ps_font_and_encoding(texname): - tex_font_map = dviread.PsfontsMap(dviread._find_tex_file('pdftex.map')) - psfont = tex_font_map[texname] - if psfont.filename is None: - raise ValueError( - f"No usable font file found for {psfont.psname} ({texname}). " - f"The font may lack a Type-1 version.") - - font = get_font(psfont.filename) - - if psfont.encoding: - # If psfonts.map specifies an encoding, use it: it gives us a - # mapping of glyph indices to Adobe glyph names; use it to convert - # dvi indices to glyph names and use the FreeType-synthesized - # Unicode charmap to convert glyph names to glyph indices (with - # FT_Get_Name_Index/get_name_index), and load the glyph using - # FT_Load_Glyph/load_glyph. (That charmap has a coverage at least - # as good as, and possibly better than, the native charmaps.) - enc = dviread._parse_enc(psfont.encoding) - else: - # If psfonts.map specifies no encoding, the indices directly - # map to the font's "native" charmap; so don't use the - # FreeType-synthesized charmap but the native ones (we can't - # directly identify it but it's typically an Adobe charmap), and - # directly load the dvi glyph indices using FT_Load_Char/load_char. - for charmap_code in [ - 1094992451, # ADOBE_CUSTOM. - 1094995778, # ADOBE_STANDARD. - ]: - try: - font.select_charmap(charmap_code) - except (ValueError, RuntimeError): - pass - else: - break + def _select_native_charmap(font): + # Select the native charmap. (we can't directly identify it but it's + # typically an Adobe charmap). + for charmap_code in [ + 1094992451, # ADOBE_CUSTOM. + 1094995778, # ADOBE_STANDARD. + ]: + try: + font.select_charmap(charmap_code) + except (ValueError, RuntimeError): + pass else: - _log.warning("No supported encoding in font (%s).", - psfont.filename) - enc = None - - return font, enc + break + else: + _log.warning("No supported encoding in font (%s).", font.fname) text_to_path = TextToPath()