Skip to content

Commit 8aedfd2

Browse files
authored
Merge pull request #31977 from iccir/coretext-find-fonts
Use CoreText to find macOS fonts
2 parents 220ff9c + 7a279ce commit 8aedfd2

6 files changed

Lines changed: 97 additions & 11 deletions

File tree

‎lib/matplotlib/_c_internal_utils.pyi‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
def display_is_valid() -> bool: ...
22
def xdisplay_is_valid() -> bool: ...
3+
def get_available_fonts() -> set[str] | None: ...
34

45
def Win32_GetForegroundWindow() -> int | None: ...
56
def Win32_SetForegroundWindow(hwnd: int) -> None: ...

‎lib/matplotlib/font_manager.py‎

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,13 @@
3636
from numbers import Integral
3737
import os
3838
from pathlib import Path
39-
import plistlib
4039
import re
4140
import subprocess
4241
import sys
4342
import threading
4443

4544
import matplotlib as mpl
46-
from matplotlib import _api, _afm, cbook, ft2font
45+
from matplotlib import _api, _afm, cbook, ft2font, _c_internal_utils
4746
from matplotlib._fontconfig_pattern import (
4847
parse_fontconfig_pattern, generate_fontconfig_pattern)
4948
from matplotlib.rcsetup import _validators
@@ -265,13 +264,12 @@ def _get_fontconfig_fonts():
265264

266265
@cache
267266
def _get_macos_fonts():
268-
"""Cache and list the font paths known to ``system_profiler SPFontsDataType``."""
269-
try:
270-
d, = plistlib.loads(
271-
subprocess.check_output(["system_profiler", "-xml", "SPFontsDataType"]))
272-
except (OSError, subprocess.CalledProcessError, plistlib.InvalidFileException):
267+
"""Cache and list the font paths known to CoreText."""
268+
path_strings = _c_internal_utils.get_available_fonts()
269+
if path_strings:
270+
return [Path(path_string) for path_string in path_strings]
271+
else:
273272
return []
274-
return [Path(entry["path"]) for entry in d["_items"]]
275273

276274

277275
def findSystemFonts(fontpaths=None, fontext='ttf'):
@@ -1175,7 +1173,7 @@ class FontManager:
11751173
# Increment this version number whenever the font cache data
11761174
# format or behavior has changed and requires an existing font
11771175
# cache files to be rebuilt.
1178-
__version__ = '3.11.0'
1176+
__version__ = '3.12.0a1'
11791177

11801178
def __init__(self, size=None, weight='normal'):
11811179
self._version = self.__version__

‎lib/matplotlib/font_manager.pyi‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def get_fontext_synonyms(fontext: str) -> list[str]: ...
2323
def list_fonts(directory: str, extensions: Iterable[str]) -> list[str]: ...
2424
def win32FontDirectory() -> str: ...
2525
def _get_fontconfig_fonts() -> list[Path]: ...
26+
def _get_macos_fonts() -> list[Path]: ...
2627
def _get_font_alt_names(
2728
font: ft2font.FT2Font, primary_name: str
2829
) -> list[tuple[str, int]]: ...

‎lib/matplotlib/tests/test_font_manager.py‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
findfont, findSystemFonts, FontEntry, FontPath, FontProperties, fontManager,
1919
json_dump, json_load, get_font, is_opentype_cff_font,
2020
MSUserFontDirectories, ttfFontProperty, _get_font_alt_names,
21-
_get_fontconfig_fonts, _normalize_weight)
21+
_get_fontconfig_fonts, _get_macos_fonts, _normalize_weight)
2222
from matplotlib import cbook, ft2font, pyplot as plt, rc_context, figure as mfigure
2323
from matplotlib.testing import subprocess_run_helper, subprocess_run_for_testing
2424

@@ -201,6 +201,20 @@ def test_find_invalid(tmp_path):
201201
get_font(bytes(tmp_path / 'non-existent-font-name.ttf'))
202202

203203

204+
@pytest.mark.skipif(sys.platform != 'darwin', reason='macOS only')
205+
def test_get_macos_fonts(tmpdir, monkeypatch):
206+
fonts_found = {font_path.stem for font_path in _get_macos_fonts()}
207+
208+
# Check for various system fonts that are listed on:
209+
# https://developer.apple.com/fonts/system-fonts/
210+
assorted_system_fonts = {
211+
'Apple Braille', 'Avenir', 'Baskerville', 'Cochin', 'Didot', 'Helvetica',
212+
'Hoefler Text', 'Impact', 'Monaco', 'Tahoma', 'Verdana'
213+
}
214+
215+
assert assorted_system_fonts.issubset(fonts_found)
216+
217+
204218
@pytest.mark.skipif(sys.platform != 'linux' or not has_fclist,
205219
reason='only Linux with fontconfig installed')
206220
def test_user_fonts_linux(tmpdir, monkeypatch):

