From daae68a3686dcdc761291ff3b6192bf08736d033 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 18 Jul 2025 03:50:41 -0400 Subject: [PATCH 1/7] Expose face index when loading fonts This enables loading a non-initial font from collections (`.ttc` files). Currently exposed for `FT2Font`, only. --- lib/matplotlib/ft2font.pyi | 3 +++ lib/matplotlib/tests/test_ft2font.py | 19 +++++++++++++++++++ src/ft2font.cpp | 4 ++-- src/ft2font.h | 2 +- src/ft2font_wrapper.cpp | 19 +++++++++++++++---- 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index 9345c1c9057f..71bd89f81561 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -214,6 +214,7 @@ class FT2Font(Buffer): filename: str | bytes | PathLike | BinaryIO, hinting_factor: int = ..., *, + face_index: int = ..., _fallback_list: list[FT2Font] | None = ..., _kerning_factor: int | None = ... ) -> None: ... @@ -283,6 +284,8 @@ class FT2Font(Buffer): @property def face_flags(self) -> FaceFlags: ... @property + def face_index(self) -> int: ... + @property def family_name(self) -> str: ... @property def fname(self) -> str | bytes: ... diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 4a874deb5343..17492e7690c0 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -199,6 +199,25 @@ def test_ft2font_invalid_args(tmp_path): ft2font.FT2Font(file, _kerning_factor=123) +@pytest.mark.parametrize('name, size, skippable', + [('DejaVu Sans', 1, False), ('WenQuanYi Zen Hei', 3, True)]) +def test_ft2font_face_index(name, size, skippable): + try: + file = fm.findfont(name, fallback_to_default=False) + except ValueError: + if skippable: + pytest.skip(r'Font {name} may be missing') + raise + for index in range(size): + font = ft2font.FT2Font(file, face_index=index) + assert font.num_faces >= size + assert font.face_index == index + with pytest.raises(ValueError, match='must be between'): # out of bounds for spec + ft2font.FT2Font(file, face_index=0x1ffff) + with pytest.raises(RuntimeError, match='invalid argument'): # invalid for this font + ft2font.FT2Font(file, face_index=0xff) + + def test_ft2font_clear(): file = fm.findfont('DejaVu Sans') font = ft2font.FT2Font(file) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index 0e98506536d0..b70f3a29d469 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -222,9 +222,9 @@ FT2Font::~FT2Font() close(); } -void FT2Font::open(FT_Open_Args &open_args) +void FT2Font::open(FT_Open_Args &open_args, FT_Long face_index) { - FT_CHECK(FT_Open_Face, _ft2Library, &open_args, 0, &face); + FT_CHECK(FT_Open_Face, _ft2Library, &open_args, face_index, &face); if (open_args.stream != nullptr) { face->face_flags |= FT_FACE_FLAG_EXTERNAL_STREAM; } diff --git a/src/ft2font.h b/src/ft2font.h index b36bc4f02a76..68d31bac9a41 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -107,7 +107,7 @@ class FT2Font FT2Font(long hinting_factor, std::vector &fallback_list, bool warn_if_used); virtual ~FT2Font(); - void open(FT_Open_Args &open_args); + void open(FT_Open_Args &open_args, FT_Long face_index); void close(); void clear(); void set_size(double ptsize, double dpi); diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 21d8b01656b8..d5cf07e7762d 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -430,6 +430,9 @@ const char *PyFT2Font_init__doc__ = R"""( hinting_factor : int, optional Must be positive. Used to scale the hinting in the x-direction. + face_index : int, optional + The index of the face in the font file to load. + _fallback_list : list of FT2Font, optional A list of FT2Font objects used to find missing glyphs. @@ -444,7 +447,7 @@ const char *PyFT2Font_init__doc__ = R"""( )"""; static PyFT2Font * -PyFT2Font_init(py::object filename, long hinting_factor = 8, +PyFT2Font_init(py::object filename, long hinting_factor = 8, FT_Long face_index = 0, std::optional> fallback_list = std::nullopt, std::optional kerning_factor = std::nullopt, bool warn_if_used = false) @@ -460,6 +463,10 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8, kerning_factor = 0; } + if (face_index < 0 || face_index > 0xffff) { + throw std::range_error("face_index must be between 0 and 65535, inclusive"); + } + std::vector fallback_fonts; if (fallback_list) { // go through fallbacks to add them to our lists @@ -509,7 +516,7 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8, self->stream.close = nullptr; } - self->open(open_args); + self->open(open_args, face_index); return self; } @@ -1637,7 +1644,7 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) auto cls = py::class_(m, "FT2Font", py::is_final(), py::buffer_protocol(), PyFT2Font__doc__) .def(py::init(&PyFT2Font_init), - "filename"_a, "hinting_factor"_a=8, py::kw_only(), + "filename"_a, "hinting_factor"_a=8, py::kw_only(), "face_index"_a=0, "_fallback_list"_a=py::none(), "_kerning_factor"_a=py::none(), "_warn_if_used"_a=false, PyFT2Font_init__doc__) @@ -1714,8 +1721,12 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) }, "PostScript name of the font.") .def_property_readonly( "num_faces", [](PyFT2Font *self) { - return self->get_face()->num_faces; + return self->get_face()->num_faces & 0xffff; }, "Number of faces in file.") + .def_property_readonly( + "face_index", [](PyFT2Font *self) { + return self->get_face()->face_index; + }, "The index of the font in the file.") .def_property_readonly( "family_name", [](PyFT2Font *self) { if (const char *name = self->get_face()->family_name) { From b839f87b93f0a6447569768ff745de2ddefec5aa Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 18 Jul 2025 04:51:05 -0400 Subject: [PATCH 2/7] Parse data from all fonts within a collection This should allow listing the metadata from the whole collection, which will also pick the right one if specified, though it will not load the specific index yet. --- lib/matplotlib/font_manager.py | 12 +++++++++--- lib/matplotlib/font_manager.pyi | 1 + lib/matplotlib/tests/test_font_manager.py | 11 ++++++++++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index d1a96826fbf6..a8427cdbaa30 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -319,6 +319,7 @@ class FontEntry: """ fname: str = '' + index: int = 0 name: str = '' style: str = 'normal' variant: str = 'normal' @@ -465,7 +466,8 @@ def get_weight(): # From fontconfig's FcFreeTypeQueryFaceInternal. raise NotImplementedError("Non-scalable fonts are not supported") size = 'scalable' - return FontEntry(font.fname, name, style, variant, weight, stretch, size) + return FontEntry(font.fname, font.face_index, name, + style, variant, weight, stretch, size) def afmFontProperty(fontpath, font): @@ -535,7 +537,7 @@ def afmFontProperty(fontpath, font): size = 'scalable' - return FontEntry(fontpath, name, style, variant, weight, stretch, size) + return FontEntry(fontpath, 0, name, style, variant, weight, stretch, size) def _cleanup_fontproperties_init(init_method): @@ -1069,7 +1071,7 @@ class FontManager: # Increment this version number whenever the font cache data # format or behavior has changed and requires an existing font # cache files to be rebuilt. - __version__ = '3.11.0a1' + __version__ = '3.11.0a2' def __init__(self, size=None, weight='normal'): self._version = self.__version__ @@ -1134,6 +1136,10 @@ def addfont(self, path): font = ft2font.FT2Font(path) prop = ttfFontProperty(font) self.ttflist.append(prop) + for face_index in range(1, font.num_faces): + subfont = ft2font.FT2Font(path, face_index=face_index) + prop = ttfFontProperty(subfont) + self.ttflist.append(prop) self._findfont_cached.cache_clear() @property diff --git a/lib/matplotlib/font_manager.pyi b/lib/matplotlib/font_manager.pyi index f5e3910e5f63..6b072f707f66 100644 --- a/lib/matplotlib/font_manager.pyi +++ b/lib/matplotlib/font_manager.pyi @@ -29,6 +29,7 @@ def findSystemFonts( @dataclass class FontEntry: fname: str = ... + index: int = ... name: str = ... style: str = ... variant: str = ... diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index d51eb8d9837f..6301163b4527 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -117,8 +117,13 @@ def test_utf16m_sfnt(): def test_find_ttc(): fp = FontProperties(family=["WenQuanYi Zen Hei"]) - if Path(findfont(fp)).name != "wqy-zenhei.ttc": + fontpath = findfont(fp) + if Path(fontpath).name != "wqy-zenhei.ttc": pytest.skip("Font wqy-zenhei.ttc may be missing") + # All fonts from this collection should have loaded as well. + for name in ["WenQuanYi Zen Hei Mono", "WenQuanYi Zen Hei Sharp"]: + assert findfont(FontProperties(family=[name]), + fallback_to_default=False) == fontpath fig, ax = plt.subplots() ax.text(.5, .5, "\N{KANGXI RADICAL DRAGON}", fontproperties=fp) for fmt in ["raw", "svg", "pdf", "ps"]: @@ -363,6 +368,10 @@ def test_get_font_names(): font = ft2font.FT2Font(path) prop = ttfFontProperty(font) ttf_fonts.append(prop.name) + for face_index in range(1, font.num_faces): + font = ft2font.FT2Font(path, face_index=face_index) + prop = ttfFontProperty(font) + ttf_fonts.append(prop.name) except Exception: pass available_fonts = sorted(list(set(ttf_fonts))) From 6d2ae678f44e0250e5ecbedccad5dc634d4cbd62 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 25 Jul 2025 03:33:54 -0400 Subject: [PATCH 3/7] Implement loading of any font in a collection For backwards-compatibility, the path+index is passed around in a lightweight subclass of `str`. --- lib/matplotlib/font_manager.py | 97 ++++++++++++++++++++--- lib/matplotlib/font_manager.pyi | 22 ++++- lib/matplotlib/tests/test_font_manager.py | 44 +++++++++- 3 files changed, 144 insertions(+), 19 deletions(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index a8427cdbaa30..5ca1c9aeafb7 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -310,6 +310,69 @@ def findSystemFonts(fontpaths=None, fontext='ttf'): return [fname for fname in fontfiles if os.path.exists(fname)] +# To maintain backwards-compatibility with the current code we need to continue to +# return a str. However to support indexing into the file we need to return both the +# path and the index. Thus, we sub-class str to maintain compatibility and extend it to +# carry the index. +# +# The other alternative would be to create a completely new API and deprecate the +# existing one. In this case, sub-classing str is the simpler and less-disruptive +# option. +class FontPath(str): + """ + A class to describe a path to a font with a face index. + + Parameters + ---------- + path : str + The path to a font. + face_index : int + The face index in the font. + """ + + __match_args__ = ('path', 'face_index') + + def __new__(cls, path, face_index): + ret = super().__new__(cls, path) + ret._face_index = face_index + return ret + + @property + def path(self): + """The path to a font.""" + return str(self) + + @property + def face_index(self): + """The face index in a font.""" + return self._face_index + + def _as_tuple(self): + return (self.path, self.face_index) + + def __eq__(self, other): + if isinstance(other, FontPath): + return self._as_tuple() == other._as_tuple() + return super().__eq__(other) + + def __ne__(self, other): + return not (self == other) + + def __lt__(self, other): + if isinstance(other, FontPath): + return self._as_tuple() < other._as_tuple() + return super().__lt__(other) + + def __gt__(self, other): + return not (self == other or self < other) + + def __hash__(self): + return hash(self._as_tuple()) + + def __repr__(self): + return f'FontPath{self._as_tuple()}' + + @dataclasses.dataclass(frozen=True) class FontEntry: """ @@ -1326,7 +1389,7 @@ def findfont(self, prop, fontext='ttf', directory=None, Returns ------- - str + FontPath The filename of the best matching font. Notes @@ -1396,7 +1459,7 @@ def _find_fonts_by_props(self, prop, fontext='ttf', directory=None, Returns ------- - list[str] + list[FontPath] The paths of the fonts found. Notes @@ -1542,7 +1605,7 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default, # actually raised. return cbook._ExceptionInfo(ValueError, "No valid font could be found") - return _cached_realpath(result) + return FontPath(_cached_realpath(result), best_font.index) @_api.deprecated("3.11") @@ -1562,15 +1625,16 @@ def is_opentype_cff_font(filename): @lru_cache(64) def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id, enable_last_resort): - first_fontpath, *rest = font_filepaths + (first_fontpath, first_fontindex), *rest = font_filepaths fallback_list = [ - ft2font.FT2Font(fpath, hinting_factor, _kerning_factor=_kerning_factor) - for fpath in rest + ft2font.FT2Font(fpath, hinting_factor, face_index=index, + _kerning_factor=_kerning_factor) + for fpath, index in rest ] last_resort_path = _cached_realpath( cbook._get_data_path('fonts', 'ttf', 'LastResortHE-Regular.ttf')) try: - last_resort_index = font_filepaths.index(last_resort_path) + last_resort_index = font_filepaths.index((last_resort_path, 0)) except ValueError: last_resort_index = -1 # Add Last Resort font so we always have glyphs regardless of font, unless we're @@ -1582,7 +1646,7 @@ def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id, _warn_if_used=True)) last_resort_index = len(fallback_list) font = ft2font.FT2Font( - first_fontpath, hinting_factor, + first_fontpath, hinting_factor, face_index=first_fontindex, _fallback_list=fallback_list, _kerning_factor=_kerning_factor ) @@ -1617,7 +1681,8 @@ def get_font(font_filepaths, hinting_factor=None): Parameters ---------- - font_filepaths : Iterable[str, bytes, os.PathLike], str, bytes, os.PathLike + font_filepaths : Iterable[str, bytes, os.PathLike, FontPath], \ +str, bytes, os.PathLike, FontPath Relative or absolute paths to the font files to be used. If a single string, bytes, or `os.PathLike`, then it will be treated @@ -1632,10 +1697,16 @@ def get_font(font_filepaths, hinting_factor=None): `.ft2font.FT2Font` """ - if isinstance(font_filepaths, (str, bytes, os.PathLike)): - paths = (_cached_realpath(font_filepaths),) - else: - paths = tuple(_cached_realpath(fname) for fname in font_filepaths) + match font_filepaths: + case FontPath(path, index): + paths = ((_cached_realpath(path), index), ) + case str() | bytes() | os.PathLike() as path: + paths = ((_cached_realpath(path), 0), ) + case _: + paths = tuple( + (_cached_realpath(fname.path), fname.face_index) + if isinstance(fname, FontPath) else (_cached_realpath(fname), 0) + for fname in font_filepaths) hinting_factor = mpl._val_or_rc(hinting_factor, 'text.hinting_factor') diff --git a/lib/matplotlib/font_manager.pyi b/lib/matplotlib/font_manager.pyi index 6b072f707f66..936dad426522 100644 --- a/lib/matplotlib/font_manager.pyi +++ b/lib/matplotlib/font_manager.pyi @@ -3,7 +3,7 @@ from dataclasses import dataclass from numbers import Integral import os from pathlib import Path -from typing import Any, Literal +from typing import Any, Final, Literal from matplotlib._afm import AFM from matplotlib import ft2font @@ -26,6 +26,22 @@ def _get_fontconfig_fonts() -> list[Path]: ... def findSystemFonts( fontpaths: Iterable[str | os.PathLike] | None = ..., fontext: str = ... ) -> list[str]: ... + +class FontPath(str): + __match_args__: Final[tuple[str, ...]] + def __new__(cls: type[str], path: str, face_index: int) -> FontPath: ... + @property + def path(self) -> str: ... + @property + def face_index(self) -> int: ... + def _as_tuple(self) -> tuple[str, int]: ... + def __eq__(self, other: Any) -> bool: ... + def __ne__(self, other: Any) -> bool: ... + def __lt__(self, other: Any) -> bool: ... + def __gt__(self, other: Any) -> bool: ... + def __hash__(self) -> int: ... + def __repr__(self) -> str: ... + @dataclass class FontEntry: fname: str = ... @@ -116,12 +132,12 @@ class FontManager: directory: str | None = ..., fallback_to_default: bool = ..., rebuild_if_missing: bool = ..., - ) -> str: ... + ) -> FontPath: ... def get_font_names(self) -> list[str]: ... def is_opentype_cff_font(filename: str) -> bool: ... def get_font( - font_filepaths: Iterable[str | bytes | os.PathLike] | str | bytes | os.PathLike, + font_filepaths: Iterable[str | bytes | os.PathLike | FontPath] | str | bytes | os.PathLike | FontPath, hinting_factor: int | None = ..., ) -> ft2font.FT2Font: ... diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index 6301163b4527..cc8ae03a9f97 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -13,7 +13,7 @@ import matplotlib as mpl from matplotlib.font_manager import ( - findfont, findSystemFonts, FontEntry, FontProperties, fontManager, + findfont, findSystemFonts, FontEntry, FontPath, FontProperties, fontManager, json_dump, json_load, get_font, is_opentype_cff_font, MSUserFontDirectories, ttfFontProperty, _get_fontconfig_fonts, _normalize_weight) @@ -24,6 +24,38 @@ has_fclist = shutil.which('fc-list') is not None +def test_font_path(): + fp = FontPath('foo', 123) + fp2 = FontPath('foo', 321) + assert str(fp) == 'foo' + assert repr(fp) == "FontPath('foo', 123)" + assert fp.path == 'foo' + assert fp.face_index == 123 + # Should be immutable. + with pytest.raises(AttributeError, match='has no setter'): + fp.path = 'bar' + with pytest.raises(AttributeError, match='has no setter'): + fp.face_index = 321 + # Should be comparable with str and itself. + assert fp == 'foo' + assert fp == FontPath('foo', 123) + assert fp <= fp + assert fp >= fp + assert fp != fp2 + assert fp < fp2 + assert fp <= fp2 + assert fp2 > fp + assert fp2 >= fp + # Should be hashable, but not the same as str. + d = {fp: 1, 'bar': 2} + assert fp in d + assert d[fp] == 1 + assert d[FontPath('foo', 123)] == 1 + assert fp2 not in d + assert 'foo' not in d + assert FontPath('bar', 0) not in d + + def test_font_priority(): with rc_context(rc={ 'font.sans-serif': @@ -122,8 +154,12 @@ def test_find_ttc(): pytest.skip("Font wqy-zenhei.ttc may be missing") # All fonts from this collection should have loaded as well. for name in ["WenQuanYi Zen Hei Mono", "WenQuanYi Zen Hei Sharp"]: - assert findfont(FontProperties(family=[name]), - fallback_to_default=False) == fontpath + subfontpath = findfont(FontProperties(family=[name]), fallback_to_default=False) + assert subfontpath.path == fontpath.path + assert subfontpath.face_index != fontpath.face_index + subfont = get_font(subfontpath) + assert subfont.fname == subfontpath.path + assert subfont.face_index == subfontpath.face_index fig, ax = plt.subplots() ax.text(.5, .5, "\N{KANGXI RADICAL DRAGON}", fontproperties=fp) for fmt in ["raw", "svg", "pdf", "ps"]: @@ -161,6 +197,8 @@ def __fspath__(self): assert font.fname == file_str font = get_font(PathLikeClass(file_bytes)) assert font.fname == file_bytes + font = get_font(FontPath(file_str, 0)) + assert font.fname == file_str # Note, fallbacks are not currently accessible. font = get_font([file_str, file_bytes, From ea80bbb58f6c349da05b3eb94c766059c0ac9bc8 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 25 Jul 2025 06:13:59 -0400 Subject: [PATCH 4/7] pdf/ps: Support any font in a collection --- lib/matplotlib/backends/_backend_pdf_ps.py | 10 +++--- lib/matplotlib/backends/backend_pdf.py | 29 +++++++++-------- lib/matplotlib/backends/backend_ps.py | 12 +++----- lib/matplotlib/dviread.py | 4 +++ lib/matplotlib/dviread.pyi | 2 ++ lib/matplotlib/tests/test_backend_pdf.py | 35 ++++++++++++++++++++- lib/matplotlib/tests/test_backend_ps.py | 36 +++++++++++++++++++++- 7 files changed, 101 insertions(+), 27 deletions(-) diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index b1a3f5c9f18b..83a8566517a7 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -42,7 +42,7 @@ def get_glyphs_subset(fontfile: str, glyphs: set[GlyphIndexType]) -> TTFont: Parameters ---------- - fontfile : str + fontfile : FontPath Path to the font file glyphs : set[GlyphIndexType] Set of glyph indices to include in subset. @@ -80,8 +80,7 @@ def get_glyphs_subset(fontfile: str, glyphs: set[GlyphIndexType]) -> TTFont: 'xref', # The cross-reference table (some Apple font tooling information). ] # if fontfile is a ttc, specify font number - if fontfile.endswith(".ttc"): - options.font_number = 0 + options.font_number = fontfile.face_index font = subset.load_font(fontfile, options) subsetter = subset.Subsetter(options=options) @@ -267,11 +266,12 @@ def track_glyph(self, font: FT2Font, chars: str | CharacterCodeType, charcode = chars chars = chr(chars) - glyph_map = self.glyph_maps.setdefault(font.fname, GlyphMap()) + font_path = font_manager.FontPath(font.fname, font.face_index) + glyph_map = self.glyph_maps.setdefault(font_path, GlyphMap()) if result := glyph_map.get(chars, glyph): return result - subset_maps = self.used.setdefault(font.fname, [{}]) + subset_maps = self.used.setdefault(font_path, [{}]) use_next_charmap = ( # Multi-character glyphs always go in the non-0 subset. len(chars) > 1 or diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 613b00987730..a926cb41bb3b 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -32,7 +32,7 @@ RendererBase) from matplotlib.backends.backend_mixed import MixedModeRenderer from matplotlib.figure import Figure -from matplotlib.font_manager import get_font, fontManager as _fontManager +from matplotlib.font_manager import FontPath, get_font, fontManager as _fontManager from matplotlib._afm import AFM from matplotlib.ft2font import FT2Font, FaceFlags, LoadFlags, StyleFlags from matplotlib.transforms import Affine2D, BboxBase @@ -894,8 +894,10 @@ def fontName(self, fontprop, subset=0): as the filename of the font. """ - if isinstance(fontprop, str): + if isinstance(fontprop, FontPath): filenames = [fontprop] + elif isinstance(fontprop, str): + filenames = [FontPath(fontprop, 0)] elif mpl.rcParams['pdf.use14corefonts']: filenames = _fontManager._find_fonts_by_props( fontprop, fontext='afm', directory=RendererPdf._afm_font_dir @@ -935,7 +937,7 @@ def writeFonts(self): _log.debug('Embedding Type-1 font %s from dvi.', dvifont.texname) fonts[pdfname] = self._embedTeXFont(dvifont) for (filename, subset), Fx in sorted(self._fontNames.items()): - _log.debug('Embedding font %s:%d.', filename, subset) + _log.debug('Embedding font %r:%d.', filename, subset) if filename.endswith('.afm'): # from pdf.use14corefonts _log.debug('Writing AFM font.') @@ -986,10 +988,11 @@ def _embedTeXFont(self, dvifont): # Reduce the font to only the glyphs used in the document, get the encoding # for that subset, and compute various properties based on the encoding. - charmap = self._character_tracker.used[dvifont.fname][0] + font_path = FontPath(dvifont.fname, dvifont.face_index) + charmap = self._character_tracker.used[font_path][0] chars = { # DVI type 1 fonts always map single glyph to single character. - ord(self._character_tracker.subset_to_unicode(dvifont.fname, 0, ccode)) + ord(self._character_tracker.subset_to_unicode(font_path, 0, ccode)) for ccode in charmap } t1font = t1font.subset(chars, self._get_subset_prefix(charmap.values())) @@ -1241,12 +1244,12 @@ def embedTTFType42(font, subset_index, charmap, descriptor): wObject = self.reserveObject('Type 0 widths') toUnicodeMapObject = self.reserveObject('ToUnicode map') - _log.debug("SUBSET %s:%d characters: %s", filename, subset_index, charmap) + _log.debug("SUBSET %r:%d characters: %s", filename, subset_index, charmap) with _backend_pdf_ps.get_glyphs_subset(filename, charmap.values()) as subset: fontdata = _backend_pdf_ps.font_as_file(subset) _log.debug( - "SUBSET %s:%d %d -> %d", filename, subset_index, + "SUBSET %r:%d %d -> %d", filename, subset_index, os.stat(filename).st_size, fontdata.getbuffer().nbytes ) @@ -2137,13 +2140,13 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): for font, fontsize, ccode, glyph_index, ox, oy in glyphs: subset_index, subset_charcode = self.file._character_tracker.track_glyph( font, ccode, glyph_index) - fontname = font.fname + font_path = FontPath(font.fname, font.face_index) self._setup_textpos(ox, oy, 0, oldx, oldy) oldx, oldy = ox, oy - if (fontname, subset_index, fontsize) != prev_font: - self.file.output(self.file.fontName(fontname, subset_index), fontsize, + if (font_path, subset_index, fontsize) != prev_font: + self.file.output(self.file.fontName(font_path, subset_index), fontsize, Op.selectfont) - prev_font = fontname, subset_index, fontsize + prev_font = font_path, subset_index, fontsize self.file.output(self._encode_glyphs([subset_charcode], fonttype), Op.show) self.file.output(Op.end_text) @@ -2338,7 +2341,9 @@ def output_singlebyte_chunk(kerns_or_chars): item.ft_object, item.char, item.glyph_index) if (item.ft_object, subset) != prev_font: output_singlebyte_chunk(singlebyte_chunk) - ft_name = self.file.fontName(item.ft_object.fname, subset) + font_path = FontPath(item.ft_object.fname, + item.ft_object.face_index) + ft_name = self.file.fontName(font_path, subset) self.file.output(ft_name, fontsize, Op.selectfont) self._setup_textpos(item.x, 0, 0, prev_start_x, 0, 0) prev_font = (item.ft_object, subset) diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 8ad31290a643..3bdf7a0c514b 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -94,7 +94,7 @@ def _font_to_ps_type3(font_path, subset_index, glyph_indices): Parameters ---------- - font_path : path-like + font_path : FontPath Path to the font to be subsetted. subset_index : int The subset of the above font being created. @@ -176,7 +176,7 @@ def _font_to_ps_type42(font_path, subset_index, glyph_indices, fh): Parameters ---------- - font_path : path-like + font_path : FontPath Path to the font to be subsetted. subset_index : int The subset of the above font being created. @@ -187,12 +187,8 @@ def _font_to_ps_type42(font_path, subset_index, glyph_indices, fh): """ _log.debug("SUBSET %s:%d characters: %s", font_path, subset_index, glyph_indices) try: - kw = {} - # fix this once we support loading more fonts from a collection - # https://github.com/matplotlib/matplotlib/issues/3135#issuecomment-571085541 - if font_path.endswith('.ttc'): - kw['fontNumber'] = 0 - with (fontTools.ttLib.TTFont(font_path, **kw) as font, + with (fontTools.ttLib.TTFont(font_path.path, + fontNumber=font_path.face_index) as font, _backend_pdf_ps.get_glyphs_subset(font_path, glyph_indices) as subset): fontdata = _backend_pdf_ps.font_as_file(subset).getvalue() _log.debug( diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index f07157a63524..1a79e7277be8 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -719,6 +719,10 @@ def fname(self): """A fake filename""" return self.texname.decode('latin-1') + @property + def face_index(self): # For compatibility with FT2Font. + return 0 + def _get_fontmap(self, string): """Get the mapping from characters to the font that includes them. diff --git a/lib/matplotlib/dviread.pyi b/lib/matplotlib/dviread.pyi index 1a3b3943d07b..de429bd0b7f1 100644 --- a/lib/matplotlib/dviread.pyi +++ b/lib/matplotlib/dviread.pyi @@ -78,6 +78,8 @@ class DviFont: def widths(self) -> list[int]: ... @property def fname(self) -> str: ... + @property + def face_index(self) -> int: ... def resolve_path(self) -> Path: ... @property def subfont(self) -> int: ... diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index 2dc22fd9170e..2a9c3542e277 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -3,6 +3,7 @@ import io import os from pathlib import Path +import string import numpy as np import pytest @@ -365,7 +366,7 @@ def test_glyphs_subset(): # subsetted FT2Font glyph_indices = {nosubcmap[ord(c)] for c in chars} - with get_glyphs_subset(fpath, glyph_indices) as subset: + with get_glyphs_subset(fm.FontPath(fpath, 0), glyph_indices) as subset: subfont = FT2Font(font_as_file(subset)) subfont.set_text(chars) subcmap = subfont.get_charmap() @@ -402,6 +403,38 @@ def test_multi_font_type42(): horizontalalignment='center', verticalalignment='center') +@image_comparison(['ttc_type3.pdf'], style='mpl20') +def test_ttc_type3(): + fp = fm.FontProperties(family=['WenQuanYi Zen Hei']) + if Path(fm.findfont(fp)).name != 'wqy-zenhei.ttc': + pytest.skip('Font wqy-zenhei.ttc may be missing') + + fonts = ['WenQuanYi Zen Hei', 'WenQuanYi Zen Hei Mono'] + plt.rc('font', size=16) + plt.rc('pdf', fonttype=3) + + figs = plt.figure(figsize=(7, len(fonts) / 2)).subfigures(len(fonts)) + for font, fig in zip(fonts, figs): + fig.text(0.5, 0.5, f'{font}: {string.ascii_uppercase}', font=font, + horizontalalignment='center', verticalalignment='center') + + +@image_comparison(['ttc_type42.pdf'], style='mpl20') +def test_ttc_type42(): + fp = fm.FontProperties(family=['WenQuanYi Zen Hei']) + if Path(fm.findfont(fp)).name != 'wqy-zenhei.ttc': + pytest.skip('Font wqy-zenhei.ttc may be missing') + + fonts = ['WenQuanYi Zen Hei', 'WenQuanYi Zen Hei Mono'] + plt.rc('font', size=16) + plt.rc('pdf', fonttype=42) + + figs = plt.figure(figsize=(7, len(fonts) / 2)).subfigures(len(fonts)) + for font, fig in zip(fonts, figs): + fig.text(0.5, 0.5, f'{font}: {string.ascii_uppercase}', font=font, + horizontalalignment='center', verticalalignment='center') + + @pytest.mark.parametrize('family_name, file_name', [("Noto Sans", "NotoSans-Regular.otf"), ("FreeMono", "FreeMono.otf")]) diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index 9859a286e5fd..bb6b08d14a6d 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -1,12 +1,14 @@ from collections import Counter import io +from pathlib import Path import re +import string import tempfile import numpy as np import pytest -from matplotlib import cbook, path, patheffects +from matplotlib import cbook, font_manager, path, patheffects from matplotlib.figure import Figure from matplotlib.patches import Ellipse from matplotlib.testing import _gen_multi_font_text @@ -340,6 +342,38 @@ def test_multi_font_type42(): horizontalalignment='center', verticalalignment='center') +@image_comparison(['ttc_type3.eps'], style='mpl20') +def test_ttc_type3(): + fp = font_manager.FontProperties(family=['WenQuanYi Zen Hei']) + if Path(font_manager.findfont(fp)).name != 'wqy-zenhei.ttc': + pytest.skip('Font wqy-zenhei.ttc may be missing') + + fonts = ['WenQuanYi Zen Hei', 'WenQuanYi Zen Hei Mono'] + plt.rc('font', size=16) + plt.rc('pdf', fonttype=3) + + figs = plt.figure(figsize=(7, len(fonts) / 2)).subfigures(len(fonts)) + for font, fig in zip(fonts, figs): + fig.text(0.5, 0.5, f'{font}: {string.ascii_uppercase}', font=font, + horizontalalignment='center', verticalalignment='center') + + +@image_comparison(['ttc_type42.eps'], style='mpl20') +def test_ttc_type42(): + fp = font_manager.FontProperties(family=['WenQuanYi Zen Hei']) + if Path(font_manager.findfont(fp)).name != 'wqy-zenhei.ttc': + pytest.skip('Font wqy-zenhei.ttc may be missing') + + fonts = ['WenQuanYi Zen Hei', 'WenQuanYi Zen Hei Mono'] + plt.rc('font', size=16) + plt.rc('pdf', fonttype=42) + + figs = plt.figure(figsize=(7, len(fonts) / 2)).subfigures(len(fonts)) + for font, fig in zip(fonts, figs): + fig.text(0.5, 0.5, f'{font}: {string.ascii_uppercase}', font=font, + horizontalalignment='center', verticalalignment='center') + + @image_comparison(["scatter.eps"]) def test_path_collection(): rng = np.random.default_rng(19680801) From e9917908b800db32f57acd3a8b3a58df54937dd0 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 25 Jul 2025 19:53:21 -0400 Subject: [PATCH 5/7] pgf: Support any font in a collection Note, this only has an effect if set as the global font. Otherwise, just the font name is recorded, and the TeX engine's normal lookup is performed. --- lib/matplotlib/backends/backend_pgf.py | 25 ++++++++++++------------ lib/matplotlib/tests/test_backend_pgf.py | 20 +++++++++++++++++++ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 48b6e8ac152c..ab9782b369a3 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -38,9 +38,17 @@ def _get_preamble(): """Prepare a LaTeX preamble based on the rcParams configuration.""" - font_size_pt = FontProperties( - size=mpl.rcParams["font.size"] - ).get_size_in_points() + def _to_fontspec(): + for command, family in [("setmainfont", "serif"), + ("setsansfont", "sans\\-serif"), + ("setmonofont", "monospace")]: + font_path = fm.findfont(family) + path = pathlib.Path(font_path) + yield r" \%s{%s}[Path=\detokenize{%s/}%s]" % ( + command, path.name, path.parent.as_posix(), + f',FontIndex={font_path.face_index:d}' if path.suffix == '.ttc' else '') + + font_size_pt = FontProperties(size=mpl.rcParams["font.size"]).get_size_in_points() return "\n".join([ # Remove Matplotlib's custom command \mathdefault. (Not using # \mathnormal instead since this looks odd with Computer Modern.) @@ -63,15 +71,8 @@ def _get_preamble(): *([ r"\ifdefined\pdftexversion\else % non-pdftex case.", r" \usepackage{fontspec}", - ] + [ - r" \%s{%s}[Path=\detokenize{%s/}]" - % (command, path.name, path.parent.as_posix()) - for command, path in zip( - ["setmainfont", "setsansfont", "setmonofont"], - [pathlib.Path(fm.findfont(family)) - for family in ["serif", "sans\\-serif", "monospace"]] - ) - ] + [r"\fi"] if mpl.rcParams["pgf.rcfonts"] else []), + *_to_fontspec(), + r"\fi"] if mpl.rcParams["pgf.rcfonts"] else []), # Documented as "must come last". mpl.texmanager._usepackage_if_not_loaded("underscore", option="strings"), ]) diff --git a/lib/matplotlib/tests/test_backend_pgf.py b/lib/matplotlib/tests/test_backend_pgf.py index e218a81cdceb..e5b73c9450f3 100644 --- a/lib/matplotlib/tests/test_backend_pgf.py +++ b/lib/matplotlib/tests/test_backend_pgf.py @@ -1,7 +1,9 @@ import datetime from io import BytesIO import os +from pathlib import Path import shutil +import string import numpy as np from packaging.version import parse as parse_version @@ -9,6 +11,7 @@ import matplotlib as mpl import matplotlib.pyplot as plt +from matplotlib.font_manager import FontProperties, findfont from matplotlib.testing import _has_tex_package, _check_for_pgf from matplotlib.testing.exceptions import ImageComparisonFailure from matplotlib.testing.compare import compare_images @@ -330,6 +333,23 @@ def test_png_transparency(): # Actually, also just testing that png works. assert (t[..., 3] == 0).all() # fully transparent. +@needs_pgf_xelatex +@pytest.mark.backend('pgf') +@image_comparison(['ttc_pgf.pdf'], style='mpl20') +def test_ttc_output(): + fp = FontProperties(family=['WenQuanYi Zen Hei']) + if Path(findfont(fp)).name != 'wqy-zenhei.ttc': + pytest.skip('Font wqy-zenhei.ttc may be missing') + + fonts = {'sans-serif': 'WenQuanYi Zen Hei', 'monospace': 'WenQuanYi Zen Hei Mono'} + plt.rc('font', size=16, **fonts) + + figs = plt.figure(figsize=(7, len(fonts) / 2)).subfigures(len(fonts)) + for font, fig in zip(fonts.values(), figs): + fig.text(0.5, 0.5, f'{font}: {string.ascii_uppercase}', font=font, + horizontalalignment='center', verticalalignment='center') + + @needs_pgf_xelatex def test_unknown_font(caplog): with caplog.at_level("WARNING"): From 9c20b0ce57938213201897e7c9cf3dc012e78cb2 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 31 Oct 2025 04:33:26 -0400 Subject: [PATCH 6/7] DOC: Add what's new note for TTC loading --- doc/release/next_whats_new/ttc_fonts.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 doc/release/next_whats_new/ttc_fonts.rst diff --git a/doc/release/next_whats_new/ttc_fonts.rst b/doc/release/next_whats_new/ttc_fonts.rst new file mode 100644 index 000000000000..b80b1186707b --- /dev/null +++ b/doc/release/next_whats_new/ttc_fonts.rst @@ -0,0 +1,18 @@ +Support for loading TrueType Collection fonts +--------------------------------------------- + +TrueType Collection fonts (commonly found as files with a ``.ttc`` extension) are now +supported. Namely, Matplotlib will include these file extensions in its scan for system +fonts, and will add all sub-fonts to its list of available fonts (i.e., the list from +`~.font_manager.get_font_names`). + +From most high-level API, this means you should be able to specify the name of any +sub-font in a collection just as you would any other font. Note that at this time, there +is no way to specify the entire collection with any sort of automated selection of the +internal sub-fonts. + +In the low-level API, to ensure backwards-compatibility while facilitating this new +support, a `.FontPath` instance (comprised of a font path and a sub-font index, with +behaviour similar to a `str`) may be passed to the font management API in place of a +simple `os.PathLike` path. Any font management API that previously returned a string path +now returns a `.FontPath` instance instead. From b2aa1f238d355551d6cff5866c2feccb69f27cbd Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 26 Sep 2025 00:26:26 -0400 Subject: [PATCH 7/7] Update test images for previous changes --- .../test_backend_pdf/ttc_type3.pdf | Bin 0 -> 21114 bytes .../test_backend_pdf/ttc_type42.pdf | Bin 0 -> 9696 bytes .../test_backend_pgf/ttc_pgf.pdf | Bin 0 -> 15526 bytes .../test_backend_ps/ttc_type3.eps | 1483 +++++++++++++++++ .../test_backend_ps/ttc_type42.eps | 1483 +++++++++++++++++ 5 files changed, 2966 insertions(+) create mode 100644 lib/matplotlib/tests/baseline_images/test_backend_pdf/ttc_type3.pdf create mode 100644 lib/matplotlib/tests/baseline_images/test_backend_pdf/ttc_type42.pdf create mode 100644 lib/matplotlib/tests/baseline_images/test_backend_pgf/ttc_pgf.pdf create mode 100644 lib/matplotlib/tests/baseline_images/test_backend_ps/ttc_type3.eps create mode 100644 lib/matplotlib/tests/baseline_images/test_backend_ps/ttc_type42.eps diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pdf/ttc_type3.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pdf/ttc_type3.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a3fad017236451b768f01358368b601991bf5cf8 GIT binary patch literal 21114 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 Ch|0hK literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pdf/ttc_type42.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pdf/ttc_type42.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d43f233ef4e6bba524f535b400d38f7fe5a45bac GIT binary patch literal 9696 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 LJG-2QJn?@4aq!mr literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pgf/ttc_pgf.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pgf/ttc_pgf.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5d695e734577100ab881040016fbbdb0d76a016a GIT binary patch literal 15526 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 K