FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Use CoreText to find macOS fonts by iccir · Pull Request #31977 · matplotlib/matplotlib · GitHub

Repository navigation

1 change: 1 addition & 0 deletions lib/matplotlib/_c_internal_utils.pyi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
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

Back | FazBrowse Home | New Git URL