‎src/_c_internal_utils.cpp‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
#else
2929
#define UNUSED_ON_NON_WINDOWS Py_UNUSED
3030
#endif
31+
#ifdef __APPLE__
32+
#include <CoreFoundation/CoreFoundation.h>
33+
#include <CoreText/CoreText.h>
34+
#endif
3135

3236
namespace py = pybind11;
3337
using namespace pybind11::literals;
@@ -95,6 +99,61 @@ mpl_display_is_valid(void)
9599
#endif
96100
}
97101

102+
static py::object
103+
mpl_get_available_fonts(void)
104+
{
105+
#if defined(__APPLE__)
106+
py::set fonts;
107+
108+
auto cfStringToPyStr = [](CFStringRef str) -> py::str {
109+
auto cstr = CFStringGetCStringPtr(str, kCFStringEncodingUTF8);
110+
if (cstr) {
111+
return py::str(cstr);
112+
}
113+
auto length = CFStringGetLength(str);
114+
auto maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
115+
auto buffer = std::make_unique<char[]>(maxSize);
116+
py::str result;
117+
if (CFStringGetCString(str, buffer.get(), maxSize, kCFStringEncodingUTF8)) {
118+
result = py::str(buffer.get());
119+
}
120+
return result;
121+
};
122+
123+
auto collection = CTFontCollectionCreateFromAvailableFonts(NULL);
124+
auto descriptors = collection ?
125+
CTFontCollectionCreateMatchingFontDescriptors(collection) : NULL;
126+
auto count = descriptors ? CFArrayGetCount(descriptors) : 0;
127+
for (CFIndex i = 0; i < count; i++) {
128+
auto descriptor = static_cast<CTFontDescriptorRef>(
129+
CFArrayGetValueAtIndex(descriptors, i));
130+
auto url = static_cast<CFURLRef>(
131+
CTFontDescriptorCopyAttribute(descriptor, kCTFontURLAttribute));
132+
CFStringRef path = nullptr;
133+
if (url) {
134+
path = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle);
135+
CFRelease(url);
136+
}
137+
if (path) {
138+
auto pyStr = cfStringToPyStr(path);
139+
if (pyStr) {
140+
fonts.add(pyStr);
141+
}
142+
CFRelease(path);
143+
}
144+
}
145+
if (descriptors) {
146+
CFRelease(descriptors);
147+
}
148+
if (collection) {
149+
CFRelease(collection);
150+
}
151+
return fonts;
152+
#else
153+
return py::none();
154+
#endif
155+
}
156+
98157
static py::object
99158
mpl_GetCurrentProcessExplicitAppUserModelID(void)
100159
{
@@ -210,6 +269,13 @@ PYBIND11_MODULE(_c_internal_utils, m, py::mod_gil_not_used())
210269
only (e.g., for Tkinter).
211270
212271
On other platforms, always returns True.)""");
272+
m.def(
273+
"get_available_fonts", &mpl_get_available_fonts,
274+
R"""( --
275+
On macOS, uses CoreText to find all fonts available to the current
276+
process and returns the paths as a set of strings.
277+
278+
On other platforms, always returns None.)""");
213279
m.def(
214280
"Win32_GetCurrentProcessExplicitAppUserModelID",
215281
&mpl_GetCurrentProcessExplicitAppUserModelID,

‎src/meson.build‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ else
3030
user32 = []
3131
endif
3232

33+
if host_machine.system() == 'darwin'
34+
coretext = dependency('appleframeworks', modules: 'CoreText')
35+
else
36+
coretext = []
37+
endif
38+
3339
extension_data = {
3440
'_backend_agg': {
3541
'subdir': 'matplotlib/backends',
@@ -44,7 +50,7 @@ extension_data = {
4450
'sources': files(
4551
'_c_internal_utils.cpp',
4652
),
47-
'dependencies': [pybind11_dep, dl, ole32, shell32, user32],
53+
'dependencies': [pybind11_dep, dl, ole32, shell32, user32, coretext],
4854
},
4955
'ft2font': {
5056
'subdir': 'matplotlib',

0 commit comments

Comments
 (0)