Skip to content

Commit ba80e42

Browse files
committed
Implement loading of any font in a collection
For backwards-compatibility, the path+index is passed around in a lightweight subclass of `str`.
1 parent 24c1576 commit ba80e42

File tree

3 files changed

+131
-18
lines changed

3 files changed

+131
-18
lines changed

lib/matplotlib/font_manager.py

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,61 @@ def findSystemFonts(fontpaths=None, fontext='ttf'):
310310
return [fname for fname in fontfiles if os.path.exists(fname)]
311311

312312

313+
class FontPath(str):
314+
"""
315+
A class to describe a path to a font with a face index.
316+
317+
Parameters
318+
----------
319+
path : str
320+
The path to a font.
321+
face_index : int
322+
The face index in the font.
323+
"""
324+
325+
__match_args__ = ('path', 'face_index')
326+
327+
def __new__(cls, path, face_index):
328+
ret = super().__new__(cls, path)
329+
ret._face_index = face_index
330+
return ret
331+
332+
@property
333+
def path(self):
334+
"""The path to a font."""
335+
return str(self)
336+
337+
@property
338+
def face_index(self):
339+
"""The face index in a font."""
340+
return self._face_index
341+
342+
def _as_tuple(self):
343+
return (self.path, self.face_index)
344+
345+
def __eq__(self, other):
346+
if isinstance(other, FontPath):
347+
return self._as_tuple() == other._as_tuple()
348+
return super().__eq__(other)
349+
350+
def __ne__(self, other):
351+
return not (self == other)
352+
353+
def __lt__(self, other):
354+
if isinstance(other, FontPath):
355+
return self._as_tuple() < other._as_tuple()
356+
return super().__lt__(other)
357+
358+
def __gt__(self, other):
359+
return not (self == other or self < other)
360+
361+
def __hash__(self):
362+
return hash(self._as_tuple())
363+
364+
def __repr__(self):
365+
return f'FontPath{self._as_tuple()}'
366+
367+
313368
@dataclasses.dataclass(frozen=True)
314369
class FontEntry:
315370
"""
@@ -1326,7 +1381,7 @@ def findfont(self, prop, fontext='ttf', directory=None,
13261381
13271382
Returns
13281383
-------
1329-
str
1384+
FontPath
13301385
The filename of the best matching font.
13311386
13321387
Notes
@@ -1396,7 +1451,7 @@ def _find_fonts_by_props(self, prop, fontext='ttf', directory=None,
13961451
13971452
Returns
13981453
-------
1399-
list[str]
1454+
list[FontPath]
14001455
The paths of the fonts found.
14011456
14021457
Notes
@@ -1542,7 +1597,7 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default,
15421597
# actually raised.
15431598
return cbook._ExceptionInfo(ValueError, "No valid font could be found")
15441599

1545-
return _cached_realpath(result)
1600+
return FontPath(_cached_realpath(result), best_font.index)
15461601

15471602

15481603
@_api.deprecated("3.11")
@@ -1562,15 +1617,16 @@ def is_opentype_cff_font(filename):
15621617
@lru_cache(64)
15631618
def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id,
15641619
enable_last_resort):
1565-
first_fontpath, *rest = font_filepaths
1620+
(first_fontpath, first_fontindex), *rest = font_filepaths
15661621
fallback_list = [
1567-
ft2font.FT2Font(fpath, hinting_factor, _kerning_factor=_kerning_factor)
1568-
for fpath in rest
1622+
ft2font.FT2Font(fpath, hinting_factor, face_index=index,
1623+
_kerning_factor=_kerning_factor)
1624+
for fpath, index in rest
15691625
]
15701626
last_resort_path = _cached_realpath(
15711627
cbook._get_data_path('fonts', 'ttf', 'LastResortHE-Regular.ttf'))
15721628
try:
1573-
last_resort_index = font_filepaths.index(last_resort_path)
1629+
last_resort_index = font_filepaths.index((last_resort_path, 0))
15741630
except ValueError:
15751631
last_resort_index = -1
15761632
# Add Last Resort font so we always have glyphs regardless of font, unless we're
@@ -1582,7 +1638,7 @@ def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id,
15821638
_warn_if_used=True))
15831639
last_resort_index = len(fallback_list)
15841640
font = ft2font.FT2Font(
1585-
first_fontpath, hinting_factor,
1641+
first_fontpath, hinting_factor, face_index=first_fontindex,
15861642
_fallback_list=fallback_list,
15871643
_kerning_factor=_kerning_factor
15881644
)
@@ -1617,7 +1673,8 @@ def get_font(font_filepaths, hinting_factor=None):
16171673
16181674
Parameters
16191675
----------
1620-
font_filepaths : Iterable[str, bytes, os.PathLike], str, bytes, os.PathLike
1676+
font_filepaths : Iterable[str, bytes, os.PathLike, FontPath], \
1677+
str, bytes, os.PathLike, FontPath
16211678
Relative or absolute paths to the font files to be used.
16221679
16231680
If a single string, bytes, or `os.PathLike`, then it will be treated
@@ -1632,10 +1689,16 @@ def get_font(font_filepaths, hinting_factor=None):
16321689
`.ft2font.FT2Font`
16331690
16341691
"""
1635-
if isinstance(font_filepaths, (str, bytes, os.PathLike)):
1636-
paths = (_cached_realpath(font_filepaths),)
1637-
else:
1638-
paths = tuple(_cached_realpath(fname) for fname in font_filepaths)
1692+
match font_filepaths:
1693+
case FontPath(path, index):
1694+
paths = ((_cached_realpath(path), index), )
1695+
case str() | bytes() | os.PathLike() as path:
1696+
paths = ((_cached_realpath(path), 0), )
1697+
case _:
1698+
paths = tuple(
1699+
(_cached_realpath(fname.path), fname.face_index)
1700+
if isinstance(fname, FontPath) else (_cached_realpath(fname), 0)
1701+
for fname in font_filepaths)
16391702

