Get variable font data from CoreText

This commit is contained in:
Kovid Goyal
2024-04-22 14:29:38 +05:30
parent 2f3578ad93
commit 2244b664bc
6 changed files with 150 additions and 56 deletions

View File

@@ -22,6 +22,8 @@
#import <Foundation/NSDictionary.h>
#define debug debug_fonts
static inline void cleanup_cfrelease(void *__p) { CFTypeRef *tp = (CFTypeRef *)__p; CFTypeRef cf = *tp; if (cf) { CFRelease(cf); } }
#define RAII_CoreFoundation(type, name, initializer) __attribute__((cleanup(cleanup_cfrelease))) type name = initializer
typedef struct {
PyObject_HEAD
@@ -35,20 +37,22 @@ typedef struct {
PyTypeObject CTFace_Type;
static CTFontRef window_title_font = nil;
static char*
static PyObject*
convert_cfstring(CFStringRef src, int free_src) {
#define SZ 4094
static char buf[SZ+2] = {0};
bool ok = false;
if(!CFStringGetCString(src, buf, SZ, kCFStringEncodingUTF8)) PyErr_SetString(PyExc_ValueError, "Failed to convert CFString");
else ok = true;
if (free_src) CFRelease(src);
return ok ? buf : NULL;
RAII_CoreFoundation(CFStringRef, releaseme, free_src ? src : nil);
(void)releaseme;
if (!src) return PyUnicode_FromString("");
const char *fast = CFStringGetCStringPtr(src, kCFStringEncodingUTF8);
if (fast) return PyUnicode_FromString(fast);
#define SZ 4096
char buf[SZ];
if(!CFStringGetCString(src, buf, SZ, kCFStringEncodingUTF8)) { PyErr_SetString(PyExc_ValueError, "Failed to convert CFString"); return NULL; }
return PyUnicode_FromString(buf);
#undef SZ
}
static void
init_face(CTFace *self, CTFontRef font, FONTS_DATA_HANDLE fg UNUSED) {
init_face(CTFace *self, CTFontRef font) {
if (self->hb_font) hb_font_destroy(self->hb_font);
self->hb_font = NULL;
if (self->ct_font) CFRelease(self->ct_font);
@@ -63,15 +67,15 @@ init_face(CTFace *self, CTFontRef font, FONTS_DATA_HANDLE fg UNUSED) {
}
static CTFace*
ct_face(CTFontRef font, FONTS_DATA_HANDLE fg) {
ct_face(CTFontRef font) {
CTFace *self = (CTFace *)CTFace_Type.tp_alloc(&CTFace_Type, 0);
if (self) {
init_face(self, font, fg);
self->family_name = Py_BuildValue("s", convert_cfstring(CTFontCopyFamilyName(self->ct_font), true));
self->full_name = Py_BuildValue("s", convert_cfstring(CTFontCopyFullName(self->ct_font), true));
self->postscript_name = Py_BuildValue("s", convert_cfstring(CTFontCopyPostScriptName(self->ct_font), true));
init_face(self, font);
self->family_name = convert_cfstring(CTFontCopyFamilyName(self->ct_font), true);
self->full_name = convert_cfstring(CTFontCopyFullName(self->ct_font), true);
self->postscript_name = convert_cfstring(CTFontCopyPostScriptName(self->ct_font), true);
NSURL *url = (NSURL*)CTFontCopyAttribute(self->ct_font, kCTFontURLAttribute);
self->path = Py_BuildValue("s", [[url path] UTF8String]);
self->path = PyUnicode_FromString([[url path] UTF8String]);
[url release];
if (self->family_name == NULL || self->full_name == NULL || self->postscript_name == NULL || self->path == NULL) { Py_CLEAR(self); }
}
@@ -323,7 +327,7 @@ apply_styles_to_fallback_font(CTFontRef original_fallback_font, bool bold, bool
PyObject*
create_fallback_face(PyObject *base_face, CPUCell* cell, bool bold, bool italic, bool emoji_presentation, FONTS_DATA_HANDLE fg) {
CTFace *self = (CTFace*)base_face;
CTFontRef new_font;
RAII_CoreFoundation(CTFontRef, new_font, NULL);
#define search_for_fallback() \
char text[64] = {0}; \
cell_as_utf8_for_fallback(cell, text); \
@@ -341,7 +345,8 @@ create_fallback_face(PyObject *base_face, CPUCell* cell, bool bold, bool italic,
}
else { search_for_fallback(); new_font = apply_styles_to_fallback_font(new_font, bold, italic); }
if (new_font == NULL) return NULL;
PyObject *postscript_name = Py_BuildValue("s", convert_cfstring(CTFontCopyPostScriptName(new_font), true));
RAII_PyObject(postscript_name, convert_cfstring(CTFontCopyPostScriptName(new_font), true));
if (!postscript_name) return NULL;
ssize_t idx = -1;
PyObject *q, *ans = NULL;
while ((q = iter_fallback_faces(fg, &idx))) {
@@ -351,10 +356,7 @@ create_fallback_face(PyObject *base_face, CPUCell* cell, bool bold, bool italic,
break;
}
}
Py_CLEAR(postscript_name);
if (ans == NULL) return (PyObject*)ct_face(new_font, fg);
CFRelease(new_font);
return ans;
return ans ? ans : (PyObject*)ct_face(new_font);
}
unsigned int
@@ -393,7 +395,7 @@ set_size_for_face(PyObject *s, unsigned int UNUSED desired_height, bool force, F
if (!force && self->scaled_point_sz == sz) return true;
CTFontRef new_font = CTFontCreateCopyWithAttributes(self->ct_font, sz, NULL, NULL);
if (new_font == NULL) fatal("Out of memory");
init_face(self, new_font, fg);
init_face(self, new_font);
return true;
}
@@ -479,21 +481,34 @@ PyObject*
face_from_descriptor(PyObject *descriptor, FONTS_DATA_HANDLE fg) {
CTFontDescriptorRef desc = font_descriptor_from_python(descriptor);
if (!desc) return NULL;
CTFontRef font = CTFontCreateWithFontDescriptor(desc, scaled_point_sz(fg), NULL);
CTFontRef font = CTFontCreateWithFontDescriptor(desc, fg ? scaled_point_sz(fg) : 12, NULL);
CFRelease(desc); desc = NULL;
if (!font) { PyErr_SetString(PyExc_ValueError, "Failed to create CTFont object"); return NULL; }
return (PyObject*) ct_face(font, fg);
return (PyObject*) ct_face(font);
}
PyObject*
face_from_path(const char *path, int UNUSED index, FONTS_DATA_HANDLE fg) {
face_from_path(const char *path, int UNUSED index, FONTS_DATA_HANDLE fg UNUSED) {
CFStringRef s = CFStringCreateWithCString(NULL, path, kCFStringEncodingUTF8);
CFURLRef url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, s, kCFURLPOSIXPathStyle, false);
CGDataProviderRef dp = CGDataProviderCreateWithURL(url);
CGFontRef cg_font = CGFontCreateWithDataProvider(dp);
CTFontRef ct_font = CTFontCreateWithGraphicsFont(cg_font, 0.0, NULL, NULL);
CFRelease(cg_font); CFRelease(dp); CFRelease(url); CFRelease(s);
return (PyObject*) ct_face(ct_font, fg);
return (PyObject*) ct_face(ct_font);
}
static PyObject*
new(PyTypeObject *type UNUSED, PyObject *args, PyObject *kw) {
const char *path = NULL;
PyObject *descriptor = NULL;
static char *kwds[] = {"descriptor", "path", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kw, "|Os", kwds, &descriptor, &path)) return NULL;
if (descriptor) return face_from_descriptor(descriptor, NULL);
if (path) return face_from_path(path, 0, NULL);
PyErr_SetString(PyExc_TypeError, "Must specify either path or descriptor");
return NULL;
}
PyObject*
@@ -760,12 +775,84 @@ render_glyphs_in_cells(PyObject *s, bool bold, bool italic, hb_glyph_info_t *inf
static PyObject*
display_name(CTFace *self) {
CFStringRef dn = CTFontCopyDisplayName(self->ct_font);
const char *d = convert_cfstring(dn, true);
return Py_BuildValue("s", d);
return convert_cfstring(dn, true);
}
static const char*
tag_to_string(uint32_t tag, uint8_t bytes[5]) {
bytes[0] = (tag >> 24) & 0xff;
bytes[1] = (tag >> 16) & 0xff;
bytes[2] = (tag >> 8) & 0xff;
bytes[3] = (tag) & 0xff;
bytes[4] = 0;
return (const char*)bytes;
}
static void
convert_variation_to_python(const void *key, const void *value_, void *axis_values) {
unsigned long tag; float value;
CFNumberGetValue(key, kCFNumberLongType, &tag);
CFNumberGetValue(value_, kCFNumberFloatType, &value);
uint8_t tag_buf[5] = {0};
RAII_PyObject(val, PyFloat_FromDouble((double)value));
if (val) PyDict_SetItemString(axis_values, tag_to_string(tag, tag_buf), val);
}
static PyObject*
get_variable_data(CTFace *self) {
uint8_t tag_buf[5] = {0};
RAII_CoreFoundation(CFArrayRef, cfaxes, CTFontCopyVariationAxes(self->ct_font));
RAII_PyObject(axes, PyTuple_New(CFArrayGetCount(cfaxes)));
for (CFIndex i = 0; i < CFArrayGetCount(cfaxes); i++) {
CFDictionaryRef a = (CFDictionaryRef)CFArrayGetValueAtIndex(cfaxes, i);
#define get_number(key, output, type, optional) { \
CFNumberRef value = (CFNumberRef)CFDictionaryGetValue(a, key); \
if (value) CFNumberGetValue(value, type, &output); \
else if (!optional) { PyErr_Format(PyExc_RuntimeError, "The %s key was not present in the varioation axis dictionary", #key); return NULL; } \
}
float minimum = 0, maximum = 0, def = 0; unsigned long tag = 0, hidden = 0;
get_number(kCTFontVariationAxisDefaultValueKey, def, kCFNumberFloatType, false);
get_number(kCTFontVariationAxisMaximumValueKey, maximum, kCFNumberFloatType, false);
get_number(kCTFontVariationAxisMinimumValueKey, minimum, kCFNumberFloatType, false);
get_number(kCTFontVariationAxisIdentifierKey, tag, kCFNumberLongType, false);
get_number(kCTFontVariationAxisHiddenKey, hidden, kCFNumberLongType, true);
#undef get_number
CFStringRef n = (CFStringRef)CFDictionaryGetValue(a, kCTFontVariationAxisNameKey);
PyObject *axis = Py_BuildValue("{sd sd sd ss sO sO}",
"minimum", minimum, "maximum", maximum, "default", def, "tag", tag_to_string(tag, tag_buf),
"hidden", hidden ? Py_True : Py_False, "strid", convert_cfstring(n, false)
);
if (!axis) return NULL;
PyTuple_SET_ITEM(axes, i, axis);
}
RAII_CoreFoundation(CFStringRef, font_family_name, CTFontCopyFamilyName(self->ct_font));
RAII_CoreFoundation(CTFontDescriptorRef, descriptor,
CTFontDescriptorCreateWithAttributes((__bridge CFDictionaryRef)@{(id)kCTFontFamilyNameAttribute: (__bridge NSString*)font_family_name}));
RAII_CoreFoundation(CTFontCollectionRef, collection,
CTFontCollectionCreateWithFontDescriptors((__bridge CFArrayRef)@[(__bridge id)descriptor], NULL));
RAII_CoreFoundation(CFArrayRef, descriptors, CTFontCollectionCreateMatchingFontDescriptors(collection));
RAII_PyObject(named_styles, PyTuple_New(CFArrayGetCount(descriptors)));
Py_ssize_t actual_num = 0;
for (CFIndex i = 0; i < CFArrayGetCount(descriptors); i++) {
CTFontDescriptorRef descriptor = (CTFontDescriptorRef)CFArrayGetValueAtIndex(descriptors, i);
RAII_CoreFoundation(CFDictionaryRef, variation, CTFontDescriptorCopyAttribute(descriptor, kCTFontVariationAttribute));
if (!variation) continue;
RAII_CoreFoundation(CFStringRef, style, CTFontDescriptorCopyAttribute(descriptor, kCTFontStyleNameAttribute));
RAII_CoreFoundation(CFStringRef, psname, CTFontDescriptorCopyAttribute(descriptor, kCTFontNameAttribute));
RAII_PyObject(axis_values, PyDict_New());
if (!axis_values) return NULL;
CFDictionaryApplyFunction(variation, convert_variation_to_python, axis_values);
PyObject *ns = Py_BuildValue("{sO sN sN}", "axis_values", axis_values, "name", convert_cfstring(style, false), "psname", convert_cfstring(psname, false));
if (!ns) return NULL;
PyTuple_SET_ITEM(named_styles, actual_num, ns); actual_num++;
}
_PyTuple_Resize(&named_styles, actual_num);
return Py_BuildValue("{sO sO}", "axes", axes, "named_styles", named_styles); //, "named_styles", named_styles);
}
static PyMethodDef methods[] = {
METHODB(display_name, METH_NOARGS),
METHODB(get_variable_data, METH_NOARGS),
{NULL} /* Sentinel */
};
@@ -813,6 +900,7 @@ static PyMemberDef members[] = {
PyTypeObject CTFace_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "fast_data_types.CTFace",
.tp_new = new,
.tp_basicsize = sizeof(CTFace),
.tp_dealloc = (destructor)dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,

View File

@@ -440,6 +440,11 @@ class CoreTextFont(TypedDict):
traits: int
class CTFace:
def __init__(self, descriptor: Optional[CoreTextFont] = None, path: str = ''): ...
def get_variable_data(self) -> VariableData: ...
def coretext_all_fonts() -> Tuple[CoreTextFont, ...]:
pass

View File

@@ -1,5 +1,5 @@
from enum import Enum, IntEnum, auto
from typing import NamedTuple, Optional, Tuple, TypedDict, Union
from typing import Dict, NamedTuple, Optional, Tuple, TypedDict, Union
from kitty.typing import CoreTextFont, FontConfigPattern
@@ -19,11 +19,11 @@ class VariableAxis(TypedDict):
default: float
hidden: bool
tag: str
strid: Optional[str]
strid: str # Can be empty string when not present
class NamedStyle(TypedDict):
axis_values: Tuple[float, ...]
axis_values: Dict[str, float]
name: Optional[str]
psname: Optional[str]

View File

@@ -4,7 +4,7 @@
import re
from typing import Dict, Generator, Iterable, List, Optional, Tuple
from kitty.fast_data_types import coretext_all_fonts
from kitty.fast_data_types import CTFace, coretext_all_fonts
from kitty.fonts import FontFeature, VariableData
from kitty.options.types import Options
from kitty.typing import CoreTextFont
@@ -120,4 +120,4 @@ def font_for_family(family: str) -> Tuple[CoreTextFont, bool, bool]:
def get_variable_data_for_descriptor(f: ListedFont) -> VariableData:
d = f['descriptor']
assert d['descriptor_type'] == 'core_text'
return d['variable_data']
return CTFace(descriptor=d).get_variable_data()

View File

@@ -76,8 +76,8 @@ def show_variable(f: ListedFont, psnames: bool) -> None:
if psnames:
name += f' ({ns["psname"] or ""})'
axes = []
for axis, val in zip(vd['axes'], ns['axis_values']):
axes.append(f'{axis["tag"]}={val:g}')
for axis_tag, val in ns['axis_values'].items():
axes.append(f'{axis_tag}={val:g}')
p = name + ': ' + ' '.join(axes)
print(indented(p, level=3))

View File

@@ -787,15 +787,26 @@ static inline void cleanup_ftmm(FT_MM_Var **p) { if (*p) FT_Done_MM_Var(library,
#define RAII_FTMMVar(name) __attribute__((cleanup(cleanup_ftmm))) FT_MM_Var *name = NULL
static const char*
tag_to_string(uint32_t tag, uint8_t bytes[5]) {
bytes[0] = (tag >> 24) & 0xff;
bytes[1] = (tag >> 16) & 0xff;
bytes[2] = (tag >> 8) & 0xff;
bytes[3] = (tag) & 0xff;
bytes[4] = 0;
return (const char*)bytes;
}
static PyObject*
convert_named_style_to_python(Face *face, const FT_Var_Named_Style *src, unsigned num_of_axes) {
RAII_PyObject(axis_values, PyTuple_New(num_of_axes));
convert_named_style_to_python(Face *face, const FT_Var_Named_Style *src, FT_Var_Axis *axes, unsigned num_of_axes) {
RAII_PyObject(axis_values, PyDict_New());
if (!axis_values) return NULL;
uint8_t tag_buf[5] = {0};
for (FT_UInt i = 0; i < num_of_axes; i++) {
double val = src->coords[i] / 65536.0;
PyObject *pval = PyFloat_FromDouble(val);
RAII_PyObject(pval, PyFloat_FromDouble(val));
if (!pval) return NULL;
PyTuple_SET_ITEM(axis_values, i, pval);
if (PyDict_SetItemString(axis_values, tag_to_string(axes[i].tag, tag_buf), pval) != 0) return NULL;
}
RAII_PyObject(name, _get_best_name(face, src->strid));
if (!name) PyErr_Clear();
@@ -804,25 +815,15 @@ convert_named_style_to_python(Face *face, const FT_Var_Named_Style *src, unsigne
return Py_BuildValue("{sO sO sO}", "axis_values", axis_values, "name", name ? name : Py_None, "psname", psname ? psname : Py_None);
}
static PyObject*
tag_to_string(uint32_t tag) {
if (!tag) return PyUnicode_FromString("");
unsigned char bytes[5] = {0};
bytes[0] = (tag >> 24) & 0xff;
bytes[1] = (tag >> 16) & 0xff;
bytes[2] = (tag >> 8) & 0xff;
bytes[3] = (tag) & 0xff;
return PyUnicode_DecodeASCII((const char*)bytes, 4, "replace");
}
static PyObject*
convert_axis_to_python(Face *face, const FT_Var_Axis *src, FT_UInt flags) {
RAII_PyObject(strid, _get_best_name(face, src->strid));
if (!strid) PyErr_Clear();
return Py_BuildValue("{sd sd sd sO ss sN sO}",
PyObject *strid = _get_best_name(face, src->strid);
if (!strid) { PyErr_Clear(); strid = PyUnicode_FromString(""); }
uint8_t tag_buf[5] = {0};
return Py_BuildValue("{sd sd sd sO ss ss sN}",
"minimum", src->minimum / 65536.0, "maximum", src->maximum / 65536.0, "default", src->def / 65536.0,
"hidden", flags & FT_VAR_AXIS_FLAG_HIDDEN ? Py_True : Py_False, "name", src->name, "tag", tag_to_string(src->tag),
"strid", strid ? strid : Py_None
"hidden", flags & FT_VAR_AXIS_FLAG_HIDDEN ? Py_True : Py_False, "name", src->name, "tag", tag_to_string(src->tag, tag_buf),
"strid", strid
);
}
@@ -835,7 +836,7 @@ get_variable_data(Face *self, PyObject *a UNUSED) {
RAII_PyObject(named_styles, PyTuple_New(mm->num_namedstyles));
if (!axes || !named_styles) return NULL;
for (FT_UInt i = 0; i < mm->num_namedstyles; i++) {
PyObject *s = convert_named_style_to_python(self, mm->namedstyle + i, mm->num_axis);
PyObject *s = convert_named_style_to_python(self, mm->namedstyle + i, mm->axis, mm->num_axis);
if (!s) return NULL;
PyTuple_SET_ITEM(named_styles, i, s);
}