Skip to content

Commit 4899954

Browse files
committed
Expose face index when loading fonts
This enables loading a non-initial font from collections (`.ttc` files). Currently exposed for `FT2Font` and `font_manager.get_font`.
1 parent 7a628e5 commit 4899954

File tree

7 files changed

+59
-20
lines changed

7 files changed

+59
-20
lines changed

lib/matplotlib/font_manager.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,10 +1556,11 @@ def is_opentype_cff_font(filename):
15561556
@lru_cache(64)
15571557
def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id,
15581558
enable_last_resort):
1559-
first_fontpath, *rest = font_filepaths
1559+
(first_fontpath, first_fontindex), *rest = font_filepaths
15601560
fallback_list = [
1561-
ft2font.FT2Font(fpath, hinting_factor, _kerning_factor=_kerning_factor)
1562-
for fpath in rest
1561+
ft2font.FT2Font(fpath, hinting_factor, face_index=index,
1562+
_kerning_factor=_kerning_factor)
1563+
for fpath, index in rest
15631564
]
15641565
last_resort_path = _cached_realpath(
15651566
cbook._get_data_path('fonts', 'ttf', 'LastResortHE-Regular.ttf'))
@@ -1576,7 +1577,7 @@ def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id,
15761577
_warn_if_used=True))
15771578
last_resort_index = len(fallback_list)
15781579
font = ft2font.FT2Font(
1579-
first_fontpath, hinting_factor,
1580+
first_fontpath, hinting_factor, face_index=first_fontindex,
15801581
_fallback_list=fallback_list,
15811582
_kerning_factor=_kerning_factor
15821583
)
@@ -1611,7 +1612,8 @@ def get_font(font_filepaths, hinting_factor=None):
16111612
16121613
Parameters
16131614
----------
1614-
font_filepaths : Iterable[str, Path, bytes], str, Path, bytes
1615+
font_filepaths : Iterable[str, Path, bytes, tuple[str | Path | bytes, int]], \
1616+
str, Path, bytes, tuple[str | Path | bytes, int]
16151617
Relative or absolute paths to the font files to be used.
16161618
16171619
If a single string, bytes, or `pathlib.Path`, then it will be treated
@@ -1626,10 +1628,16 @@ def get_font(font_filepaths, hinting_factor=None):
16261628
`.ft2font.FT2Font`
16271629
16281630
"""
1629-
if isinstance(font_filepaths, (str, Path, bytes)):
1630-
paths = (_cached_realpath(font_filepaths),)
1631-
else:
1632-
paths = tuple(_cached_realpath(fname) for fname in font_filepaths)
1631+
match font_filepaths:
1632+
case str() | Path() | bytes() as path:
1633+
paths = ((_cached_realpath(path), 0), )
1634+
case (str() | Path() | bytes() as path, int() as index):
1635+
paths = ((_cached_realpath(path), index), )
1636+
case _:
1637+
paths = tuple(
1638+
(_cached_realpath(fname[0]), fname[1]) if isinstance(fname, tuple)
1639+
else (_cached_realpath(fname), 0)
1640+
for fname in font_filepaths)
16331641

16341642
hinting_factor = mpl._val_or_rc(hinting_factor, 'text.hinting_factor')
16351643

lib/matplotlib/font_manager.pyi

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ from dataclasses import dataclass
33
from numbers import Integral
44
import os
55
from pathlib import Path
6-
from typing import Any, Literal
6+
from typing import Any, Literal, TypeAlias
77

88
from matplotlib._afm import AFM
99
from matplotlib import ft2font
1010

11+
FontFace: TypeAlias = tuple[str | Path | bytes, int]
12+
1113
font_scalings: dict[str | None, float]
1214
stretch_dict: dict[str, int]
1315
weight_dict: dict[str, int]
@@ -120,7 +122,7 @@ class FontManager:
120122

121123
def is_opentype_cff_font(filename: str) -> bool: ...
122124
def get_font(
123-
font_filepaths: Iterable[str | Path | bytes] | str | Path | bytes,
125+
font_filepaths: Iterable[str | Path | bytes | FontFace] | str | Path | bytes | FontFace,
124126
hinting_factor: int | None = ...,
125127
) -> ft2font.FT2Font: ...
126128

lib/matplotlib/ft2font.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ class FT2Font(Buffer):
190190
filename: str | BinaryIO,
191191
hinting_factor: int = ...,
192192
*,
193+
face_index: int = ...,
193194
_fallback_list: list[FT2Font] | None = ...,
194195
_kerning_factor: int = ...
195196
) -> None: ...

lib/matplotlib/tests/test_ft2font.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,24 @@ def test_ft2font_invalid_args(tmp_path):
170170
ft2font.FT2Font(file, _kerning_factor=1.3)
171171

172172

173+
@pytest.mark.parametrize('name, size, skippable',
174+
[('DejaVu Sans', 1, False), ('WenQuanYi Zen Hei', 3, True)])
175+
def test_ft2font_face_index(name, size, skippable):
176+
try:
177+
file = fm.findfont(name, fallback_to_default=False)
178+
except ValueError:
179+
if skippable:
180+
pytest.skip(r'Font {name} may be missing')
181+
raise
182+
for index in range(size):
183+
font = ft2font.FT2Font(file, face_index=index)
184+
assert font.num_faces >= size
185+
with pytest.raises(ValueError, match='must be between'): # out of bounds for spec
186+
ft2font.FT2Font(file, face_index=0x1ffff)
187+
with pytest.raises(RuntimeError, match='invalid argument'): # invalid for this font
188+
ft2font.FT2Font(file, face_index=0xff)
189+
190+
173191
def test_ft2font_clear():
174192
file = fm.findfont('DejaVu Sans')
175193
font = ft2font.FT2Font(file)

src/ft2font.cpp

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,7 @@ FT2Font::get_path(std::vector<double> &vertices, std::vector<unsigned char> &cod
207207
codes.push_back(CLOSEPOLY);
208208
}
209209

210-
FT2Font::FT2Font(FT_Open_Args &open_args,
211-
long hinting_factor_,
210+
FT2Font::FT2Font(FT_Long face_index, FT_Open_Args &open_args, long hinting_factor_,
212211
std::vector<FT2Font *> &fallback_list,
213212
FT2Font::WarnFunc warn, bool warn_if_used)
214213
: ft_glyph_warn(warn), warn_if_used(warn_if_used), image({1, 1}), face(nullptr),
@@ -217,7 +216,7 @@ FT2Font::FT2Font(FT_Open_Args &open_args,
217216
kerning_factor(0)
218217
{
219218
clear();
220-
FT_CHECK(FT_Open_Face, _ft2Library, &open_args, 0, &face);
219+
FT_CHECK(FT_Open_Face, _ft2Library, &open_args, face_index, &face);
221220
if (open_args.stream != nullptr) {
222221
face->face_flags |= FT_FACE_FLAG_EXTERNAL_STREAM;
223222
}

src/ft2font.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ class FT2Font
9999
typedef void (*WarnFunc)(FT_ULong charcode, std::set<FT_String*> family_names);
100100

101101
public:
102-
FT2Font(FT_Open_Args &open_args, long hinting_factor,
102+
FT2Font(FT_Long face_index, FT_Open_Args &open_args, long hinting_factor,
103103
std::vector<FT2Font *> &fallback_list,
104104
WarnFunc warn, bool warn_if_used);
105105
virtual ~FT2Font();

src/ft2font_wrapper.cpp

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,9 @@ const char *PyFT2Font_init__doc__ = R"""(
426426
hinting_factor : int, optional
427427
Must be positive. Used to scale the hinting in the x-direction.
428428
429+
face_index : int, optional
430+
The index of the face in the font file to load.
431+
429432
_fallback_list : list of FT2Font, optional
430433
A list of FT2Font objects used to find missing glyphs.
431434
@@ -446,14 +449,18 @@ const char *PyFT2Font_init__doc__ = R"""(
446449
)""";
447450

448451
static PyFT2Font *
449-
PyFT2Font_init(py::object filename, long hinting_factor = 8,
452+
PyFT2Font_init(py::object filename, long hinting_factor = 8, FT_Long face_index = 0,
450453
std::optional<std::vector<PyFT2Font *>> fallback_list = std::nullopt,
451454
int kerning_factor = 0, bool warn_if_used = false)
452455
{
453456
if (hinting_factor <= 0) {
454457
throw py::value_error("hinting_factor must be greater than 0");
455458
}
456459

460+
if (face_index < 0 || face_index >= 1<<16) {
461+
throw std::range_error("face_index must be between 0 and 65535, inclusive");
462+
}
463+
457464
PyFT2Font *self = new PyFT2Font();
458465
self->x = nullptr;
459466
memset(&self->stream, 0, sizeof(FT_StreamRec));
@@ -497,8 +504,8 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8,
497504
self->stream.close = nullptr;
498505
}
499506

500-
self->x = new FT2Font(open_args, hinting_factor, fallback_fonts, ft_glyph_warn,
501-
warn_if_used);
507+
self->x = new FT2Font(face_index, open_args, hinting_factor, fallback_fonts,
508+
ft_glyph_warn, warn_if_used);
502509

503510
self->x->set_kerning_factor(kerning_factor);
504511

@@ -1604,7 +1611,7 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
16041611
auto cls = py::class_<PyFT2Font>(m, "FT2Font", py::is_final(), py::buffer_protocol(),
16051612
PyFT2Font__doc__)
16061613
.def(py::init(&PyFT2Font_init),
1607-
"filename"_a, "hinting_factor"_a=8, py::kw_only(),
1614+
"filename"_a, "hinting_factor"_a=8, py::kw_only(), "face_index"_a=0,
16081615
"_fallback_list"_a=py::none(), "_kerning_factor"_a=0,
16091616
"_warn_if_used"_a=false,
16101617
PyFT2Font_init__doc__)
@@ -1677,8 +1684,12 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
16771684
}, "PostScript name of the font.")
16781685
.def_property_readonly(
16791686
"num_faces", [](PyFT2Font *self) {
1680-
return self->x->get_face()->num_faces;
1687+
return self->x->get_face()->num_faces & 0xffff;
16811688
}, "Number of faces in file.")
1689+
.def_property_readonly(
1690+
"face_index", [](PyFT2Font *self) {
1691+
return self->x->get_face()->face_index;
1692+
}, "The index of the font in the file.")
16821693
.def_property_readonly(
16831694
"family_name", [](PyFT2Font *self) {
16841695
if (const char *name = self->x->get_face()->family_name) {

0 commit comments

Comments
 (0)