Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/matplotlib/_c_internal_utils.pyi
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
def display_is_valid() -> bool: ...
def xdisplay_is_valid() -> bool: ...
def get_available_fonts() -> set[str] | None: ...

def Win32_GetForegroundWindow() -> int | None: ...
def Win32_SetForegroundWindow(hwnd: int) -> None: ...
Expand Down
14 changes: 6 additions & 8 deletions lib/matplotlib/font_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,13 @@
from numbers import Integral
import os
from pathlib import Path
import plistlib
import re
import subprocess
import sys
import threading

import matplotlib as mpl
from matplotlib import _api, _afm, cbook, ft2font
from matplotlib import _api, _afm, cbook, ft2font, _c_internal_utils
from matplotlib._fontconfig_pattern import (
parse_fontconfig_pattern, generate_fontconfig_pattern)
from matplotlib.rcsetup import _validators
Expand Down Expand Up @@ -266,13 +265,12 @@ def _get_fontconfig_fonts():

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


def findSystemFonts(fontpaths=None, fontext='ttf'):
Expand Down
1 change: 1 addition & 0 deletions lib/matplotlib/font_manager.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def get_fontext_synonyms(fontext: str) -> list[str]: ...
def list_fonts(directory: str, extensions: Iterable[str]) -> list[str]: ...
def win32FontDirectory() -> str: ...
def _get_fontconfig_fonts() -> list[Path]: ...
def _get_macos_fonts() -> list[Path]: ...
def _get_font_alt_names(
font: ft2font.FT2Font, primary_name: str
) -> list[tuple[str, int]]: ...
Expand Down
16 changes: 15 additions & 1 deletion lib/matplotlib/tests/test_font_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
findfont, findSystemFonts, FontEntry, FontPath, FontProperties, fontManager,
json_dump, json_load, get_font, is_opentype_cff_font,
MSUserFontDirectories, ttfFontProperty, _get_font_alt_names,
_get_fontconfig_fonts, _normalize_weight)
_get_fontconfig_fonts, _normalize_weight, _get_macos_fonts)
Comment thread
iccir marked this conversation as resolved.
Outdated
from matplotlib import cbook, ft2font, pyplot as plt, rc_context, figure as mfigure
from matplotlib.testing import subprocess_run_helper, subprocess_run_for_testing

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


@pytest.mark.skipif(sys.platform != 'darwin', reason='macOS only')
def test_get_macos_fonts(tmpdir, monkeypatch):
fonts_found = set(font_path.stem for font_path in _get_macos_fonts())
Comment thread
iccir marked this conversation as resolved.
Outdated

# Check for various system fonts that are listed on:
# https://developer.apple.com/fonts/system-fonts/
assorted_system_fonts = set([
'Apple Braille', 'Avenir', 'Baskerville', 'Cochin', 'Didot', 'Helvetica',
'Hoefler Text', 'Impact', 'Monaco', 'Tahoma', 'Verdana'
])
Comment thread
iccir marked this conversation as resolved.
Outdated

assert assorted_system_fonts.issubset(fonts_found)


@pytest.mark.skipif(sys.platform != 'linux' or not has_fclist,
reason='only Linux with fontconfig installed')
def test_user_fonts_linux(tmpdir, monkeypatch):
Expand Down
67 changes: 67 additions & 0 deletions src/_c_internal_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
#else
#define UNUSED_ON_NON_WINDOWS Py_UNUSED
#endif
#ifdef __APPLE__
#include <CoreFoundation/CoreFoundation.h>
#include <CoreText/CoreText.h>
#endif

namespace py = pybind11;
using namespace pybind11::literals;
Expand Down Expand Up @@ -95,6 +99,62 @@ mpl_display_is_valid(void)
#endif
}

static py::object
mpl_GetAvailableFonts(void)
Comment thread
iccir marked this conversation as resolved.
Outdated
{
#if defined(__APPLE__)
py::set fonts;

auto cfStringToPyStr = [](CFStringRef str) -> py::str {
auto cstr = CFStringGetCStringPtr(str, kCFStringEncodingUTF8);
if (cstr) {
return py::str(cstr);
}
auto length = CFStringGetLength(str);
auto maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
auto buffer = new char[maxSize];
Comment thread
iccir marked this conversation as resolved.
Outdated
py::str result;
if (CFStringGetCString(str, buffer, maxSize, kCFStringEncodingUTF8)) {
result = py::str(buffer);
}
delete[] buffer;
return result;
};

auto collection = CTFontCollectionCreateFromAvailableFonts(NULL);
auto descriptors = collection ?
CTFontCollectionCreateMatchingFontDescriptors(collection) : NULL;
auto count = descriptors ? CFArrayGetCount(descriptors) : 0;
for (CFIndex i = 0; i < count; i++) {
auto descriptor = static_cast<CTFontDescriptorRef>(
CFArrayGetValueAtIndex(descriptors, i));
auto url = static_cast<CFURLRef>(
CTFontDescriptorCopyAttribute(descriptor, kCTFontURLAttribute));
CFStringRef path = NULL;
Comment thread
iccir marked this conversation as resolved.
Outdated
if (url) {
path = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle);
CFRelease(url);
}
if (path) {
auto pyStr = cfStringToPyStr(path);
if (pyStr) {
fonts.add(pyStr);
}
CFRelease(path);
}
}
if (descriptors) {
CFRelease(descriptors);
}
if (collection) {
CFRelease(collection);
}
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!) :)

#endif
}

static py::object
mpl_GetCurrentProcessExplicitAppUserModelID(void)
{
Expand Down Expand Up @@ -210,6 +270,13 @@ PYBIND11_MODULE(_c_internal_utils, m, py::mod_gil_not_used())
only (e.g., for Tkinter).

On other platforms, always returns True.)""");
m.def(
"get_available_fonts", &mpl_GetAvailableFonts,
R"""( --
On macOS, uses CoreText to find all fonts available to the current
process and returns the paths as a set of strings.

On other platforms, always returns None.)""");
m.def(
"Win32_GetCurrentProcessExplicitAppUserModelID",
&mpl_GetCurrentProcessExplicitAppUserModelID,
Expand Down
8 changes: 7 additions & 1 deletion src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ else
user32 = []
endif

if host_machine.system() == 'darwin'
coretext = dependency('appleframeworks', modules: 'CoreText')
else
coretext = []
endif

extension_data = {
'_backend_agg': {
'subdir': 'matplotlib/backends',
Expand All @@ -44,7 +50,7 @@ extension_data = {
'sources': files(
'_c_internal_utils.cpp',
),
'dependencies': [pybind11_dep, dl, ole32, shell32, user32],
'dependencies': [pybind11_dep, dl, ole32, shell32, user32, coretext],
},
'ft2font': {
'subdir': 'matplotlib',
Expand Down
Loading