From 57770f2f5598cf243dd78b31ac45d5547e6d6032 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 19 Apr 2024 21:40:41 +0530 Subject: [PATCH] Code to get variable data from freetype to python --- kitty/fast_data_types.pyi | 21 ++++++ kitty/fonts/fontconfig.py | 2 +- kitty/freetype.c | 151 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 173 insertions(+), 1 deletion(-) diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 87cc6483d..74dcaf084 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -416,8 +416,29 @@ def fc_match_postscript_name( pass +class VariableAxis(TypedDict): + minimum: float + maximum: float + default: float + hidden: bool + tag: str + strid: Optional[str] + + +class NamedStyle(TypedDict): + axis_values: Tuple[float, ...] + name: Optional[str] + psname: Optional[str] + + +class VariableData(TypedDict): + axes: Tuple[VariableAxis, ...] + named_styles: Tuple[NamedStyle, ...] + + class Face: def __init__(self, descriptor: Optional[FontConfigPattern] = None, path: str = '', index: int = 0): ... + def get_variable_data(self) -> VariableData: ... class CoreTextFont(TypedDict): diff --git a/kitty/fonts/fontconfig.py b/kitty/fonts/fontconfig.py index d84336df9..1bbb938ac 100644 --- a/kitty/fonts/fontconfig.py +++ b/kitty/fonts/fontconfig.py @@ -178,4 +178,4 @@ def font_for_family(family: str) -> Tuple[FontConfigPattern, bool, bool]: def get_variable_data_for_descriptor(fd: FontConfigPattern) -> None: f = Face(descriptor=fd) - print(f) + print(f.get_variable_axes()) diff --git a/kitty/freetype.c b/kitty/freetype.c index 7d49b43e5..1947937df 100644 --- a/kitty/freetype.c +++ b/kitty/freetype.c @@ -19,6 +19,8 @@ #include FT_BITMAP_H #include FT_TRUETYPE_TABLES_H +#include FT_MULTIPLE_MASTERS_H +#include FT_SFNT_NAMES_H typedef union FaceIndex { struct { @@ -46,6 +48,7 @@ typedef struct { void *extra_data; free_extra_data_func free_extra_data; float apple_leading; + PyObject *name_lookup_table; } Face; PyTypeObject Face_Type; @@ -307,6 +310,7 @@ dealloc(Face* self) { if (self->face) FT_Done_Face(self->face); if (self->extra_data && self->free_extra_data) self->free_extra_data(self->extra_data); Py_CLEAR(self->path); + Py_CLEAR(self->name_lookup_table); Py_TYPE(self)->tp_free((PyObject*)self); } @@ -696,6 +700,151 @@ extra_data(PyObject *self, PyObject *a UNUSED) { return PyLong_FromVoidPtr(((Face*)self)->extra_data); } +// NAME table {{{ +static PyObject* +decode_name_record(PyObject *namerec) { +#define d(x) PyLong_AsUnsignedLong(PyTuple_GET_ITEM(namerec, x)) + unsigned long platform_id = d(0), encoding_id = d(1), language_id = d(2); +#undef d + const char *encoding = "unicode_escape"; + if ((platform_id == 3 && encoding_id == 1) || platform_id == 0) encoding = "utf-16-be"; + else if (platform_id == 1 && encoding_id == 0 && language_id == 0) encoding = "mac-roman"; + PyObject *b = PyTuple_GET_ITEM(namerec, 3); + return PyUnicode_Decode(PyBytes_AS_STRING(b), PyBytes_GET_SIZE(b), encoding, "replace"); +} + +static bool +ensure_name_table(Face *self) { + if (self->name_lookup_table) return true; + RAII_PyObject(ans, PyDict_New()); + if (!ans) return false; + FT_SfntName temp; + for (FT_UInt i = 0; i < FT_Get_Sfnt_Name_Count(self->face); i++) { + FT_Error err = FT_Get_Sfnt_Name(self->face, i, &temp); + if (err != 0) continue; + RAII_PyObject(key, PyLong_FromUnsignedLong((unsigned long)temp.name_id)); + if (!key) return false; + RAII_PyObject(list, PyDict_GetItem(ans, key)); + if (list == NULL) { + list = PyList_New(0); + if (!list) return false; + if (PyDict_SetItem(ans, key, list) != 0) return false; + } else Py_INCREF(list); + RAII_PyObject(value, Py_BuildValue("(H H H y#)", temp.platform_id, temp.encoding_id, temp.language_id, temp.string, (Py_ssize_t)temp.string_len)); + if (!value) return false; + if (PyList_Append(list, value) != 0) return false; + } + self->name_lookup_table = ans; Py_INCREF(ans); + return true; +} + +static bool +namerec_matches(PyObject *namerec, unsigned platform_id, unsigned encoding_id, unsigned language_id) { +#define d(x) PyLong_AsUnsignedLong(PyTuple_GET_ITEM(namerec, x)) + return d(0) == platform_id && d(1) == encoding_id && d(2) == language_id; +#undef d +} + +static PyObject* +find_matching_namerec(PyObject *namerecs, unsigned platform_id, unsigned encoding_id, unsigned language_id) { + for (Py_ssize_t i = 0; i < PyList_GET_SIZE(namerecs); i++) { + PyObject *namerec = PyList_GET_ITEM(namerecs, i); + if (namerec_matches(namerec, platform_id, encoding_id, language_id)) return decode_name_record(namerec); + } + return NULL; +} + +static PyObject* +get_best_name(Face *self, PyObject *nameid) { + if (!ensure_name_table(self)) return NULL; + PyObject *namerecs = PyDict_GetItem(self->name_lookup_table, nameid); + if (namerecs == NULL) return PyUnicode_FromString(""); + if (PyList_GET_SIZE(namerecs) == 1) return decode_name_record(PyList_GET_ITEM(namerecs, 0)); +#define d(...) { PyObject *ans = find_matching_namerec(namerecs, __VA_ARGS__); if (ans != NULL || PyErr_Occurred()) return ans; } + d(3, 1, 1033); // Microsoft/Windows/US English + d(1, 0, 0); // Mac/Roman/English + d(0, 6, 0); // Unicode/SMP/* + d(0, 4, 0); // Unicode/SMP/* + d(0, 3, 0); // Unicode/BMP/* + d(0, 2, 0); // Unicode/10646-BMP/* + d(0, 1, 0); // Unicode/1.1/* +#undef d + return PyUnicode_FromString(""); +} + +static PyObject* +_get_best_name(Face *self, unsigned long nameid) { + RAII_PyObject(key, PyLong_FromUnsignedLong(nameid)); + return key ? get_best_name(self, key) : NULL; +} +// }}} + +static inline void cleanup_ftmm(FT_MM_Var **p) { if (*p) FT_Done_MM_Var(library, *p); *p = NULL; } + +#define RAII_FTMMVar(name) __attribute__((cleanup(cleanup_ftmm))) FT_MM_Var *name = NULL + +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)); + if (!axis_values) return NULL; + for (FT_UInt i = 0; i < num_of_axes; i++) { + double val = src->coords[i] / 65536.0; + PyObject *pval = PyFloat_FromDouble(val); + if (!pval) return NULL; + PyTuple_SET_ITEM(axis_values, i, pval); + } + RAII_PyObject(name, _get_best_name(face, src->strid)); + if (!name) PyErr_Clear(); + RAII_PyObject(psname, src->psid == 0xffff ? NULL : _get_best_name(face, src->psid)); + if (!psname) PyErr_Clear(); + 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) { + 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}", + "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 + ); +} + +static PyObject* +get_variable_data(Face *self, PyObject *a UNUSED) { + RAII_FTMMVar(mm); + FT_Error err = FT_Get_MM_Var(self->face, &mm); + if (err) { set_freetype_error("Failed to get variable axis data from font with error:", err); return NULL; } + RAII_PyObject(axes, PyTuple_New(mm->num_axis)); + 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); + if (!s) return NULL; + PyTuple_SET_ITEM(named_styles, i, s); + } + + for (FT_UInt i = 0; i < mm->num_axis; i++) { + FT_UInt flags; + FT_Get_Var_Axis_Flags(mm, i, &flags); + PyObject *s = convert_axis_to_python(self, mm->axis + i, flags); + + if (!s) return NULL; + PyTuple_SET_ITEM(axes, i, s); + } + return Py_BuildValue("{sO sO}", "axes", axes, "named_styles", named_styles); +} StringCanvas render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) { @@ -759,6 +908,8 @@ static PyMemberDef members[] = { static PyMethodDef methods[] = { METHODB(display_name, METH_NOARGS), METHODB(extra_data, METH_NOARGS), + METHODB(get_variable_data, METH_NOARGS), + METHODB(get_best_name, METH_O), {NULL} /* Sentinel */ };