From 0f76d3e13bc7bcece1b815367b193b9745e92cad Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 23 May 2024 10:39:42 +0530 Subject: [PATCH] Code to read features from GSUB/GPOS tables --- kitty/core_text.m | 16 ++++++++++++++++ kitty/fast_data_types.pyi | 4 +++- kitty/font-names.c | 22 ++++++++++++++++++++++ kitty/fonts.h | 2 ++ kitty/fonts/common.py | 15 +++++++++------ kitty/freetype.c | 26 ++++++++++++++++++++++++++ 6 files changed, 78 insertions(+), 7 deletions(-) diff --git a/kitty/core_text.m b/kitty/core_text.m index fd92d1389..43921f66c 100644 --- a/kitty/core_text.m +++ b/kitty/core_text.m @@ -946,6 +946,21 @@ get_variation(CTFace *self) { return variation_to_python(src); } +static PyObject* +get_features(CTFace *self, PyObject *a UNUSED) { + RAII_PyObject(output, PyFrozenSet_New(NULL)); if (!output) return NULL; + RAII_CoreFoundation(CFDataRef, cftable, CTFontCopyTable(self->ct_font, kCTFontTableGSUB, kCTFontTableOptionNoOptions)); + const uint8_t *table = cftable ? CFDataGetBytePtr(cftable) : NULL; + size_t table_len = cftable ? CFDataGetLength(cftable) : 0; + if (!read_features_from_font_table(table, table_len, output)) return NULL; + RAII_CoreFoundation(CFDataRef, cfpostable, CTFontCopyTable(self->ct_font, kCTFontTableGPOS, kCTFontTableOptionNoOptions)); + table = cfpostable ? CFDataGetBytePtr(cfpostable) : NULL; + table_len = cfpostable ? CFDataGetLength(cfpostable) : 0; + if (!read_features_from_font_table(table, table_len, output)) return NULL; + Py_INCREF(output); return output; +} + + static PyObject* get_variable_data(CTFace *self) { if (!ensure_name_table(self)) return NULL; @@ -989,6 +1004,7 @@ static PyMethodDef methods[] = { METHODB(display_name, METH_NOARGS), METHODB(postscript_name, METH_NOARGS), METHODB(get_variable_data, METH_NOARGS), + METHODB(get_features, METH_NOARGS), METHODB(get_variation, METH_NOARGS), METHODB(identify_for_debug, METH_NOARGS), METHODB(set_size, METH_VARARGS), diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 4114efc70..6c2ac5c21 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1,6 +1,6 @@ import termios from ctypes import Array, c_ubyte -from typing import Any, Callable, Dict, Iterator, List, Literal, NewType, Optional, Tuple, TypedDict, Union, overload +from typing import Any, Callable, Dict, Iterator, List, Literal, NewType, Optional, Tuple, TypedDict, Union, overload, FrozenSet from kitty.boss import Boss from kitty.fonts import FontFeature, VariableData @@ -433,6 +433,7 @@ class Face: def set_size(self, sz_in_pts: float, dpi_x: float, dpi_y: float) -> None: ... def render_sample_text(self, text: str, width: int, height: int, fg_color: int = 0xffffff) -> bytes: ... def get_variation(self) -> Optional[Dict[str, float]]: ... + def get_features(self) -> FrozenSet[str]: ... class CoreTextFont(TypedDict): @@ -467,6 +468,7 @@ class CTFace: def set_size(self, sz_in_pts: float, dpi_x: float, dpi_y: float) -> None: ... def render_sample_text(self, text: str, width: int, height: int, fg_color: int = 0xffffff) -> bytes: ... def get_variation(self) -> Optional[Dict[str, float]]: ... + def get_features(self) -> FrozenSet[str]: ... def coretext_all_fonts(monospaced_only: bool) -> Tuple[CoreTextFont, ...]: diff --git a/kitty/font-names.c b/kitty/font-names.c index 67264c4ea..a2874da11 100644 --- a/kitty/font-names.c +++ b/kitty/font-names.c @@ -119,6 +119,28 @@ read_name_font_table(const uint8_t *table, size_t table_len) { } +bool +read_features_from_font_table(const uint8_t *table, size_t table_len, PyObject *output) { + if (table_len < 20) return true; + const uint16_t *p = (uint16_t*)table; + const uint8_t *limit = table + table_len; + uint16_t major_version = next, minor_version = next, script_list_offset = next, feature_list_offset = next; + (void)major_version; (void)minor_version; (void)script_list_offset; + const uint8_t *feature_list_table = table + feature_list_offset; + char tag_buf[5] = {0}; + if (feature_list_table + 2 >= limit) return true; + p = (uint16_t*)feature_list_table; + uint16_t feature_count = next; + const uint8_t *pos = (uint8_t*)p; + for (uint16_t i = 0; i < feature_count && pos + 4 < limit; pos += 6, i++) { + memcpy(tag_buf, pos, 4); + RAII_PyObject(tag, PyUnicode_FromString(tag_buf)); + if (!tag) return false; + if (PySet_Add(output, tag) != 0) return false; + } + return true; +} + 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)); diff --git a/kitty/fonts.h b/kitty/fonts.h index d0789c593..74328b86f 100644 --- a/kitty/fonts.h +++ b/kitty/fonts.h @@ -52,6 +52,8 @@ 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); +bool +read_features_from_font_table(const uint8_t *table, size_t table_len, PyObject *output); static inline void right_shift_canvas(pixel *canvas, size_t width, size_t height, size_t amt) { diff --git a/kitty/fonts/common.py b/kitty/fonts/common.py index bf8df42ed..0132565c1 100644 --- a/kitty/fonts/common.py +++ b/kitty/fonts/common.py @@ -470,16 +470,19 @@ def develop(family: str = '') -> None: opts = Options() opts.font_family = parse_font_spec(family) ff = get_font_files(opts) - def s(d: Descriptor) -> str: - return str(face_from_descriptor(d)) + def s(name: str, d: Descriptor) -> None: + f = face_from_descriptor(d) + print(name, str(f)) + features = f.get_features() + print(' Features :', ' '.join(sorted(features))) - print('Medium :', s(ff['medium'])) + s('Medium :', ff['medium']) print() - print('Bold :', s(ff['bold'])) + s('Bold :', ff['bold']) print() - print('Italic :', s(ff['italic'])) + s('Italic :', ff['italic']) print() - print('Bold-Italic:', s(ff['bi'])) + s('Bold-Italic:', ff['bi']) if __name__ == '__main__': diff --git a/kitty/freetype.c b/kitty/freetype.c index c34b74164..e7ffa6338 100644 --- a/kitty/freetype.c +++ b/kitty/freetype.c @@ -842,6 +842,31 @@ get_variation(Face *self, PyObject *a UNUSED) { Py_INCREF(ans); return ans; } +static PyObject* +get_features(Face *self, PyObject *a UNUSED) { + FT_Error err; + FT_ULong length = 0; + RAII_PyObject(output, PyFrozenSet_New(NULL)); if (!output) return NULL; + if ((err = FT_Load_Sfnt_Table(self->face, FT_MAKE_TAG('G', 'S', 'U', 'B'), 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('G', 'S', 'U', 'B'), 0, table, &length))) { + set_freetype_error("Failed to load the GSUB table from font with error:", err); return NULL; + } + if (!read_features_from_font_table(table, length, output)) return NULL; + } + length = 0; + if ((err = FT_Load_Sfnt_Table(self->face, FT_MAKE_TAG('G', 'P', 'O', 'S'), 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('G', 'P', 'O', 'S'), 0, table, &length))) { + set_freetype_error("Failed to load the GSUB table from font with error:", err); return NULL; + } + if (!read_features_from_font_table(table, length, output)) return NULL; + } + Py_INCREF(output); return output; +} + static PyObject* get_variable_data(Face *self, PyObject *a UNUSED) { if (!ensure_name_table(self)) return NULL; @@ -992,6 +1017,7 @@ static PyMethodDef methods[] = { METHODB(identify_for_debug, METH_NOARGS), METHODB(extra_data, METH_NOARGS), METHODB(get_variable_data, METH_NOARGS), + METHODB(get_features, METH_NOARGS), METHODB(get_variation, METH_NOARGS), METHODB(get_best_name, METH_O), METHODB(set_size, METH_VARARGS),