Get feature human readable names

This commit is contained in:
Kovid Goyal 2024-05-24 09:04:48 +05:30
parent a2a0f3cf41
commit 7e56920fa3
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
9 changed files with 136 additions and 22 deletions

View file

@ -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)

View file

@ -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) {

View file

@ -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;
}

View file

@ -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, ...]:

View file

@ -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;
}

View file

@ -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) {

View file

@ -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()

View file

@ -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;
}

View file

@ -21,4 +21,4 @@ PowerlineStyle = str
MatchType = str
Protocol = object
OptionsProtocol = object
NotRequired = object
NotRequired = Tuple