Skip to content

Commit 56308af

Browse files
authored
Merge pull request matplotlib#30143 from QuLogic/character-glyph-types
TYP: Make glyph indices distinct from character codes
2 parents 42c108a + 733cd7d commit 56308af

29 files changed

+5253
-4759
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__)
@@ -197,7 +198,7 @@ class CharMetrics(NamedTuple):
197198
The bbox of the character (B) as a tuple (*llx*, *lly*, *urx*, *ury*)."""
198199

199200

200-
def _parse_char_metrics(fh: BinaryIO) -> tuple[dict[int, CharMetrics],
201+
def _parse_char_metrics(fh: BinaryIO) -> tuple[dict[CharacterCodeType, CharMetrics],
201202
dict[str, CharMetrics]]:
202203
"""
203204
Parse the given filehandle for character metrics information.
@@ -218,7 +219,7 @@ def _parse_char_metrics(fh: BinaryIO) -> tuple[dict[int, CharMetrics],
218219
"""
219220
required_keys = {'C', 'WX', 'N', 'B'}
220221

221-
ascii_d: dict[int, CharMetrics] = {}
222+
ascii_d: dict[CharacterCodeType, CharMetrics] = {}
222223
name_d: dict[str, CharMetrics] = {}
223224
for bline in fh:
224225
# We are defensively letting values be utf8. The spec requires
@@ -409,19 +410,21 @@ def get_str_bbox_and_descent(self, s: str) -> tuple[int, int, float, int, int]:
409410

410411
return left, miny, total_width, maxy - miny, -miny
411412

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

416-
def get_char_index(self, c: int) -> int: # For consistency with FT2Font.
418+
def get_char_index(self, # For consistency with FT2Font.
419+
c: CharacterCodeType) -> GlyphIndexType:
417420
"""
418421
Return the glyph index corresponding to a character code point.
419422
420423
Note, for AFM fonts, we treat the glyph index the same as the codepoint.
421424
"""
422-
return c
425+
return cast(GlyphIndexType, c)
423426

424-
def get_width_char(self, c: int) -> float:
427+
def get_width_char(self, c: CharacterCodeType) -> float:
425428
"""Get the width of the character code from the character metric WX field."""
426429
return self._metrics[c].width
427430

lib/matplotlib/_mathtext.py

Lines changed: 17 additions & 15 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.
@@ -425,7 +426,9 @@ def get_kern(self, font1: str, fontclass1: str, sym1: str, fontsize1: float,
425426
info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi)
426427
info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi)
427428
font = info1.font
428-
return font.get_kerning(info1.num, info2.num, Kerning.DEFAULT) / 64
429+
return font.get_kerning(font.get_char_index(info1.num),
430+
font.get_char_index(info2.num),
431+
Kerning.DEFAULT) / 64
429432
return super().get_kern(font1, fontclass1, sym1, fontsize1,
430433
font2, fontclass2, sym2, fontsize2, dpi)
431434

@@ -459,7 +462,7 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag
459462
_slanted_symbols = set(r"\int \oint".split())
460463

461464
def _get_glyph(self, fontname: str, font_class: str,
462-
sym: str) -> tuple[FT2Font, int, bool]:
465+
sym: str) -> tuple[FT2Font, CharacterCodeType, bool]:
463466
font = None
464467
if fontname in self.fontmap and sym in latex_to_bakoma:
465468
basename, num = latex_to_bakoma[sym]
@@ -551,7 +554,7 @@ class UnicodeFonts(TruetypeFonts):
551554
# Some glyphs are not present in the `cmr10` font, and must be brought in
552555
# from `cmsy10`. Map the Unicode indices of those glyphs to the indices at
553556
# which they are found in `cmsy10`.
554-
_cmr10_substitutions = {
557+
_cmr10_substitutions: dict[CharacterCodeType, CharacterCodeType] = {
555558
0x00D7: 0x00A3, # Multiplication sign.
556559
0x2212: 0x00A1, # Minus sign.
557560
}
@@ -594,11 +597,11 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag
594597
_slanted_symbols = set(r"\int \oint".split())
595598

596599
def _map_virtual_font(self, fontname: str, font_class: str,
597-
uniindex: int) -> tuple[str, int]:
600+
uniindex: CharacterCodeType) -> tuple[str, CharacterCodeType]:
598601
return fontname, uniindex
599602

600603
def _get_glyph(self, fontname: str, font_class: str,
601-
sym: str) -> tuple[FT2Font, int, bool]:
604+
sym: str) -> tuple[FT2Font, CharacterCodeType, bool]:
602605
try:
603606
uniindex = get_unicode_index(sym)
604607
found_symbol = True
@@ -607,8 +610,7 @@ def _get_glyph(self, fontname: str, font_class: str,
607610
found_symbol = False
608611
_log.warning("No TeX to Unicode mapping for %a.", sym)
609612

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

613615
new_fontname = fontname
614616

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

695697
def _get_glyph(self, fontname: str, font_class: str,
696-
sym: str) -> tuple[FT2Font, int, bool]:
698+
sym: str) -> tuple[FT2Font, CharacterCodeType, bool]:
697699
# Override prime symbol to use Bakoma.
698700
if sym == r'\prime':
699701
return self.bakoma._get_glyph(fontname, font_class, sym)
@@ -783,7 +785,7 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag
783785
self.fontmap[name] = fullpath
784786

785787
def _map_virtual_font(self, fontname: str, font_class: str,
786-
uniindex: int) -> tuple[str, int]:
788+
uniindex: CharacterCodeType) -> tuple[str, CharacterCodeType]:
787789
# Handle these "fonts" that are actually embedded in
788790
# other fonts.
789791
font_mapping = stix_virtual_fonts.get(fontname)
@@ -1170,7 +1172,7 @@ def __init__(self, elements: T.Sequence[Node]):
11701172
self.glue_sign = 0 # 0: normal, -1: shrinking, 1: stretching
11711173
self.glue_order = 0 # The order of infinity (0 - 3) for the glue
11721174

1173-
def __repr__(self):
1175+
def __repr__(self) -> str:
11741176
return "{}<w={:.02f} h={:.02f} d={:.02f} s={:.02f}>[{}]".format(
11751177
super().__repr__(),
11761178
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 import Self
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: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
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, cast, final, overload
44
from typing_extensions import Buffer # < Py 3.12
55

66
import numpy as np
@@ -9,6 +9,12 @@ from numpy.typing import NDArray
99
__freetype_build_type__: str
1010
__freetype_version__: str
1111

12+
# We can't change the type hints for standard library chr/ord, so character codes are a
13+
# simple type alias.
14+
CharacterCodeType: TypeAlias = int
15+
# But glyph indices are internal, so use a distinct type hint.
16+
GlyphIndexType = NewType('GlyphIndexType', int)
17+
1218
class FaceFlags(Flag):
1319
SCALABLE = cast(int, ...)
1420
FIXED_SIZES = cast(int, ...)
@@ -202,13 +208,13 @@ class FT2Font(Buffer):
202208
) -> None: ...
203209
def draw_glyphs_to_bitmap(self, antialiased: bool = ...) -> None: ...
204210
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]: ...
211+
def get_char_index(self, codepoint: CharacterCodeType) -> GlyphIndexType: ...
212+
def get_charmap(self) -> dict[CharacterCodeType, GlyphIndexType]: ...
207213
def get_descent(self) -> int: ...
208-
def get_glyph_name(self, index: int) -> str: ...
214+
def get_glyph_name(self, index: GlyphIndexType) -> str: ...
209215
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: ...
216+
def get_kerning(self, left: GlyphIndexType, right: GlyphIndexType, mode: Kerning) -> int: ...
217+
def get_name_index(self, name: str) -> GlyphIndexType: ...
212218
def get_num_glyphs(self) -> int: ...
213219
def get_path(self) -> tuple[NDArray[np.float64], NDArray[np.int8]]: ...
214220
def get_ps_font_info(
@@ -230,8 +236,8 @@ class FT2Font(Buffer):
230236
@overload
231237
def get_sfnt_table(self, name: Literal["pclt"]) -> _SfntPcltDict | None: ...
232238
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: ...
239+
def load_char(self, charcode: CharacterCodeType, flags: LoadFlags = ...) -> Glyph: ...
240+
def load_glyph(self, glyphindex: GlyphIndexType, flags: LoadFlags = ...) -> Glyph: ...
235241
def select_charmap(self, i: int) -> None: ...
236242
def set_charmap(self, i: int) -> None: ...
237243
def set_size(self, ptsize: float, dpi: float) -> None: ...

0 commit comments

Comments
 (0)