diff --git a/kitty/core_text.m b/kitty/core_text.m index 524c6d9d8..b62851519 100644 --- a/kitty/core_text.m +++ b/kitty/core_text.m @@ -819,10 +819,17 @@ get_best_name(CTFace *self, PyObject *nameid) { static PyObject* get_variable_data(CTFace *self) { if (!ensure_name_table(self)) return NULL; + RAII_PyObject(output, PyDict_New()); + if (!output) return NULL; RAII_CoreFoundation(CFDataRef, cftable, CTFontCopyTable(self->ct_font, kCTFontTableFvar, kCTFontTableOptionNoOptions)); const uint8_t *table = cftable ? CFDataGetBytePtr(cftable) : NULL; size_t table_len = cftable ? CFDataGetLength(cftable) : 0; - return read_fvar_font_table(table, table_len, self->name_lookup_table); + if (!read_fvar_font_table(table, table_len, self->name_lookup_table, output)) return NULL; + RAII_CoreFoundation(CFDataRef, stable, CTFontCopyTable(self->ct_font, kCTFontTableSTAT, kCTFontTableOptionNoOptions)); + table = stable ? CFDataGetBytePtr(stable) : NULL; + table_len = stable ? CFDataGetLength(stable) : 0; + if (!read_STAT_font_table(table, table_len, self->name_lookup_table, output)) return NULL; + Py_INCREF(output); return output; } static PyObject* diff --git a/kitty/font-names.c b/kitty/font-names.c index 04883d3a3..67264c4ea 100644 --- a/kitty/font-names.c +++ b/kitty/font-names.c @@ -119,13 +119,106 @@ read_name_font_table(const uint8_t *table, size_t table_len) { } -PyObject* -read_fvar_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table) { - RAII_PyObject(named_styles, PyTuple_New(0)); if (!named_styles) return NULL; - RAII_PyObject(axes, PyTuple_New(0)); if (!axes) return NULL; -#define retval Py_BuildValue("{sO sO sN}", "axes", axes, "named_styles", named_styles, "variations_postscript_name_prefix", get_best_name(name_lookup_table, 25)) +bool +read_STAT_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table, PyObject *output) { + RAII_PyObject(design_axes, PyTuple_New(0)); + RAII_PyObject(multi_axis_styles, PyTuple_New(0)); + if (!design_axes || !multi_axis_styles) return false; + if (table_len < 20) goto ok; + + const uint16_t *p = (uint16_t*)table; + uint16_t major_version = next, minor_version = next, size_of_design_axis_entry = next, count_of_design_axis_entries = next; + const uint32_t *p32 = (uint32_t*)p; + uint32_t offset_to_start_of_design_axes_entries = byteswap32(p32++); + p = (uint16_t*)p32; + uint16_t count_of_axis_value_entries = next; + p32 = (uint32_t*)p; + uint32_t offset_to_start_of_axis_value_entries = byteswap32(p32++); + p = (uint16_t*)p32; + uint16_t elided_fallback_name_id = next; + if (major_version == 1 && minor_version < 1) elided_fallback_name_id = 0; + if (PyDict_SetItemString(output, "elided_fallback_name", elided_fallback_name_id ? get_best_name(name_lookup_table, elided_fallback_name_id) : PyUnicode_FromString("")) != 0) return false; + const uint8_t *table_limit = table + table_len; + size_t count = 0; + if (_PyTuple_Resize(&design_axes, count_of_design_axis_entries) == -1) return false; + for ( + const uint8_t *pos = table + offset_to_start_of_design_axes_entries; + pos + size_of_design_axis_entry <= table_limit && count < count_of_design_axis_entries; + pos += size_of_design_axis_entry, count++ + ) { + p = (uint16_t*)(pos + 4); + uint16_t name_id = next, ordering = next; + PyObject *rec = Py_BuildValue("{ss# sN sH sN}", "tag", (char*)pos, 4, "name", get_best_name(name_lookup_table, name_id), "ordering", ordering, "values", PyList_New(0)); + if (!rec) return false; + PyTuple_SET_ITEM(design_axes, count, rec); + } + if (_PyTuple_Resize(&design_axes, count) == -1) return false; + count = 0; + const uint8_t *start_of_axis_values_offsets_array = table + offset_to_start_of_axis_value_entries; + Py_ssize_t i = 0; + if (_PyTuple_Resize(&multi_axis_styles, count_of_axis_value_entries) == -1) return false; + for ( + const uint8_t *pos = start_of_axis_values_offsets_array; + pos + 2 <= table_limit && count < count_of_axis_value_entries; + pos += 2, count++ + ) { + p = (uint16_t*)pos; + uint16_t offset = next; + const uint8_t *start_of_axis_values_table = start_of_axis_values_offsets_array + offset; + if (start_of_axis_values_table + 12 > table_limit) continue; + p = (uint16_t*)(start_of_axis_values_table); + uint16_t format = next, axis_index = next, flags = next, value_name_id = next; + p32 = (uint32_t*)p; +#define app(fmt, ...) { \ + RAII_PyObject(v, Py_BuildValue("{sH sH sN " fmt "}", \ + "format", format, "flags", flags, "name", get_best_name(name_lookup_table, value_name_id), __VA_ARGS__)); \ + if (!v) return false; \ + PyObject *l = PyDict_GetItemString(PyTuple_GET_ITEM(design_axes, axis_index), "values"); \ + if (l && PyList_Append(l, v) != 0) return false; \ +} + switch(format) { + case 1: if (p32 + 1 <= (uint32_t*)table_limit && axis_index < PyTuple_GET_SIZE(design_axes)) { + const double value = next32; app("sd", "value", value); + } break; + case 2: if (p32 + 3 <= (uint32_t*)table_limit && axis_index < PyTuple_GET_SIZE(design_axes)) { + const double value = next32, minimum = next32, maximum = next32; + app("sd sd sd", "value", value, "minimum", minimum, "maximum", maximum); + } break; + case 3: if (p32 + 2 <= (uint32_t*)table_limit && axis_index < PyTuple_GET_SIZE(design_axes)) { + const double value = next32, linked_value = next32; + app("sd sd", "value", value, "linked_value", linked_value); + } break; + case 4: if ((uint8_t*)(p) + 6 * axis_index <= table_limit) { + RAII_PyObject(values, PyTuple_New(axis_index)); + if (!values) return false; + for (uint16_t n = 0; n < axis_index; n++, p += 3) { + uint16_t actual_axis_index = next; + p32 = (uint32_t*)p; double value = next32; + PyObject *e = Py_BuildValue("{sH sd}", "design_index", actual_axis_index, "value", value); + if (!e) return false; + PyTuple_SET_ITEM(values, n, e); + } + PyObject *e = Py_BuildValue("{sH sN sO}", "flags", flags, + "name", get_best_name(name_lookup_table, value_name_id), "values", values); + if (!e) return false; + PyTuple_SET_ITEM(multi_axis_styles, i++, e); + } break; + } + } + if (_PyTuple_Resize(&multi_axis_styles, i) == -1) return false; +ok: + if (PyDict_SetItemString(output, "design_axes", design_axes) != 0) return false; + if (PyDict_SetItemString(output, "multi_axis_styles", multi_axis_styles) != 0) return false; + return true; +} + +bool +read_fvar_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table, PyObject *output) { + RAII_PyObject(named_styles, PyTuple_New(0)); if (!named_styles) return false; + RAII_PyObject(axes, PyTuple_New(0)); if (!axes) return false; + + if (!table || table_len < 14 * sizeof(uint16_t)) goto ok; - if (!table || table_len < 14 * sizeof(uint16_t)) return retval; const uint16_t *p = (uint16_t*)table; p += 2; const uint16_t offset_to_start_of_axis_array = next; next; @@ -181,8 +274,11 @@ read_fvar_font_table(const uint8_t *table, size_t table_len, PyObject *name_look PyTuple_SET_ITEM(named_styles, i, ns); } if (_PyTuple_Resize(&named_styles, i) == -1) return NULL; - return retval; -#undef retval +ok: + if (PyDict_SetItemString(output, "variations_postscript_name_prefix", get_best_name(name_lookup_table, 25)) != 0) return false; + if (PyDict_SetItemString(output, "axes", axes) != 0) return false; + if (PyDict_SetItemString(output, "named_styles", named_styles) != 0) return false; + return true; } #undef next32 #undef next diff --git a/kitty/fonts.h b/kitty/fonts.h index 35b54569a..fec2c84c9 100644 --- a/kitty/fonts.h +++ b/kitty/fonts.h @@ -48,8 +48,10 @@ PyObject* get_best_name_from_name_table(PyObject *table, PyObject *name_id); PyObject* read_name_font_table(const uint8_t *table, size_t table_len); -PyObject* -read_fvar_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table); +bool +read_fvar_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table, PyObject *output); +bool +read_STAT_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table, PyObject *output); static inline void right_shift_canvas(pixel *canvas, size_t width, size_t height, size_t amt) { diff --git a/kitty/fonts/__init__.py b/kitty/fonts/__init__.py index 9a4dc9e79..5974c6e1c 100644 --- a/kitty/fonts/__init__.py +++ b/kitty/fonts/__init__.py @@ -1,5 +1,5 @@ from enum import Enum, IntEnum, auto -from typing import Callable, Dict, NamedTuple, Tuple, TypedDict, Union +from typing import Callable, Dict, List, Literal, NamedTuple, Tuple, TypedDict, Union from kitty.typing import CoreTextFont, FontConfigPattern @@ -28,10 +28,58 @@ class NamedStyle(TypedDict): psname: str # can be empty string when not present +class DesignValue1(TypedDict): + format: Literal[1] + flags: int + name: str + value: float + + +class DesignValue2(TypedDict): + format: Literal[2] + flags: int + name: str + value: float + minimum: float + maximum: float + + +class DesignValue3(TypedDict): + format: Literal[3] + flags: int + name: str + value: float + linked_value: float + + +DesignValue = Union[DesignValue1, DesignValue2, DesignValue3] + + +class DesignAxis(TypedDict): + name: str + ordering: int + tag: str + values: List[DesignValue] + + +class AxisValue(TypedDict): + design_index: int + value: float + + +class MultiAxisStyle(TypedDict): + flags: int + name: str + values: Tuple[AxisValue, ...] + + class VariableData(TypedDict): axes: Tuple[VariableAxis, ...] named_styles: Tuple[NamedStyle, ...] variations_postscript_name_prefix: str + elided_fallback_name: str + design_axes: Tuple[DesignAxis, ...] + multi_axis_styles: Tuple[MultiAxisStyle, ...] class FontFeature: diff --git a/kitty/freetype.c b/kitty/freetype.c index 353155790..7bc14c530 100644 --- a/kitty/freetype.c +++ b/kitty/freetype.c @@ -804,27 +804,45 @@ convert_axis_to_python(Face *face, const FT_Var_Axis *src, FT_UInt flags) { 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 (!ensure_name_table(self)) return NULL; + RAII_PyObject(output, PyDict_New()); if (!output) return NULL; + RAII_PyObject(axes, PyTuple_New(0)); + RAII_PyObject(named_styles, PyTuple_New(0)); 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->axis, mm->num_axis); - if (!s) return NULL; - PyTuple_SET_ITEM(named_styles, i, s); - } + FT_Error err; + FT_ULong length = 0; + if ((err = FT_Load_Sfnt_Table(self->face, FT_MAKE_TAG('S', 'T', 'A', 'T'), 0, NULL, &length)) == 0) { + RAII_ALLOC(uint8_t, table, malloc(length)); + if (!table) return PyErr_NoMemory(); + if ((err = FT_Load_Sfnt_Table(self->face, FT_MAKE_TAG('S', 'T', 'A', 'T'), 0, table, &length))) { + set_freetype_error("Failed to load the STAT table from font with error:", err); return NULL; + } + if (!read_STAT_font_table(table, length, self->name_lookup_table, output)) return NULL; + } else if (!read_STAT_font_table(NULL, 0, self->name_lookup_table, output)) return NULL; + if (self->is_variable) { + RAII_FTMMVar(mm); + if ((err = FT_Get_MM_Var(self->face, &mm))) { set_freetype_error("Failed to get variable axis data from font with error:", err); return NULL; } + if (_PyTuple_Resize(&axes, mm->num_axis) == -1) return NULL; + if (_PyTuple_Resize(&named_styles, mm->num_namedstyles) == -1) return NULL; + for (FT_UInt i = 0; i < mm->num_namedstyles; i++) { + 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); + } - 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); + 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); + if (!s) return NULL; + PyTuple_SET_ITEM(axes, i, s); + } } - return Py_BuildValue("{sO sO sN}", "axes", axes, "named_styles", named_styles, "variations_postscript_name_prefix", _get_best_name(self, 25)); + if (PyDict_SetItemString(output, "variations_postscript_name_prefix", _get_best_name(self, 25)) != 0) return NULL; + if (PyDict_SetItemString(output, "axes", axes) != 0) return NULL; + if (PyDict_SetItemString(output, "named_styles", named_styles) != 0) return NULL; + Py_INCREF(output); return output; } StringCanvas