Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 7 additions & 9 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 Expand Up @@ -1224,7 +1222,7 @@ class FontManager:
# Increment this version number whenever the font cache data
# format or behavior has changed and requires an existing font
# cache files to be rebuilt.
__version__ = '3.11.0'
__version__ = '3.12.0a1'

def __init__(self, size=None, weight='normal'):
self._version = self.__version__
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, _get_macos_fonts, _normalize_weight)
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 = {font_path.stem for font_path in _get_macos_fonts()}

# Check for various system fonts that are listed on:
# https://developer.apple.com/fonts/system-fonts/
assorted_system_fonts = {
'Apple Braille', 'Avenir', 'Baskerville', 'Cochin', 'Didot', 'Helvetica',
'Hoefler Text', 'Impact', 'Monaco', 'Tahoma', 'Verdana'
}

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
66 changes: 66 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,61 @@ mpl_display_is_valid(void)
#endif
}

static py::object
mpl_get_available_fonts(void)
{
#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 = std::make_unique<char[]>(maxSize);
py::str result;
if (CFStringGetCString(str, buffer.get(), maxSize, kCFStringEncodingUTF8)) {
result = py::str(buffer.get());
}
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 = nullptr;
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 +269,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_get_available_fonts,
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