Skip to content

Add support for loading all fonts from collections #30334

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: text-overhaul
Choose a base branch
from

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. FontManager.get_font accepts a tuple of font path and face index in places it would accept just a path. But also see the next point.
  3. 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 much like the tuple. 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 added 3 commits July 19, 2025 01:42
This enables loading a non-initial font from collections (`.ttc` files).
Currently exposed for `FT2Font` and `font_manager.get_font`.
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`.
@QuLogic QuLogic moved this from Waiting for other PR to Ready for Review in Font and text overhaul Jul 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Ready for Review
Development

Successfully merging this pull request may close these issues.

1 participant