16401703
hinting_factor = mpl._val_or_rc(hinting_factor, 'text.hinting_factor')
16411704

lib/matplotlib/font_manager.pyi

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@ def _get_fontconfig_fonts() -> list[Path]: ...
2626
def findSystemFonts(
2727
fontpaths: Iterable[str | os.PathLike] | None = ..., fontext: str = ...
2828
) -> list[str]: ...
29+
30+
class FontPath(str):
31+
def __new__(cls: type[str], path: str, face_index: int) -> FontPath: ...
32+
@property
33+
def path(self) -> str: ...
34+
@property
35+
def face_index(self) -> int: ...
36+
def _as_tuple(self) -> tuple[str, int]: ...
37+
def __eq__(self, other: Any) -> bool: ...
38+
def __hash__(self) -> int: ...
39+
def __repr__(self) -> str: ...
40+
2941
@dataclass
3042
class FontEntry:
3143
fname: str = ...
@@ -116,12 +128,12 @@ class FontManager:
116128
directory: str | None = ...,
117129
fallback_to_default: bool = ...,
118130
rebuild_if_missing: bool = ...,
119-
) -> str: ...
131+
) -> FontPath: ...
120132
def get_font_names(self) -> list[str]: ...
121133

122134
def is_opentype_cff_font(filename: str) -> bool: ...
123135
def get_font(
124-
font_filepaths: Iterable[str | bytes | os.PathLike] | str | bytes | os.PathLike,
136+
font_filepaths: Iterable[str | bytes | os.PathLike | FontPath] | str | bytes | os.PathLike | FontPath,
125137
hinting_factor: int | None = ...,
126138
) -> ft2font.FT2Font: ...
127139

lib/matplotlib/tests/test_font_manager.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
import matplotlib as mpl
1515
from matplotlib.font_manager import (
16-
findfont, findSystemFonts, FontEntry, FontProperties, fontManager,
16+
findfont, findSystemFonts, FontEntry, FontPath, FontProperties, fontManager,
1717
json_dump, json_load, get_font, is_opentype_cff_font,
1818
MSUserFontDirectories, ttfFontProperty,
1919
_get_fontconfig_fonts, _normalize_weight)
@@ -24,6 +24,38 @@
2424
has_fclist = shutil.which('fc-list') is not None
2525

2626

27+
def test_font_path():
28+
fp = FontPath('foo', 123)
29+
fp2 = FontPath('foo', 321)
30+
assert str(fp) == 'foo'
31+
assert repr(fp) == "FontPath('foo', 123)"
32+
assert fp.path == 'foo'
33+
assert fp.face_index == 123
34+
# Should be immutable.
35+
with pytest.raises(AttributeError, match='has no setter'):
36+
fp.path = 'bar'
37+
with pytest.raises(AttributeError, match='has no setter'):
38+
fp.face_index = 321
39+
# Should be comparable with str and itself.
40+
assert fp == 'foo'
41+
assert fp == FontPath('foo', 123)
42+
assert fp <= fp
43+
assert fp >= fp
44+
assert fp != fp2
45+
assert fp < fp2
46+
assert fp <= fp2
47+
assert fp2 > fp
48+
assert fp2 >= fp
49+
# Should be hashable, but not the same as str.
50+
d = {fp: 1, 'bar': 2}
51+
assert fp in d
52+
assert d[fp] == 1
53+
assert d[FontPath('foo', 123)] == 1
54+
assert fp2 not in d
55+
assert 'foo' not in d
56+
assert FontPath('bar', 0) not in d
57+
58+
2759
def test_font_priority():
2860
with rc_context(rc={
2961
'font.sans-serif':
@@ -122,8 +154,12 @@ def test_find_ttc():
122154
pytest.skip("Font wqy-zenhei.ttc may be missing")
123155
# All fonts from this collection should have loaded as well.
124156
for name in ["WenQuanYi Zen Hei Mono", "WenQuanYi Zen Hei Sharp"]:
125-
assert findfont(FontProperties(family=[name]),
126-
fallback_to_default=False) == fontpath
157+
subfontpath = findfont(FontProperties(family=[name]), fallback_to_default=False)
158+
assert subfontpath.path == fontpath.path
159+
assert subfontpath.face_index != fontpath.face_index
160+
subfont = get_font(subfontpath)
161+
assert subfont.fname == subfontpath.path
162+
assert subfont.face_index == subfontpath.face_index
127163
fig, ax = plt.subplots()
128164
ax.text(.5, .5, "\N{KANGXI RADICAL DRAGON}", fontproperties=fp)
129165
for fmt in ["raw", "svg", "pdf", "ps"]:
@@ -161,6 +197,8 @@ def __fspath__(self):
161197
assert font.fname == file_str
162198
font = get_font(PathLikeClass(file_bytes))
163199
assert font.fname == file_bytes
200+
font = get_font(FontPath(file_str, 0))
201+
assert font.fname == file_str
164202

165203
# Note, fallbacks are not currently accessible.
166204
font = get_font([file_str, file_bytes,

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy