Skip to content

Commit 48be1ca

Browse files
committed
Rework mapping of dvi glyph indices to freetype indices.
In 89a7e19, an API for converting "dvi glyph indices" (as stored in a dvi file) to FreeType-compatible keys (either "indices into the native charmap" or "glyph names") was introduced. It was intended that end users (i.e., backends) would check the type of `text.glyph_name_or_index` ((A) int or (B) str) and load the glyph accordingly ((A) `FT_Set_Charmap(native_cmap); FT_Load_Char(index);` or (B) `FT_Load_Glyph(FT_Get_Name_Index(name));`); however, with the future introduction of {xe,lua}tex support, this kind of type checking becomes inconvenient, because {xe,lua}tex's "dvi glyph indices", which are directly equal to FreeType glyph indices (i.e. they would be loaded with `FT_Load_Glyph(index);`), would normally also be converted to ints. This PR introduces a new API (`Text.index`) that performs this mapping (via the new `DviFont._index_dvi_to_freetype`), always mapping to FreeType glyph indices (i.e. one can always just call `FT_Load_Glyph` on the result). To do so, in case (A) it loads itself the native charmap (something the end user needed to do by themselves previously) and performs the cmap-to-index conversion (`FT_Get_Char_Index`) previously implicit in `FT_Load_Char`; in case (B) it performs itself the name-to-index conversion (`FT_Get_Name_Index`). When {xe,lua}tex support is introduced in the future, `_index_dvi_to_freetype` will just return the index as is. The old APIs are not deprecated yet, as other changes will occur for {xe,lua}tex support (e.g. font_effects will go away and be replaced by `.font.effects`, as {xe,lua}tex don't store that info in the pdftexmap entry), and grouping all API changes together seems nicer (to only require a single adaptation step by the API consumer).
1 parent b7e7663 commit 48be1ca

File tree

3 files changed

+45
-23
lines changed

3 files changed

+45
-23
lines changed

lib/matplotlib/dviread.py

+42-12
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
import numpy as np
3333

34-
from matplotlib import _api, cbook
34+
from matplotlib import _api, cbook, font_manager
3535

3636
_log = logging.getLogger(__name__)
3737

