diff --git a/kittens/choose_fonts/backend.py b/kittens/choose_fonts/backend.py index 867247a81..f1b77f554 100644 --- a/kittens/choose_fonts/backend.py +++ b/kittens/choose_fonts/backend.py @@ -6,7 +6,7 @@ import os import string import sys import tempfile -from typing import Any, Dict, Literal, Tuple, TypedDict +from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Tuple, TypedDict from kitty.cli import create_default_opts from kitty.conf.utils import to_color @@ -22,12 +22,16 @@ from kitty.fonts.common import ( is_variable, spec_for_face, ) +from kitty.fonts.features import Type, known_features from kitty.fonts.list import create_family_groups from kitty.fonts.render import display_bitmap from kitty.options.types import Options from kitty.options.utils import parse_font_spec +from kitty.typing import NotRequired from kitty.utils import screen_size_function +if TYPE_CHECKING: + from kitty.fast_data_types import FeatureData def setup_debug_print() -> bool: if 'KITTY_STDIO_FORWARDED' in os.environ: @@ -86,6 +90,35 @@ RenderedSampleTransmit = Dict[str, Any] SAMPLE_TEXT = string.ascii_lowercase + ' ' + string.digits + ' ' + string.ascii_uppercase + ' ' + string.punctuation +class FD(TypedDict): + is_index: bool + name: NotRequired[str] + tooltip: NotRequired[str] + sample: NotRequired[str] + params: NotRequired[Tuple[str, ...]] + + + +def get_features(features: Dict[str, Optional['FeatureData']]) -> Dict[str, FD]: + ans = {} + for tag, data in features.items(): + kf = known_features.get(tag) + if kf is None or kf.type is Type.hidden: + continue + fd: FD = {'is_index': kf.type is Type.index} + ans[tag] = fd + if data is not None: + if n := data.get('name'): + fd['name'] = n + if n := data.get('tooltip'): + fd['tooltip'] = n + if n := data.get('sample'): + fd['sample'] = n + if p := data.get('params'): + fd['params'] = p + return ans + + def render_face_sample(font: Descriptor, opts: Options, dpi_x: float, dpi_y: float, width: int, height: int) -> RenderedSample: face = face_from_descriptor(font) face.set_size(opts.font_size, dpi_x, dpi_y) @@ -93,6 +126,7 @@ def render_face_sample(font: Descriptor, opts: Options, dpi_x: float, dpi_y: flo 'variable_data': get_variable_data_for_face(face), 'style': font['style'], 'psname': face.postscript_name(), + 'features': get_features(face.get_features()), } if is_variable(font): ns = get_named_style(face) diff --git a/kittens/choose_fonts/types.go b/kittens/choose_fonts/types.go index 666dc4453..abd7ce71a 100644 --- a/kittens/choose_fonts/types.go +++ b/kittens/choose_fonts/types.go @@ -87,13 +87,21 @@ type ListResult struct { Resolved_faces ResolvedFaces `json:"resolved_faces"` } +type FeatureData struct { + Name string `json:"name"` + Tooltip string `json:"tooltip"` + Sample string `json:"sample"` + Params []string `json:"params"` +} + type RenderedSampleTransmit struct { - Path string `json:"path"` - Variable_data VariableData `json:"variable_data"` - Style string `json:"style"` - Psname string `json:"psname"` - Variable_named_style NamedStyle `json:"variable_named_style"` - Variable_axis_map map[string]float64 `json:"variable_axis_map"` + Path string `json:"path"` + Variable_data VariableData `json:"variable_data"` + Style string `json:"style"` + Psname string `json:"psname"` + Features map[string]FeatureData `json:"features"` + Variable_named_style NamedStyle `json:"variable_named_style"` + Variable_axis_map map[string]float64 `json:"variable_axis_map"` } func (self RenderedSampleTransmit) default_axis_values() (ans map[string]float64) { diff --git a/kitty/core_text.m b/kitty/core_text.m index 43921f66c..8d17c2338 100644 --- a/kitty/core_text.m +++ b/kitty/core_text.m @@ -948,15 +948,16 @@ get_variation(CTFace *self) { static PyObject* get_features(CTFace *self, PyObject *a UNUSED) { - RAII_PyObject(output, PyFrozenSet_New(NULL)); if (!output) return NULL; + if (!ensure_name_table(self)) return NULL; + RAII_PyObject(output, PyDict_New()); 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; + if (!read_features_from_font_table(table, table_len, self->name_lookup_table, 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; + if (!read_features_from_font_table(table, table_len, self->name_lookup_table, output)) return NULL; Py_INCREF(output); return output; } diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 6c2ac5c21..a7b890cdc 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, FrozenSet +from typing import Any, Callable, Dict, Iterator, List, Literal, NewType, Optional, Tuple, TypedDict, Union, overload from kitty.boss import Boss from kitty.fonts import FontFeature, VariableData @@ -424,6 +424,13 @@ def fc_match_postscript_name( pass +class FeatureData(TypedDict): + name: NotRequired[str] + tooltip: NotRequired[str] + sample: NotRequired[str] + params: NotRequired[Tuple[str, ...]] + + class Face: path: Optional[str] def __init__(self, descriptor: Optional[FontConfigPattern] = None, path: str = '', index: int = 0): ... @@ -433,7 +440,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]: ... + def get_features(self) -> Dict[str, Optional[FeatureData]]: ... class CoreTextFont(TypedDict): @@ -468,7 +475,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 get_features(self) -> Dict[str, Optional[FeatureData]]: ... def coretext_all_fonts(monospaced_only: bool) -> Tuple[CoreTextFont, ...]: diff --git a/kitty/font-names.c b/kitty/font-names.c index a2874da11..1e2220e84 100644 --- a/kitty/font-names.c +++ b/kitty/font-names.c @@ -119,8 +119,53 @@ read_name_font_table(const uint8_t *table, size_t table_len) { } +static bool is_digit(char x) { return '0' <= x && x <= '9'; } + +static PyObject* +read_cv_feature_table(const uint8_t *table, const uint8_t *limit, PyObject *name_lookup_table) { + RAII_PyObject(ans, PyDict_New()); if (!ans) return NULL; + if (limit - table >= 12) { + uint16_t *p = (uint16_t*)(table + 2); + uint16_t name_id = next, tooltip_id = next, sample_id = next, num_params = next, first_value_id = next; + if (name_id) { + RAII_PyObject(name, get_best_name(name_lookup_table, name_id)); if (!name) return NULL; + if (PyDict_SetItemString(ans, "name", name) != 0) return NULL; + } + if (tooltip_id) { + RAII_PyObject(tooltip, get_best_name(name_lookup_table, tooltip_id)); if (!tooltip) return NULL; + if (PyDict_SetItemString(ans, "tooltip", tooltip) != 0) return NULL; + } + if (sample_id) { + RAII_PyObject(sample, get_best_name(name_lookup_table, sample_id)); if (!sample) return NULL; + if (PyDict_SetItemString(ans, "sample", sample) != 0) return NULL; + } + if (num_params && first_value_id) { + RAII_PyObject(params, PyTuple_New(num_params)); if (!params) return NULL; + for (uint16_t i = 0; i < num_params; i++) { + PyObject *pval = get_best_name(name_lookup_table, first_value_id + i); if (!pval) return NULL; + PyTuple_SET_ITEM(params, i, pval); + } + if (PyDict_SetItemString(ans, "params", params) != 0) return NULL; + } + } + Py_INCREF(ans); return ans; +} + +static PyObject* +read_ss_feature_table(const uint8_t *table, const uint8_t *limit, PyObject *name_lookup_table) { + RAII_PyObject(ans, PyDict_New()); if (!ans) return NULL; + if (limit - table < 4) { Py_INCREF(ans); return ans; } + uint16_t *p = (uint16_t*)(table + 2); + uint16_t name_id = next; + if (name_id) { + RAII_PyObject(name, get_best_name(name_lookup_table, name_id)); if (!name) return NULL; + if (PyDict_SetItemString(ans, "name", name) != 0) return NULL; + } + Py_INCREF(ans); return ans; +} + bool -read_features_from_font_table(const uint8_t *table, size_t table_len, PyObject *output) { +read_features_from_font_table(const uint8_t *table, size_t table_len, PyObject *name_lookup_table, PyObject *output) { if (table_len < 20) return true; const uint16_t *p = (uint16_t*)table; const uint8_t *limit = table + table_len; @@ -132,11 +177,29 @@ read_features_from_font_table(const uint8_t *table, size_t table_len, PyObject * 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++) { + for (uint16_t i = 0; i < feature_count && pos + 6 <= 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; + if (PyDict_Contains(output, tag) == 1) continue; + if (PyDict_SetItem(output, tag, Py_None) != 0) return false; + p = (uint16_t*)(pos + 4); uint16_t offset_to_feature_table = next; + const uint8_t *feature_table = feature_list_table + offset_to_feature_table; + if (feature_table + 2 > limit) continue; + p = (uint16_t*)(feature_table); uint16_t offset_to_feature_params_table = next; + if (tag_buf[0] == 'c' && tag_buf[1] == 'v' && is_digit(tag_buf[2]) && is_digit(tag_buf[3])) { + if (offset_to_feature_params_table) { + RAII_PyObject(cv, read_cv_feature_table(feature_table + offset_to_feature_params_table, limit, name_lookup_table)); + if (!cv) return false; + if (PyDict_SetItem(output, tag, cv) != 0) return false; + } + } else if (tag_buf[0] == 's' && tag_buf[1] == 's' && '0' <= tag_buf[2] && tag_buf[2] <= '2' && is_digit(tag_buf[3])) { + if (offset_to_feature_params_table) { + RAII_PyObject(ss, read_ss_feature_table(feature_table + offset_to_feature_params_table, limit, name_lookup_table)); + if (!ss) return false; + if (PyDict_SetItem(output, tag, ss) != 0) return false; + } + } } return true; } diff --git a/kitty/fonts.h b/kitty/fonts.h index 74328b86f..807ba0c2d 100644 --- a/kitty/fonts.h +++ b/kitty/fonts.h @@ -53,7 +53,7 @@ read_fvar_font_table(const uint8_t *table, size_t table_len, PyObject *name_look 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); +read_features_from_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/common.py b/kitty/fonts/common.py index 0132565c1..e59cefe12 100644 --- a/kitty/fonts/common.py +++ b/kitty/fonts/common.py @@ -474,7 +474,7 @@ def develop(family: str = '') -> None: f = face_from_descriptor(d) print(name, str(f)) features = f.get_features() - print(' Features :', ' '.join(sorted(features))) + print(' Features :', features) s('Medium :', ff['medium']) print() diff --git a/kitty/freetype.c b/kitty/freetype.c index e7ffa6338..7ef9d106c 100644 --- a/kitty/freetype.c +++ b/kitty/freetype.c @@ -846,14 +846,15 @@ 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 (!ensure_name_table(self)) return NULL; + RAII_PyObject(output, PyDict_New()); 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; + if (!read_features_from_font_table(table, length, self->name_lookup_table, output)) return NULL; } length = 0; if ((err = FT_Load_Sfnt_Table(self->face, FT_MAKE_TAG('G', 'P', 'O', 'S'), 0, NULL, &length)) == 0) { @@ -862,7 +863,7 @@ get_features(Face *self, PyObject *a UNUSED) { 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; + if (!read_features_from_font_table(table, length, self->name_lookup_table, output)) return NULL; } Py_INCREF(output); return output; } diff --git a/kitty/typing.py b/kitty/typing.py index 83d7cd81e..1d35f044b 100644 --- a/kitty/typing.py +++ b/kitty/typing.py @@ -21,4 +21,4 @@ PowerlineStyle = str MatchType = str Protocol = object OptionsProtocol = object -NotRequired = object +NotRequired = Tuple