Skip to content

Commit e5b6f70

Browse files
committed
TYP: Make glyph indices distinct from character codes
Previously, these were both typed as `int`, which means you could mix and match them erroneously. While the character code can't be made a distinct type (because it's used for `chr`/`ord`), typing glyph indices as a distinct type means these can't be fully swapped. Unfortunately, you can still go back to the base type, so glyph indices still work as character codes. But this is still sufficient to catch errors such as the wrong call to `FT2Font.get_kerning` in `_mathtext.py`.
1 parent 263a5c5 commit e5b6f70

File tree

9 files changed

+71
-43
lines changed

9 files changed

+71
-43
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Glyph indices now typed distinctly from character codes
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
Previously, character codes and glyph indices were both typed as `int`, which means you
5+
could mix and match them erroneously. While the character code can't be made a distinct
6+
type (because it's used for `chr`/`ord`), typing glyph indices as a distinct type means
7+
these can't be fully swapped.

lib/matplotlib/_afm.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@
3030
import inspect
3131
import logging
3232
import re
33-
from typing import BinaryIO, NamedTuple, TypedDict
33+
from typing import BinaryIO, NamedTuple, TypedDict, cast
3434

3535
from ._mathtext_data import uni2type1
36+
from .ft2font import CharacterCodeType, GlyphIndexType
3637

3738

3839
_log = logging.getLogger(__name__)
@@ -194,7 +195,7 @@ class CharMetrics(NamedTuple):
194195
#: The bbox of the character (B) as a tuple (*llx*, *lly*, *urx*, *ury*).
195196

196197

197-
def _parse_char_metrics(fh: BinaryIO) -> tuple[dict[int, CharMetrics],
198+
def _parse_char_metrics(fh: BinaryIO) -> tuple[dict[CharacterCodeType, CharMetrics],
198199
dict[str, CharMetrics]]:
199200
"""
200201
Parse the given filehandle for character metrics information.
@@ -215,7 +216,7 @@ def _parse_char_metrics(fh: BinaryIO) -> tuple[dict[int, CharMetrics],
215216
"""
216217
required_keys = {'C', 'WX', 'N', 'B'}
217218

218-
ascii_d: dict[int, CharMetrics] = {}
219+
ascii_d: dict[CharacterCodeType, CharMetrics] = {}
219220
name_d: dict[str, CharMetrics] = {}
220221
for bline in fh:
221222
# We are defensively letting values be utf8. The spec requires
@@ -405,19 +406,21 @@ def get_str_bbox_and_descent(self, s: str) -> tuple[int, int, float, int, int]:
405406

406407
return left, miny, total_width, maxy - miny, -miny
407408

408-
def get_glyph_name(self, glyph_ind: int) -> str: # For consistency with FT2Font.
409+
def get_glyph_name(self, # For consistency with FT2Font.
410+
glyph_ind: GlyphIndexType) -> str:
409411
"""Get the name of the glyph, i.e., ord(';') is 'semicolon'."""
410-
return self._metrics[glyph_ind].name
412+
return self._metrics[cast(CharacterCodeType, glyph_ind)].name
411413

412-
def get_char_index(self, c: int) -> int: # For consistency with FT2Font.
414+
def get_char_index(self, # For consistency with FT2Font.
415+
c: CharacterCodeType) -> GlyphIndexType:
413416
"""
414417
Return the glyph index corresponding to a character code point.
415418
416419
Note, for AFM fonts, we treat the glyph index the same as the codepoint.
417420
"""
418-
return c
421+
return cast(GlyphIndexType, c)
419422

420-
def get_width_char(self, c: int) -> float:
423+
def get_width_char(self, c: CharacterCodeType) -> float:
421424
"""Get the width of the character code from the character metric WX field."""
422425
return self._metrics[c].width
423426

lib/matplotlib/_mathtext.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737

3838
if T.TYPE_CHECKING:
3939
from collections.abc import Iterable
40-
from .ft2font import Glyph
40+
from .ft2font import CharacterCodeType, Glyph
41+
4142

4243
ParserElement.enable_packrat()
4344
_log = logging.getLogger("matplotlib.mathtext")
@@ -47,7 +48,7 @@
4748
# FONTS
4849

4950

50-
def get_unicode_index(symbol: str) -> int: # Publicly exported.
51+
def get_unicode_index(symbol: str) -> CharacterCodeType: # Publicly exported.
5152
r"""
5253
Return the integer index (from the Unicode table) of *symbol*.
5354
@@ -85,7 +86,7 @@ class VectorParse(NamedTuple):
8586
width: float
8687
height: float
8788
depth: float
88-
glyphs: list[tuple[FT2Font, float, int, float, float]]
89+
glyphs: list[tuple[FT2Font, float, CharacterCodeType, float, float]]
8990
rects: list[tuple[float, float, float, float]]
9091

9192
VectorParse.__module__ = "matplotlib.mathtext"
@@ -212,7 +213,7 @@ class FontInfo(NamedTuple):
212213
fontsize: float
213214
postscript_name: str
214215
metrics: FontMetrics
215-
num: int
216+
num: CharacterCodeType
216217
glyph: Glyph
217218
offset: float
218219

@@ -365,7 +366,7 @@ def _get_offset(self, font: FT2Font, glyph: Glyph, fontsize: float,
365366
return 0.
366367

367368
def _get_glyph(self, fontname: str, font_class: str,
368-
sym: str) -> tuple[FT2Font, int, bool]:
369+
sym: str) -> tuple[FT2Font, CharacterCodeType, bool]:
369370
raise NotImplementedError
370371

371372
# The return value of _get_info is cached per-instance.
@@ -459,7 +460,7 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag
459460
_slanted_symbols = set(r"\int \oint".split())
460461

461462
def _get_glyph(self, fontname: str, font_class: str,
462-
sym: str) -> tuple[FT2Font, int, bool]:
463+
sym: str) -> tuple[FT2Font, CharacterCodeType, bool]:
463464
font = None
464465
if fontname in self.fontmap and sym in latex_to_bakoma:
465466
basename, num = latex_to_bakoma[sym]
@@ -551,7 +552,7 @@ class UnicodeFonts(TruetypeFonts):
551552
# Some glyphs are not present in the `cmr10` font, and must be brought in
552553
# from `cmsy10`. Map the Unicode indices of those glyphs to the indices at
553554
# which they are found in `cmsy10`.
554-
_cmr10_substitutions = {
555+
_cmr10_substitutions: dict[CharacterCodeType, CharacterCodeType] = {
555556
0x00D7: 0x00A3, # Multiplication sign.
556557
0x2212: 0x00A1, # Minus sign.
557558
}
@@ -594,11 +595,11 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag
594595
_slanted_symbols = set(r"\int \oint".split())
595596

596597
def _map_virtual_font(self, fontname: str, font_class: str,
597-
uniindex: int) -> tuple[str, int]:
598+
uniindex: CharacterCodeType) -> tuple[str, CharacterCodeType]:
598599
return fontname, uniindex
599600

600601
def _get_glyph(self, fontname: str, font_class: str,
601-
sym: str) -> tuple[FT2Font, int, bool]:
602+
sym: str) -> tuple[FT2Font, CharacterCodeType, bool]:
602603
try:
603604
uniindex = get_unicode_index(sym)
604605
found_symbol = True
@@ -607,8 +608,7 @@ def _get_glyph(self, fontname: str, font_class: str,
607608
found_symbol = False
608609
_log.warning("No TeX to Unicode mapping for %a.", sym)
609610

610-
fontname, uniindex = self._map_virtual_font(
611-
fontname, font_class, uniindex)
611+
fontname, uniindex = self._map_virtual_font(fontname, font_class, uniindex)
612612

613613
new_fontname = fontname
614614

@@ -693,7 +693,7 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag
693693
self.fontmap[name] = fullpath
694694

695695
def _get_glyph(self, fontname: str, font_class: str,
696-
sym: str) -> tuple[FT2Font, int, bool]:
696+
sym: str) -> tuple[FT2Font, CharacterCodeType, bool]:
697697
# Override prime symbol to use Bakoma.
698698
if sym == r'\prime':
699699
return self.bakoma._get_glyph(fontname, font_class, sym)
@@ -783,7 +783,7 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag
783783
self.fontmap[name] = fullpath
784784

785785
def _map_virtual_font(self, fontname: str, font_class: str,
786-
uniindex: int) -> tuple[str, int]:
786+
uniindex: CharacterCodeType) -> tuple[str, CharacterCodeType]:
787787
# Handle these "fonts" that are actually embedded in
788788
# other fonts.
789789
font_mapping = stix_virtual_fonts.get(fontname)
@@ -1170,7 +1170,7 @@ def __init__(self, elements: T.Sequence[Node]):
11701170
self.glue_sign = 0 # 0: normal, -1: shrinking, 1: stretching
11711171
self.glue_order = 0 # The order of infinity (0 - 3) for the glue
11721172

1173-
def __repr__(self):
1173+
def __repr__(self) -> str:
11741174
return "{}<w={:.02f} h={:.02f} d={:.02f} s={:.02f}>[{}]".format(
11751175
super().__repr__(),
11761176
self.width, self.height,

lib/matplotlib/_mathtext_data.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
"""
44

55
from __future__ import annotations
6-
from typing import overload
6+
from typing import TypeAlias, overload
77

8-
latex_to_bakoma = {
8+
from .ft2font import CharacterCodeType
9+
10+
11+
latex_to_bakoma: dict[str, tuple[str, CharacterCodeType]] = {
912
'\\__sqrt__' : ('cmex10', 0x70),
1013
'\\bigcap' : ('cmex10', 0x5c),
1114
'\\bigcup' : ('cmex10', 0x5b),
@@ -241,7 +244,7 @@
241244

242245
# Automatically generated.
243246

244-
type12uni = {
247+
type12uni: dict[str, CharacterCodeType] = {
245248
'aring' : 229,
246249
'quotedblright' : 8221,
247250
'V' : 86,
@@ -475,7 +478,7 @@
475478
# for key in sd:
476479
# print("{0:24} : {1: <s},".format("'" + key + "'", sd[key]))
477480

478-
tex2uni = {
481+
tex2uni: dict[str, CharacterCodeType] = {
479482
'#' : 0x23,
480483
'$' : 0x24,
481484
'%' : 0x25,
@@ -1113,8 +1116,9 @@
11131116
# Each element is a 4-tuple of the form:
11141117
# src_start, src_end, dst_font, dst_start
11151118

1116-
_EntryTypeIn = tuple[str, str, str, str | int]
1117-
_EntryTypeOut = tuple[int, int, str, int]
1119+
_EntryTypeIn: TypeAlias = tuple[str, str, str, str | CharacterCodeType]
1120+
_EntryTypeOut: TypeAlias = tuple[CharacterCodeType, CharacterCodeType, str,
1121+
CharacterCodeType]
11181122

11191123
_stix_virtual_fonts: dict[str, dict[str, list[_EntryTypeIn]] | list[_EntryTypeIn]] = {
11201124
'bb': {
@@ -1735,7 +1739,7 @@ def _normalize_stix_fontcodes(d):
17351739
del _stix_virtual_fonts
17361740

17371741
# Fix some incorrect glyphs.
1738-
stix_glyph_fixes = {
1742+
stix_glyph_fixes: dict[CharacterCodeType, CharacterCodeType] = {
17391743
# Cap and Cup glyphs are swapped.
17401744
0x22d2: 0x22d3,
17411745
0x22d3: 0x22d2,

lib/matplotlib/_text_helpers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
import dataclasses
88

99
from . import _api
10-
from .ft2font import FT2Font, Kerning, LoadFlags
10+
from .ft2font import FT2Font, GlyphIndexType, Kerning, LoadFlags
1111

1212

1313
@dataclasses.dataclass(frozen=True)
1414
class LayoutItem:
1515
ft_object: FT2Font
1616
char: str
17-
glyph_idx: int
17+
glyph_idx: GlyphIndexType
1818
x: float
1919
prev_kern: float
2020

lib/matplotlib/dviread.pyi

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ from collections.abc import Generator
88
from typing import NamedTuple
99
from typing_extensions import Self # < Py 3.11
1010

11+
from .ft2font import GlyphIndexType
12+
13+
1114
class _dvistate(Enum):
1215
pre = ...
1316
outer = ...
@@ -41,9 +44,9 @@ class Text(NamedTuple):
4144
@property
4245
def font_effects(self) -> dict[str, float]: ...
4346
@property
44-
def index(self) -> int: ... # type: ignore[override]
47+
def index(self) -> GlyphIndexType: ... # type: ignore[override]
4548
@property
46-
def glyph_name_or_index(self) -> int | str: ...
49+
def glyph_name_or_index(self) -> GlyphIndexType | str: ...
4750

4851
class Dvi:
4952
file: io.BufferedReader

lib/matplotlib/ft2font.pyi

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
from enum import Enum, Flag
22
import sys
3-
from typing import BinaryIO, Literal, TypedDict, final, overload, cast
3+
from typing import BinaryIO, Literal, NewType, TypeAlias, TypedDict, final, overload, cast
44
from typing_extensions import Buffer # < Py 3.12
55

66
import numpy as np
77
from numpy.typing import NDArray
88

9+
910
__freetype_build_type__: str
1011
__freetype_version__: str
1112

13+
# We can't change the type hints for standard library chr/ord, so character codes are a
14+
# simple type alias.
15+
CharacterCodeType: TypeAlias = int
16+
# But glyph indices are internal, so use a distinct type hint.
17+
GlyphIndexType = NewType('GlyphIndexType', int)
18+
1219
class FaceFlags(Flag):
1320
SCALABLE = cast(int, ...)
1421
FIXED_SIZES = cast(int, ...)
@@ -202,13 +209,13 @@ class FT2Font(Buffer):
202209
) -> None: ...
203210
def draw_glyphs_to_bitmap(self, antialiased: bool = ...) -> None: ...
204211
def get_bitmap_offset(self) -> tuple[int, int]: ...
205-
def get_char_index(self, codepoint: int) -> int: ...
206-
def get_charmap(self) -> dict[int, int]: ...
212+
def get_char_index(self, codepoint: CharacterCodeType) -> GlyphIndexType: ...
213+
def get_charmap(self) -> dict[CharacterCodeType, GlyphIndexType]: ...
207214
def get_descent(self) -> int: ...
208-
def get_glyph_name(self, index: int) -> str: ...
215+
def get_glyph_name(self, index: GlyphIndexType) -> str: ...
209216
def get_image(self) -> NDArray[np.uint8]: ...
210-
def get_kerning(self, left: int, right: int, mode: Kerning) -> int: ...
211-
def get_name_index(self, name: str) -> int: ...
217+
def get_kerning(self, left: GlyphIndexType, right: GlyphIndexType, mode: Kerning) -> int: ...
218+
def get_name_index(self, name: str) -> GlyphIndexType: ...
212219
def get_num_glyphs(self) -> int: ...
213220
def get_path(self) -> tuple[NDArray[np.float64], NDArray[np.int8]]: ...
214221
def get_ps_font_info(
@@ -230,8 +237,8 @@ class FT2Font(Buffer):
230237
@overload
231238
def get_sfnt_table(self, name: Literal["pclt"]) -> _SfntPcltDict | None: ...
232239
def get_width_height(self) -> tuple[int, int]: ...
233-
def load_char(self, charcode: int, flags: LoadFlags = ...) -> Glyph: ...
234-
def load_glyph(self, glyphindex: int, flags: LoadFlags = ...) -> Glyph: ...
240+
def load_char(self, charcode: CharacterCodeType, flags: LoadFlags = ...) -> Glyph: ...
241+
def load_glyph(self, glyphindex: GlyphIndexType, flags: LoadFlags = ...) -> Glyph: ...
235242
def select_charmap(self, i: int) -> None: ...
236243
def set_charmap(self, i: int) -> None: ...
237244
def set_size(self, ptsize: float, dpi: float) -> None: ...

lib/matplotlib/tests/test_ft2font.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import itertools
22
import io
33
from pathlib import Path
4+
from typing import cast
45

56
import numpy as np
67
import pytest
@@ -235,7 +236,7 @@ def enc(name):
235236
assert unic == after
236237

237238
# This is just a random sample from FontForge.
238-
glyph_names = {
239+
glyph_names = cast(dict[str, ft2font.GlyphIndexType], {
239240
'non-existent-glyph-name': 0,
240241
'plusminus': 115,
241242
'Racute': 278,
@@ -247,7 +248,7 @@ def enc(name):
247248
'uni2A02': 4464,
248249
'u1D305': 5410,
249250
'u1F0A1': 5784,
250-
}
251+
})
251252
for name, index in glyph_names.items():
252253
assert font.get_name_index(name) == index
253254
if name == 'non-existent-glyph-name':

src/ft2font_wrapper.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1776,5 +1776,8 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
17761776

17771777
m.attr("__freetype_version__") = version_string;
17781778
m.attr("__freetype_build_type__") = FREETYPE_BUILD_TYPE;
1779+
auto py_int = py::module_::import("builtins").attr("int");
1780+
m.attr("CharacterCodeType") = py_int;
1781+
m.attr("GlyphIndexType") = py_int;
17791782
m.def("__getattr__", ft2font__getattr__);
17801783
}

0 commit comments

Comments
 (0)