Skip to content

Conversation

@QuLogic
Copy link
Member

@QuLogic QuLogic commented Jul 19, 2025

PR summary

This turned out to be more straightforward than I expected, but it will probably need a few API decisions to be fully complete.

From bottom to top of the API:

  1. FT2Font accepts a face_index parameter to specify which face to load in a collection, and a corresponding face_index property to check what's loaded.
  2. For backwards-compatibility, FontManager.findfont returns a str-like class FontPath (name up for debate) which has a face_index attribute and is accepted by get_font. If anyone uses them as strings though, it should act pretty much the same.

For example, now I can see all variants of Noto Sans CJK:

>>> import matplotlib.font_manager
>>> for fe in matplotlib.font_manager.fontManager.ttflist:
...     if fe.name.startswith('Noto Sans Mono CJK'):
...         print(fe)
...         
FontEntry(fname='/usr/share/fonts/google-noto-sans-mono-cjk-vf-fonts/NotoSansMonoCJK-VF.ttc', index=0, name='Noto Sans Mono CJK JP', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')
FontEntry(fname='/usr/share/fonts/google-noto-sans-mono-cjk-vf-fonts/NotoSansMonoCJK-VF.ttc', index=1, name='Noto Sans Mono CJK KR', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')
FontEntry(fname='/usr/share/fonts/google-noto-sans-mono-cjk-vf-fonts/NotoSansMonoCJK-VF.ttc', index=2, name='Noto Sans Mono CJK SC', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')
FontEntry(fname='/usr/share/fonts/google-noto-sans-mono-cjk-vf-fonts/NotoSansMonoCJK-VF.ttc', index=3, name='Noto Sans Mono CJK TC', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')
FontEntry(fname='/usr/share/fonts/google-noto-sans-mono-cjk-vf-fonts/NotoSansMonoCJK-VF.ttc', index=4, name='Noto Sans Mono CJK HK', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')
FontEntry(fname='/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc', index=5, name='Noto Sans Mono CJK JP', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')
FontEntry(fname='/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc', index=6, name='Noto Sans Mono CJK KR', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')
FontEntry(fname='/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc', index=7, name='Noto Sans Mono CJK SC', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')
FontEntry(fname='/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc', index=8, name='Noto Sans Mono CJK TC', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')
FontEntry(fname='/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc', index=9, name='Noto Sans Mono CJK HK', style='normal', variant='normal', weight=400, stretch='normal', size='scalable')
FontEntry(fname='/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Bold.ttc', index=5, name='Noto Sans Mono CJK JP', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')
FontEntry(fname='/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Bold.ttc', index=6, name='Noto Sans Mono CJK KR', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')
FontEntry(fname='/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Bold.ttc', index=7, name='Noto Sans Mono CJK SC', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')
FontEntry(fname='/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Bold.ttc', index=8, name='Noto Sans Mono CJK TC', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')
FontEntry(fname='/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Bold.ttc', index=9, name='Noto Sans Mono CJK HK', style='normal', variant='normal', weight=700, stretch='normal', size='scalable')

or all variants of WenQuanYi that we use for tests:

>>> for fe in matplotlib.font_manager.fontManager.ttflist:
...     if fe.name.startswith('WenQuan'):
...         print(fe)
...         
FontEntry(fname='/usr/share/fonts/wqy-zenhei-fonts/wqy-zenhei.ttc', index=0, name='WenQuanYi Zen Hei', style='normal', variant='normal', weight=500, stretch='normal', size='scalable')
FontEntry(fname='/usr/share/fonts/wqy-zenhei-fonts/wqy-zenhei.ttc', index=1, name='WenQuanYi Zen Hei Mono', style='normal', variant='normal', weight=500, stretch='normal', size='scalable')
FontEntry(fname='/usr/share/fonts/wqy-zenhei-fonts/wqy-zenhei.ttc', index=2, name='WenQuanYi Zen Hei Sharp', style='normal', variant='normal', weight=500, stretch='normal', size='scalable')

Fixes #3135

TODO

  1. Font subsetting in vector formats still hard-codes face index 0. This is mostly because it passes around filenames everywhere, so that will have to be changed to include the face index.
  2. As you can see above, there are two versions of Noto Sans CJK that are found: one for each weight and a variable font. We don't seem to correctly read any differentiating metadata for the variable font, so whether it or the separate Regular version gets picked is a bit up in the air, I think.

API questions

  1. Considering the typing is quite long with str | bytes | Path, I wonder if we should change to os.PathLike?
  2. I originally started with the path-index tuple as an end goal, but after creating the backwards-compatible FontPath, I'm thinking maybe that's redundant and we should just stick with the FontPath class only. Do we want to accept the tuple form as well, or should I drop it?
  3. Currently, FontPath is a subclass of str which allows using it as a str as one normally would. That was the minimum implementation needed, but we probably want to flesh that out a bit. At minimum, I think we should implement __eq__ and __hash__ so that you can use it as a dictionary key without clashing with an equivalent str. But then do we want to add a deprecation warning when making those comparisons? And after thinking about it a bit more, would a namedtuple with __eq__ instead be a better choice?
  4. Do we want to figure out a way to group these somehow (in a way something like Add font.superfamily support with genre-aware resolution #30155)? Unfortunately, I think this is actually non-trivial. On Fedora for example, the Noto Sans package installs a fontconfig file that groups them and specifies which language corresponds to which font. It is likely not something embedded in the font that we can read ourselves, and we don't use fontconfig either.

PR checklist

@QuLogic QuLogic added this to the v3.11.0 milestone Jul 19, 2025
@QuLogic QuLogic added the status: needs comment/discussion needs consensus on next step label Jul 19, 2025
@github-project-automation github-project-automation bot moved this to Waiting for other PR in Font and text overhaul Jul 19, 2025
@QuLogic QuLogic moved this from Waiting for other PR to Ready for Review in Font and text overhaul Jul 19, 2025
@QuLogic
Copy link
Member Author

QuLogic commented Jul 25, 2025

We had a discussion about this on the call earlier.

  • I originally started with the path-index tuple as an end goal, but after creating the backwards-compatible FontPath, I'm thinking maybe that's redundant and we should just stick with the FontPath class only. Do we want to accept the tuple form as well, or should I drop it?

We'll drop the tuple for now, as it seems the str-ish type should be fine.

  • Currently, FontPath is a subclass of str which allows using it as a str as one normally would. That was the minimum implementation needed, but we probably want to flesh that out a bit. At minimum, I think we should implement __eq__ and __hash__ so that you can use it as a dictionary key without clashing with an equivalent str. But then do we want to add a deprecation warning when making those comparisons? And after thinking about it a bit more, would a namedtuple with __eq__ instead be a better choice?

We'll not deprecate anything for now, and see how this works out. Since FontPath is fairly equivalent to str, there likely won't be much in the way of breakage. For methods, we'll implement at least __eq__, __hash__ and __repr__.

  • Do we want to figure out a way to group these somehow

Will leave this for later, if necessary.

@QuLogic QuLogic marked this pull request as ready for review July 25, 2025 11:02
@QuLogic QuLogic removed the status: needs comment/discussion needs consensus on next step label Jul 25, 2025
@QuLogic
Copy link
Member Author

QuLogic commented Jul 25, 2025

This should work almost everywhere now, I think. The only thing that doesn't support the face index is LaTeX, via PGF or DviFont (which just hard-codes index 0). I think the latter can be covered by the refactors in #29939

@QuLogic
Copy link
Member Author

QuLogic commented Jul 25, 2025

Also, tests use the WenQuanYi Zen Hei that we already conditionally use, but maybe we should generate a minimal .ttc file for testing instead.

@QuLogic
Copy link
Member Author

QuLogic commented Sep 26, 2025

I've rebased now that I think there should not be any other PRs that might cause conficts.

@QuLogic QuLogic marked this pull request as ready for review September 26, 2025 04:47
@QuLogic QuLogic force-pushed the ttc-loading branch 2 times, most recently from e751dc2 to a32ad5b Compare October 23, 2025 08:33
@QuLogic QuLogic force-pushed the ttc-loading branch 2 times, most recently from 44f03ea to 1a2bba2 Compare October 31, 2025 08:33
@QuLogic
Copy link
Member Author

QuLogic commented Oct 31, 2025

Just realized I didn't have any What's new docs here, so I've added a note.

Copy link
Member

@tacaswell tacaswell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have one minor quibble about c++ function signatures, not critical.

This enables loading a non-initial font from collections (`.ttc` files).
Currently exposed for `FT2Font`, only.
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.
For backwards-compatibility, the path+index is passed around in a
lightweight subclass of `str`.
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.
@ksunden ksunden merged commit e84ce2b into matplotlib:text-overhaul Dec 4, 2025
34 of 36 checks passed
@github-project-automation github-project-automation bot moved this from Ready for Review to Done in Font and text overhaul Dec 4, 2025
@QuLogic QuLogic deleted the ttc-loading branch December 4, 2025 21:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Please add support for ttc font files (PDF/PS output)

3 participants