[MNT]: Possibly use CoreText to find fonts for faster performance · Issue #31965 · matplotlib/matplotlib · GitHub
Skip to content

[MNT]: Possibly use CoreText to find fonts for faster performance #31965

Description

@iccir

Summary

While investigating #28249, I became curious why system_profiler was taking 7 seconds on my machine to simply list installed fonts. The problem is that it needs to determine the localized name of the font as part of the XML output, which involves loading each font.

If we only care about the path, we should be able to query CoreText directly. The C code would look something like this:

C Program
#include <CoreFoundation/CoreFoundation.h>
#include <CoreText/CoreText.h>
#include <sys/param.h>

int main(int argc, const char *argv[])
{
    CTFontCollectionRef collection = CTFontCollectionCreateFromAvailableFonts(NULL);
    CFArrayRef descriptors = collection ? CTFontCollectionCreateMatchingFontDescriptors(collection) : NULL;
    CFIndex count = descriptors ? CFArrayGetCount(descriptors) : 0;

    for (CFIndex i = 0; i < count; i++) {
        char UTF8Path[MAXPATHLEN * 4];
        CTFontDescriptorRef descriptor = (CTFontDescriptorRef)CFArrayGetValueAtIndex(descriptors, i);

        CFURLRef    url  = CTFontDescriptorCopyAttribute(descriptor, kCTFontURLAttribute);
        CFStringRef path = NULL;
        
        if (url) {
            path = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle);
            CFRelease(url);
        }
        
        if (path) {
            CFStringGetCString(path, UTF8Path, sizeof(UTF8Path), kCFStringEncodingUTF8);
            printf("%s\n", UTF8Path);
            CFRelease(path);
        }
    }

    printf("\n%ld fonts total.\n", (long)count);

    if (descriptors) CFRelease(descriptors);
    if (collection)  CFRelease(collection);

    return 0;
}

That returns 1206 fonts in 63ms.


I asked Claude to convert to a Python script using ctypes:

Python Script
import ctypes
import ctypes.util

CoreText = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreText"))
CF       = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreFoundation"))

CF.CFArrayGetCount.argtypes         = [ctypes.c_void_p]
CF.CFArrayGetCount.restype          = ctypes.c_long
CF.CFArrayGetValueAtIndex.argtypes  = [ctypes.c_void_p, ctypes.c_long]
CF.CFArrayGetValueAtIndex.restype   = ctypes.c_void_p
CF.CFRelease.argtypes               = [ctypes.c_void_p]
CF.CFRelease.restype                = None
CF.CFStringGetCString.argtypes      = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_long, ctypes.c_uint32]
CF.CFStringGetCString.restype       = ctypes.c_bool
CF.CFURLCopyFileSystemPath.argtypes = [ctypes.c_void_p, ctypes.c_long]
CF.CFURLCopyFileSystemPath.restype  = ctypes.c_void_p

CoreText.CTFontCollectionCreateFromAvailableFonts.argtypes      = [ctypes.c_void_p]
CoreText.CTFontCollectionCreateFromAvailableFonts.restype       = ctypes.c_void_p
CoreText.CTFontCollectionCreateMatchingFontDescriptors.argtypes = [ctypes.c_void_p]
CoreText.CTFontCollectionCreateMatchingFontDescriptors.restype  = ctypes.c_void_p
CoreText.CTFontDescriptorCopyAttribute.argtypes                 = [ctypes.c_void_p, ctypes.c_void_p]
CoreText.CTFontDescriptorCopyAttribute.restype                  = ctypes.c_void_p

kCTFontURLAttribute   = ctypes.c_void_p.in_dll(CoreText, "kCTFontURLAttribute")
kCFURLPOSIXPathStyle  = 0
kCFStringEncodingUTF8 = 0x08000100

def cfstring_to_str(cfstr):
    buf = ctypes.create_string_buffer(4096)
    CF.CFStringGetCString(cfstr, buf, 4096, kCFStringEncodingUTF8)
    return buf.value.decode("utf-8")

def list_fonts():
    collection  = CoreText.CTFontCollectionCreateFromAvailableFonts(None)
    descriptors = CoreText.CTFontCollectionCreateMatchingFontDescriptors(collection)
    count       = CF.CFArrayGetCount(descriptors)
    paths       = []

    for i in range(count):
        descriptor = CF.CFArrayGetValueAtIndex(descriptors, i)
        url = CoreText.CTFontDescriptorCopyAttribute(descriptor, kCTFontURLAttribute.value)
        if url:
            cfpath = CF.CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle)
            if cfpath:
                paths.append(cfstring_to_str(cfpath))
                CF.CFRelease(cfpath)
            CF.CFRelease(url)

    CF.CFRelease(descriptors)
    CF.CFRelease(collection)
    return paths

print(f"{len(list_fonts())}")

That's a bit slower at ~90ms, but better than the 7 seconds it takes for system_profiler. Is it worth the added complexity of using ctypes in font_manager.py?

Proposed fix

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions