Skip to content

Use CoreText to find macOS fonts - #31977

Merged
QuLogic merged 11 commits into
matplotlib:mainfrom
iccir:coretext-find-fonts
Aug 6, 2026
Merged

Use CoreText to find macOS fonts#31977
QuLogic merged 11 commits into
matplotlib:mainfrom
iccir:coretext-find-fonts

Conversation

@iccir

@iccir iccir commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Closes #28249
Closes #31965

Why is this change necessary?

Prior to this change, findSystemFonts() in font_manager.py called system_profiler -xml SPFontsDataType. This subprocess would load 1000+ font files in order to parse localized names. As we only need font paths, we can instead query CoreText directly. This finishes in ~63ms on my machine instead of ~7 seconds.

Differences in found fonts

system_profiler uses the private kCTFontCollectionIncludeInvisibleFontsOption option to find "invisible" fonts. These are fonts with names starting with '.'. These fonts are typically unavailable using the standard macOS font APIs. On my machine (macOS 14.8), the following fonts are "invisible":

  /System/Library/Fonts/ADTNumeric.ttc
  /System/Library/Fonts/AquaKana.ttc
  /System/Library/Fonts/HelveLTMM
  /System/Library/Fonts/Keyboard.ttf
  /System/Library/Fonts/LastResort.otf
  /System/Library/Fonts/NewYork.ttf
  /System/Library/Fonts/NewYorkItalic.ttf
  /System/Library/Fonts/SFArabic.ttf
  /System/Library/Fonts/SFArabicRounded.ttf
  /System/Library/Fonts/SFArmenian.ttf
  /System/Library/Fonts/SFArmenianRounded.ttf
  /System/Library/Fonts/SFCamera.ttf
  /System/Library/Fonts/SFCompact.ttf
  /System/Library/Fonts/SFCompactItalic.ttf
  /System/Library/Fonts/SFCompactRounded.ttf
  /System/Library/Fonts/SFGeorgian.ttf
  /System/Library/Fonts/SFGeorgianRounded.ttf
  /System/Library/Fonts/SFHebrew.ttf
  /System/Library/Fonts/SFHebrewRounded.ttf
  /System/Library/Fonts/SFNS.ttf
  /System/Library/Fonts/SFNSItalic.ttf
  /System/Library/Fonts/SFNSMono.ttf
  /System/Library/Fonts/SFNSMonoItalic.ttf
  /System/Library/Fonts/SFNSRounded.ttf
  /System/Library/Fonts/ThonburiUI.ttc
  /System/Library/Fonts/TimesLTMM

My change uses the CTFontCollectionCreateFromAvailableFonts() without this private option; hence, these fonts will be missing from the list returned by _get_macos_fonts().

However, font_manager.py later re-scans the "/System/Library/Fonts" path, so it should discover these invisible fonts at that time.

I tested this change on macOS Mojave (10.14) and macOS Sonoma (14.8). I was unable to test on 10.12 or 10.13 as I updated my 10.12 test device to 10.14. The CoreText API used has been available since 10.5, so I do not foresee any compatibility issues.

AI Disclosure

The original C-based implementation in #31965 is my own without any usage of AI. I used AI to help me convert it to C++ and return a py::set (my original implementation printed the paths). I used AI to check the resulting files and verify that I was following the correct formatting guidelines.

PR checklist

Comment thread lib/matplotlib/tests/test_font_manager.py Outdated
Comment thread lib/matplotlib/_c_internal_utils.pyi Outdated
@@ -1,5 +1,6 @@
def display_is_valid() -> bool: ...
def xdisplay_is_valid() -> bool: ...
def find_available_fonts() -> set[str] | None: ...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
def find_available_fonts() -> set[str] | None: ...
def get_available_fonts() -> set[str] | None: ...