@@ -71,8 +71,8 @@ class Text(namedtuple('Text', 'x y font glyph width')):
7171
*glyph*, and *width* attributes are kept public for back-compatibility,
7272
but users wanting to draw the glyph themselves are encouraged to instead
7373
load the font specified by `font_path` at `font_size`, warp it with the
74-
effects specified by `font_effects`, and load the glyph specified by
75-
`glyph_name_or_index`.
74+
effects specified by `font_effects`, and load the glyph at the FreeType
75+
glyph `index`.
7676
"""
7777

7878
def _get_pdftexmap_entry(self):
@@ -105,6 +105,14 @@ def font_effects(self):
105105
return self._get_pdftexmap_entry().effects
106106

107107
@property
108+
def index(self):
109+
"""
110+
The FreeType index of this glyph (that can be passed to FT_Load_Glyph).
111+
"""
112+
# See DviFont._index_dvi_to_freetype for details on the index mapping.
113+
return self.font._index_dvi_to_freetype(self.glyph)
114+
115+
@property # To be deprecated together with font_size, font_effects.
108116
def glyph_name_or_index(self):
109117
"""
110118
Either the glyph name or the native charmap glyph index.
@@ -579,7 +587,7 @@ class DviFont:
579587
Size of the font in Adobe points, converted from the slightly
580588
smaller TeX points.
581589
"""
582-
__slots__ = ('texname', 'size', '_scale', '_vf', '_tfm')
590+
__slots__ = ('texname', 'size', '_scale', '_vf', '_tfm', '_encoding')
583591

584592
def __init__(self, scale, tfm, texname, vf):
585593
_api.check_isinstance(bytes, texname=texname)
@@ -588,6 +596,7 @@ def __init__(self, scale, tfm, texname, vf):
588596
self.texname = texname
589597
self._vf = vf
590598
self.size = scale * (72.0 / (72.27 * 2**16))
599+
self._encoding = None
591600

592601
widths = _api.deprecated("3.11")(property(lambda self: [
593602
(1000 * self._tfm.width.get(char, 0)) >> 20
@@ -630,6 +639,33 @@ def _height_depth_of(self, char):
630639
hd[-1] = 0
631640
return hd
632641

642+
def _index_dvi_to_freetype(self, idx):
643+
"""Convert dvi glyph indices to FreeType ones."""
644+
# Glyphs indices stored in the dvi file map to FreeType glyph indices
645+
# (i.e., which can be passed to FT_Load_Glyph) in various ways:
646+
# - if pdftex.map specifies an ".enc" file for the font, that file maps
647+
# dvi indices to Adobe glyph names, which can then be converted to
648+
# FreeType glyph indices with FT_Get_Name_Index.
649+
# - if no ".enc" file is specified, then the font must be a Type 1
650+
# font, and dvi indices directly index into the font's CharStrings
651+
# vector.
652+
# - (xetex & luatex, currently unsupported, can also declare "native
653+
# fonts", for which dvi indices are equal to FreeType indices.)
654+
if self._encoding is None:
655+
psfont = PsfontsMap(find_tex_file("pdftex.map"))[self.texname]
656+
if psfont.filename is None:
657+
raise ValueError("No usable font file found for {} ({}); "
658+
"the font may lack a Type-1 version"
659+
.format(psfont.psname.decode("ascii"),
660+
psfont.texname.decode("ascii")))
661+
face = font_manager.get_font(psfont.filename)
662+
if psfont.encoding:
663+
self._encoding = [face.get_name_index(name)
664+
for name in _parse_enc(psfont.encoding)]
665+
else:
666+
self._encoding = face._get_type1_encoding_vector()
667+
return self._encoding[idx]
668+
633669

634670
class Vf(Dvi):
635671
r"""
@@ -1023,8 +1059,7 @@ def _parse_enc(path):
10231059
Returns
10241060
-------
10251061
list
1026-
The nth entry of the list is the PostScript glyph name of the nth
1027-
glyph.
1062+
The nth list item is the PostScript glyph name of the nth glyph.
10281063
"""
10291064
no_comments = re.sub("%.*", "", Path(path).read_text(encoding="ascii"))
10301065
array = re.search(r"(?s)\[(.*)\]", no_comments).group(1)
@@ -1156,12 +1191,7 @@ def _print_fields(*args):
11561191
face = FT2Font(fontpath)
11571192
_print_fields("x", "y", "glyph", "chr", "w")
11581193
for text in group:
1159-
if psfont.encoding:
1160-
glyph_name = _parse_enc(psfont.encoding)[text.glyph]
1161-
else:
1162-
encoding_vector = face._get_type1_encoding_vector()
1163-
glyph_name = face.get_glyph_name(encoding_vector[text.glyph])
1164-
glyph_str = fontTools.agl.toUnicode(glyph_name)
1194+
glyph_str = fontTools.agl.toUnicode(face.get_glyph_name(text.index))
11651195
_print_fields(text.x, text.y, text.glyph, glyph_str, text.width)
11661196
if page.boxes:
11671197
print("--- BOXES ---")

lib/matplotlib/dviread.pyi

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class Text(NamedTuple):
4141
@property
4242
def font_effects(self) -> dict[str, float]: ...
4343
@property
44+
def index(self) -> int: ... # type: ignore[override]
45+
@property
4446
def glyph_name_or_index(self) -> int | str: ...
4547

4648
class Dvi:

lib/matplotlib/textpath.py

+1-11
Original file line numberDiff line numberDiff line change
@@ -239,17 +239,7 @@ def get_glyphs_tex(self, prop, s, glyph_map=None,
239239
if char_id not in glyph_map:
240240
font.clear()
241241
font.set_size(self.FONT_SCALE, self.DPI)
242-
glyph_name_or_index = text.glyph_name_or_index
243-
if isinstance(glyph_name_or_index, str):
244-
index = font.get_name_index(glyph_name_or_index)
245-
elif isinstance(glyph_name_or_index, int):
246-
if font not in t1_encodings:
247-
t1_encodings[font] = font._get_type1_encoding_vector()
248-
index = t1_encodings[font][glyph_name_or_index]
249-
else: # Should not occur.
250-
raise TypeError(f"Glyph spec of unexpected type: "
251-
f"{glyph_name_or_index!r}")
252-
font.load_glyph(index, flags=LoadFlags.TARGET_LIGHT)
242+
font.load_glyph(text.index, flags=LoadFlags.TARGET_LIGHT)
253243
glyph_map_new[char_id] = font.get_path()
254244

255245
glyph_ids.append(char_id)

0 commit comments

Comments
 (0)