Skip to content

Commit 45eae37

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 (`_index_dvi_to_freetype`) to perform this mapping, 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. Note that this API is intentionally kept private for now (even though it is used by textpath) and the old APIs are not deprecated yet; I intend to wait until {xe,lua}tex support is actually merged to do so, to avoid possible future back-and-forth changes on the public APIs. In case (A), this PR also improves on the detection of the native charmap, which was previously detected via heuristics (`_select_native_charmap`), but is now read by directly accessing the Type 1 font "encoding vector".
1 parent 7d5d027 commit 45eae37

File tree

3 files changed

+65
-40
lines changed

3 files changed

+65
-40
lines changed

lib/matplotlib/dviread.py

+35-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

@@ -579,7 +579,7 @@ class DviFont:
579579
Size of the font in Adobe points, converted from the slightly
580580
smaller TeX points.
581581
"""
582-
__slots__ = ('texname', 'size', '_scale', '_vf', '_tfm')
582+
__slots__ = ('texname', 'size', '_scale', '_vf', '_tfm', '_encoding')
583583

584584
def __init__(self, scale, tfm, texname, vf):
585585
_api.check_isinstance(bytes, texname=texname)
@@ -588,6 +588,7 @@ def __init__(self, scale, tfm, texname, vf):
588588
self.texname = texname
589589
self._vf = vf
590590
self.size = scale * (72.0 / (72.27 * 2**16))
591+
self._encoding = None
591592

592593
widths = _api.deprecated("3.11")(property(lambda self: [
593594
(1000 * self._tfm.width.get(char, 0)) >> 20
@@ -630,6 +631,35 @@ def _height_depth_of(self, char):
630631
hd[-1] = 0
631632
return hd
632633

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

634664
class Vf(Dvi):
635665
r"""
@@ -1023,8 +1053,7 @@ def _parse_enc(path):
10231053
Returns
10241054
-------
10251055
list
1026-
The nth entry of the list is the PostScript glyph name of the nth
1027-
glyph.
1056+
The nth list item is the PostScript glyph name of the nth glyph.
10281057
"""
10291058
no_comments = re.sub("%.*", "", Path(path).read_text(encoding="ascii"))
10301059
array = re.search(r"(?s)\[(.*)\]", no_comments).group(1)
@@ -1132,7 +1161,6 @@ def _fontfile(cls, suffix, texname):
11321161
import fontTools.agl
11331162

11341163
from matplotlib.ft2font import FT2Font
1135-
from matplotlib.textpath import TextToPath
11361164

11371165
parser = ArgumentParser()
11381166
parser.add_argument("filename")
@@ -1155,15 +1183,10 @@ def _print_fields(*args):
11551183
print(f"font: {font.texname.decode('latin-1')} "
11561184
f"(scale: {font._scale / 2 ** 20}) at {fontpath}")
11571185
face = FT2Font(fontpath)
1158-
TextToPath._select_native_charmap(face)
11591186
_print_fields("x", "y", "glyph", "chr", "w")
11601187
for text in group:
1161-
if psfont.encoding:
1162-
glyph_name = _parse_enc(psfont.encoding)[text.glyph]
1163-
else:
1164-
glyph_name = face.get_glyph_name(
1165-
face.get_char_index(text.glyph))
1166-
glyph_str = fontTools.agl.toUnicode(glyph_name)
1188+
glyph_str = fontTools.agl.toUnicode(
1189+
face.get_glyph_name(font._index_dvi_to_freetype(text.glyph)))
11671190
_print_fields(text.x, text.y, text.glyph, glyph_str, text.width)
11681191
if page.boxes:
11691192
print("--- BOXES ---")

lib/matplotlib/textpath.py

+2-28
Original file line numberDiff line numberDiff line change
@@ -238,17 +238,8 @@ def get_glyphs_tex(self, prop, s, glyph_map=None,
238238
if char_id not in glyph_map:
239239
font.clear()
240240
font.set_size(self.FONT_SCALE, self.DPI)
241-
glyph_name_or_index = text.glyph_name_or_index
242-
if isinstance(glyph_name_or_index, str):
243-
index = font.get_name_index(glyph_name_or_index)
244-
font.load_glyph(index, flags=LoadFlags.TARGET_LIGHT)
245-
elif isinstance(glyph_name_or_index, int):
246-
self._select_native_charmap(font)
247-
font.load_char(
248-
glyph_name_or_index, flags=LoadFlags.TARGET_LIGHT)
249-
else: # Should not occur.
250-
raise TypeError(f"Glyph spec of unexpected type: "
251-
f"{glyph_name_or_index!r}")
241+
idx = text.font._index_dvi_to_freetype(text.glyph)
242+
font.load_glyph(idx, flags=LoadFlags.TARGET_LIGHT)
252243
glyph_map_new[char_id] = font.get_path()
253244

254245
glyph_ids.append(char_id)
@@ -269,23 +260,6 @@ def get_glyphs_tex(self, prop, s, glyph_map=None,
269260
return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
270261
glyph_map_new, myrects)
271262

272-
@staticmethod
273-
def _select_native_charmap(font):
274-
# Select the native charmap. (we can't directly identify it but it's
275-
# typically an Adobe charmap).
276-
for charmap_code in [
277-
1094992451, # ADOBE_CUSTOM.
278-
1094995778, # ADOBE_STANDARD.
279-
]:
280-
try:
281-
font.select_charmap(charmap_code)
282-
except (ValueError, RuntimeError):
283-
pass
284-
else:
285-
break
286-
else:
287-
_log.warning("No supported encoding in font (%s).", font.fname)
288-
289263

290264
text_to_path = TextToPath()
291265

src/ft2font_wrapper.cpp

+28
Original file line numberDiff line numberDiff line change
@@ -1431,6 +1431,32 @@ PyFT2Font_get_image(PyFT2Font *self)
14311431
return py::array_t<unsigned char>(dims, im.get_buffer());
14321432
}
14331433

1434+
const char *PyFT2Font__get_type1_encoding_vector__doc__ = R"""(
1435+
Return a list mapping CharString indices of a Type 1 font to FreeType glyph indices.
1436+
1437+
Returns
1438+
-------
1439+
list[int]
1440+
)""";
1441+
1442+
static std::array<FT_UInt, 256>
1443+
PyFT2Font__get_type1_encoding_vector(PyFT2Font *self)
1444+
{
1445+
auto face = self->x->get_face();
1446+
auto indices = std::array<FT_UInt, 256>{};
1447+
for (auto i = 0; i < indices.size(); ++i) {
1448+
auto len = FT_Get_PS_Font_Value(face, PS_DICT_ENCODING_ENTRY, i, nullptr, 0);
1449+
if (len == -1) {
1450+
throw std::runtime_error{
1451+
"FT_Get_PS_Font_Value tried to access a non-existent value"};
1452+
}
1453+
auto buf = std::unique_ptr<char[]>{new char[len]};
1454+
FT_Get_PS_Font_Value(face, PS_DICT_ENCODING_ENTRY, i, buf.get(), len);
1455+
indices[i] = FT_Get_Name_Index(face, buf.get());
1456+
}
1457+
return indices;
1458+
}
1459+
14341460
static const char *
14351461
PyFT2Font_postscript_name(PyFT2Font *self)
14361462
{
@@ -1761,6 +1787,8 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
17611787
PyFT2Font_get_sfnt_table__doc__)
17621788
.def("get_path", &PyFT2Font_get_path, PyFT2Font_get_path__doc__)
17631789
.def("get_image", &PyFT2Font_get_image, PyFT2Font_get_image__doc__)
1790+
.def("_get_type1_encoding_vector", &PyFT2Font__get_type1_encoding_vector,
1791+
PyFT2Font__get_type1_encoding_vector__doc__)
17641792

17651793
.def_property_readonly("postscript_name", &PyFT2Font_postscript_name,
17661794
"PostScript name of the font.")

0 commit comments

Comments
 (0)