Comment thread src/_c_internal_utils.cpp
}
return fonts;
#else
return py::none();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should this be a py::set() call so that you can iterate it on the returning item even on a non macos platform? It is private, so probably not a big deal either way, but you are calling it as for x in None in the _get_macos helper above which would raise instead of returning empty.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was trying to mimic the "on other platforms, returns None" in Win32_GetCurrentProcessExplicitAppUserModelID and Win32_GetForegroundWindow.

I think _get_macos_fonts() should be changed to check for None rather than returning an empty set. To me, an empty set would indicate that the platform-specific code ran but found no fonts.

I think raising NotImplementedError might be the proper solution if the other functions in that file would do the same? I'm not sure (still learning!) :)

Comment thread lib/matplotlib/tests/test_font_manager.py Outdated
Co-authored-by: Greg Lucas <greg.m.lucas@gmail.com>
Comment thread lib/matplotlib/tests/test_font_manager.py Outdated
Comment thread lib/matplotlib/tests/test_font_manager.py Outdated
Comment thread lib/matplotlib/tests/test_font_manager.py Outdated
Comment thread src/_c_internal_utils.cpp Outdated
Comment thread src/_c_internal_utils.cpp Outdated
Comment thread src/_c_internal_utils.cpp Outdated
iccir and others added 6 commits July 27, 2026 19:42
Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com>
Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com>
Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com>
Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com>
@QuLogic

QuLogic commented Jul 29, 2026

Copy link
Copy Markdown
Member

I happened across foliojs/font-manager#19 while looking at these functions. Is that also a problem in this implementation (and was it something that we supported before)?

@iccir

iccir commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

I happened across foliojs/font-manager#19 while looking at these functions. Is that also a problem in this implementation (and was it something that we supported before)?

I'm not sure what is going on in the linked bug. ~/Library/Fonts is being checked on my test devices:

>>> pprint.pp([x for x in _c_internal_utils.get_available_fonts() if "iccir" in x])
['/Users/iccir/Library/Fonts/SF-Mono-Light.otf',
 '/Users/iccir/Library/Fonts/SF-Mono-Bold.otf',
 '/Users/iccir/Library/Fonts/DINishCondensed-Heavy.ttf',
 '/Users/iccir/Library/Fonts/DINish-Italic.otf',
…

My suspicion is one of the following:

  1. There was a bug circa-2017 with CoreText.
  2. The node process didn't have access to ~/Library/Fonts. I wouldn't expect this back in 2017. It would be more common nowadays with all of the additional macOS restrictions.
  3. The user was placing fonts in ~/Library/Fonts without actually registering them with macOS via Font Book. Although the user mentions that they are available in TextEdit so I wouldn't expect this to be the case.

I'm leaning towards 1.

In any case, our previous implemented ran system_profiler as a subprocess, which also uses CTFontCollectionCreateFromAvailableFonts() internally. It's possible that ~/Library/Fonts isn't being accessed on some versions of macOS, but I wouldn't expect that to be a regression from the previous implementation.

@iccir

iccir commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

I did some more digging last night.

system_profiler calls CTFontCollectionCreateFromAvailableFonts() with kCTFontCollectionIncludeDisabledFontsOption turned on. This means that fonts explicitly deactivated/disabled by the user were being included in _get_macos_fonts().

The new code calls CTFontCollectionCreateFromAvailableFonts() with NULL, so deactivated/disabled fonts will not be included.

This is a subtle change of behavior, but I'm not sure if the old behavior is correct. There are situations where a user may have multiple user-level fonts installed with the same name and wants to deactivate one of them.

Assuming we want to change this behavior, should the version number of the font cache be bumped in font_manager.py? It's currently:

    __version__ = '3.11.0'

@QuLogic

QuLogic commented Aug 6, 2026

Copy link
Copy Markdown
Member

Hmm, yes, I think that is correct, as we did the same thing for Windows: #22859

You can bump the cache key to 3.12.0a1 or similar.

@QuLogic
QuLogic merged commit 8aedfd2 into matplotlib:main Aug 6, 2026
42 checks passed
@QuLogic QuLogic added this to the v3.12.0 milestone Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

4 participants