From 56f8fb9f7524aa3d87d80bdb40cb8ace5c587983 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 25 Apr 2024 15:13:48 +0530 Subject: [PATCH] Implement basic support for selecting font variations in fontconfig --- kitty/boss.py | 2 +- kitty/fast_data_types.pyi | 22 ++++- kitty/fontconfig.c | 25 ++++-- kitty/fonts.c | 22 +++-- kitty/fonts/__init__.py | 6 +- kitty/fonts/core_text.py | 18 +++-- kitty/fonts/fontconfig.py | 163 ++++++++++++++++++++++++++++---------- kitty/fonts/render.py | 41 +++------- kitty/freetype.c | 33 ++++++-- kitty/main.py | 6 +- 10 files changed, 233 insertions(+), 105 deletions(-) diff --git a/kitty/boss.py b/kitty/boss.py index dd724b387..dbe857896 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -2653,7 +2653,7 @@ def apply_new_options(self, opts: Options) -> None: # Update font data set_scale(opts.box_drawing_scale) from .fonts.render import set_font_family - set_font_family(opts, debug_font_matching=self.args.debug_font_fallback) + set_font_family(opts) for os_window_id, tm in self.os_window_map.items(): if tm is not None: os_window_font_size(os_window_id, opts.font_size, True) diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index bebe8120d..11c61372e 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, NotRequired, Optional, Tuple, TypedDict, Union, overload from kitty.boss import Boss from kitty.fonts import FontFeature, VariableData @@ -394,6 +394,10 @@ class FontConfigPattern(TypedDict): color: bool variable: bool + # The following two are used by C code to get a face from the pattern + named_style: NotRequired[int] + axes: NotRequired[List[float]] + def fc_list(spacing: int = -1, allow_bitmapped_fonts: bool = False, only_variable: bool = False) -> Tuple[FontConfigPattern, ...]: pass @@ -421,6 +425,7 @@ class Face: def __init__(self, descriptor: Optional[FontConfigPattern] = None, path: str = '', index: int = 0): ... def get_variable_data(self) -> VariableData: ... def identify_for_debug(self) -> str: ... + def display_name(self) -> str: ... class CoreTextFont(TypedDict): @@ -447,6 +452,7 @@ class CTFace: def __init__(self, descriptor: Optional[CoreTextFont] = None, path: str = ''): ... def get_variable_data(self) -> VariableData: ... def identify_for_debug(self) -> str: ... + def display_name(self) -> str: ... def coretext_all_fonts() -> Tuple[CoreTextFont, ...]: @@ -903,8 +909,18 @@ def concat_cells(cell_width: int, cell_height: int, is_32_bit: bool, cells: Tupl pass -def current_fonts() -> Dict[str, Any]: - pass +FontFace = Union[Face, CTFace] + +class CurrentFonts(TypedDict): + medium: FontFace + bold: FontFace + italic: FontFace + bi: FontFace + symbol: Tuple[FontFace, ...] + fallback: Tuple[FontFace, ...] + + +def current_fonts() -> CurrentFonts: ... def remove_window(os_window_id: int, tab_id: int, window_id: int) -> None: diff --git a/kitty/fontconfig.c b/kitty/fontconfig.c index bea6ede52..18bc6da92 100644 --- a/kitty/fontconfig.c +++ b/kitty/fontconfig.c @@ -395,25 +395,40 @@ end: PyObject* specialize_font_descriptor(PyObject *base_descriptor, FONTS_DATA_HANDLE fg) { ensure_initialized(); - PyObject *p = PyDict_GetItemString(base_descriptor, "path"), *ans = NULL; + PyObject *p = PyDict_GetItemString(base_descriptor, "path"); PyObject *idx = PyDict_GetItemString(base_descriptor, "index"); if (p == NULL) { PyErr_SetString(PyExc_ValueError, "Base descriptor has no path"); return NULL; } if (idx == NULL) { PyErr_SetString(PyExc_ValueError, "Base descriptor has no index"); return NULL; } + unsigned long face_idx = PyLong_AsUnsignedLong(idx); + if (PyErr_Occurred()) return NULL; + FcPattern *pat = FcPatternCreate(); if (pat == NULL) return PyErr_NoMemory(); - long face_idx = MAX(0, PyLong_AsLong(idx)); + RAII_PyObject(ans, NULL); AP(FcPatternAddString, FC_FILE, (const FcChar8*)PyUnicode_AsUTF8(p), "path"); AP(FcPatternAddInteger, FC_INDEX, face_idx, "index"); AP(FcPatternAddDouble, FC_SIZE, fg->font_sz_in_pts, "size"); AP(FcPatternAddDouble, FC_DPI, (fg->logical_dpi_x + fg->logical_dpi_y) / 2.0, "dpi"); ans = _fc_match(pat); + FcPatternDestroy(pat); pat = NULL; + if (face_idx > 0) { // For some reason FcFontMatch sets the index to zero, so manually restore it. - PyDict_SetItemString(ans, "index", idx); + if (PyDict_SetItemString(ans, "index", idx) != 0) return NULL; } -end: - if (pat != NULL) FcPatternDestroy(pat); + PyObject *named_style = PyDict_GetItemString(base_descriptor, "named_style"); + if (named_style) { + if (PyDict_SetItemString(ans, "named_style", named_style) != 0) return NULL; + } + PyObject *axes = PyDict_GetItemString(base_descriptor, "axes"); + if (axes) { + if (PyDict_SetItemString(ans, "axes", axes) != 0) return NULL; + } + Py_INCREF(ans); return ans; +end: + if (pat) FcPatternDestroy(pat); + return NULL; } bool diff --git a/kitty/fonts.c b/kitty/fonts.c index 688390433..c690799f7 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -1635,25 +1635,31 @@ concat_cells(PyObject UNUSED *self, PyObject *args) { static PyObject* current_fonts(PYNOARG) { if (!num_font_groups) { PyErr_SetString(PyExc_RuntimeError, "must create font group first"); return NULL; } - PyObject *ans = PyDict_New(); + RAII_PyObject(ans, PyDict_New()); if (!ans) return NULL; FontGroup *fg = font_groups; -#define SET(key, val) {if (PyDict_SetItemString(ans, #key, fg->fonts[val].face) != 0) { goto error; }} +#define SET(key, val) {if (PyDict_SetItemString(ans, #key, fg->fonts[val].face) != 0) { return NULL; }} SET(medium, fg->medium_font_idx); if (fg->bold_font_idx > 0) SET(bold, fg->bold_font_idx); if (fg->italic_font_idx > 0) SET(italic, fg->italic_font_idx); if (fg->bi_font_idx > 0) SET(bi, fg->bi_font_idx); - PyObject *ff = PyTuple_New(fg->fallback_fonts_count); - if (!ff) goto error; + unsigned num_symbol_fonts = fg->first_fallback_font_idx - fg->first_symbol_font_idx; + RAII_PyObject(ss, PyTuple_New(num_symbol_fonts)); + if (!ss) return NULL; + for (size_t i = 0; i < num_symbol_fonts; i++) { + Py_INCREF(fg->fonts[fg->first_symbol_font_idx + i].face); + PyTuple_SET_ITEM(ss, i, fg->fonts[fg->first_symbol_font_idx + i].face); + } + if (PyDict_SetItemString(ans, "symbol", ss) != 0) return NULL; + RAII_PyObject(ff, PyTuple_New(fg->fallback_fonts_count)); + if (!ff) return NULL; for (size_t i = 0; i < fg->fallback_fonts_count; i++) { Py_INCREF(fg->fonts[fg->first_fallback_font_idx + i].face); PyTuple_SET_ITEM(ff, i, fg->fonts[fg->first_fallback_font_idx + i].face); } - PyDict_SetItemString(ans, "fallback", ff); - Py_CLEAR(ff); + if (PyDict_SetItemString(ans, "fallback", ff) != 0) return NULL; + Py_INCREF(ans); return ans; -error: - Py_CLEAR(ans); return NULL; #undef SET } diff --git a/kitty/fonts/__init__.py b/kitty/fonts/__init__.py index 253bb232a..f07ac2c19 100644 --- a/kitty/fonts/__init__.py +++ b/kitty/fonts/__init__.py @@ -1,5 +1,5 @@ from enum import Enum, IntEnum, auto -from typing import Dict, NamedTuple, Optional, Tuple, TypedDict, Union +from typing import Dict, NamedTuple, Tuple, TypedDict, Union from kitty.typing import CoreTextFont, FontConfigPattern @@ -24,8 +24,8 @@ class VariableAxis(TypedDict): class NamedStyle(TypedDict): axis_values: Dict[str, float] - name: Optional[str] - psname: Optional[str] + name: str + psname: str # can be empty string when not present class VariableData(TypedDict): diff --git a/kitty/fonts/core_text.py b/kitty/fonts/core_text.py index 9b26d2617..ca0bf759a 100644 --- a/kitty/fonts/core_text.py +++ b/kitty/fonts/core_text.py @@ -5,7 +5,7 @@ from typing import Dict, Generator, Iterable, List, Optional, Tuple from kitty.fast_data_types import CTFace, coretext_all_fonts -from kitty.fonts import FontFeature, FontSpec, VariableData +from kitty.fonts import FontSpec, VariableData from kitty.options.types import Options from kitty.typing import CoreTextFont from kitty.utils import log_error @@ -52,11 +52,6 @@ def list_fonts() -> Generator[ListedFont, None, None]: 'is_variable': fd['variable'], 'descriptor': fd} -def find_font_features(postscript_name: str) -> Tuple[FontFeature, ...]: - """Not Implemented""" - return () - - def find_best_match(family: str, bold: bool = False, italic: bool = False, ignore_face: Optional[CoreTextFont] = None) -> CoreTextFont: q = re.sub(r'\s+', ' ', family.lower()) font_map = all_fonts_map() @@ -124,8 +119,17 @@ def font_for_family(family: str) -> Tuple[CoreTextFont, bool, bool]: return ans, ans['bold'], ans['italic'] +cache_for_variable_data_by_path: Dict[str, VariableData] = {} + + def get_variable_data_for_descriptor(f: ListedFont) -> VariableData: - return CTFace(descriptor=descriptor(f)).get_variable_data() + d = descriptor(f) + if not d['path']: + return CTFace(descriptor=d).get_variable_data() + ans = cache_for_variable_data_by_path.get(d['path']) + if ans is None: + ans = cache_for_variable_data_by_path[d['path']] = CTFace(descriptor=d).get_variable_data() + return ans def descriptor(f: ListedFont) -> CoreTextFont: diff --git a/kitty/fonts/fontconfig.py b/kitty/fonts/fontconfig.py index 58d0af614..66cf4680a 100644 --- a/kitty/fonts/fontconfig.py +++ b/kitty/fonts/fontconfig.py @@ -3,7 +3,7 @@ import re from functools import lru_cache -from typing import Dict, Generator, List, Optional, Tuple, cast +from typing import Callable, Dict, Generator, List, Literal, NamedTuple, Optional, Tuple, cast from kitty.fast_data_types import ( FC_DUAL, @@ -15,15 +15,12 @@ FC_WIDTH_NORMAL, Face, fc_list, - fc_match_postscript_name, - parse_font_feature, ) from kitty.fast_data_types import fc_match as fc_match_impl from kitty.options.types import Options from kitty.typing import FontConfigPattern -from kitty.utils import log_error -from . import FontFeature, FontSpec, ListedFont, VariableData +from . import FontSpec, ListedFont, VariableData attr_map = {(False, False): 'font_family', (True, False): 'bold_font', @@ -31,7 +28,8 @@ (True, True): 'bold_italic_font'} -FontMap = Dict[str, Dict[str, List[FontConfigPattern]]] +FontCollectionMapType = Literal['family_map', 'ps_map', 'full_map'] +FontMap = Dict[FontCollectionMapType, Dict[str, List[FontConfigPattern]]] def create_font_map(all_fonts: Tuple[FontConfigPattern, ...]) -> FontMap: @@ -84,47 +82,56 @@ def fc_match(family: str, bold: bool, italic: bool, spacing: int = FC_MONO) -> F return fc_match_impl(family, bold, italic, spacing) -def find_font_features(postscript_name: str) -> Tuple[FontFeature, ...]: - pat = fc_match_postscript_name(postscript_name) +class Score(NamedTuple): + variable_score: int + style_score: int + monospace_score: int + width_score: int - if pat.get('postscript_name') != postscript_name or 'fontfeatures' not in pat: - return () +Scorer = Callable[[FontConfigPattern], Score] - features = [] - for feat in pat['fontfeatures']: - try: - parsed = parse_font_feature(feat) - except ValueError: - log_error(f'Ignoring invalid font feature: {feat}') - else: - features.append(FontFeature(feat, parsed)) +def create_scorer(bold: bool = False, italic: bool = False, monospaced: bool = True, prefer_variable: bool = False) -> Scorer: - return tuple(features) + def score(candidate: FontConfigPattern) -> Score: + variable_score = 0 if prefer_variable and candidate['variable'] else 1 + bold_score = abs((FC_WEIGHT_BOLD if bold else FC_WEIGHT_REGULAR) - candidate.get('weight', 0)) + italic_score = abs((FC_SLANT_ITALIC if italic else FC_SLANT_ROMAN) - candidate.get('slant', 0)) + monospace_match = 0 + if monospaced: + monospace_match = 0 if candidate.get('spacing') == 'MONO' else 1 + width_score = abs(candidate.get('width', FC_WIDTH_NORMAL) - FC_WIDTH_NORMAL) + + return Score(variable_score, bold_score + italic_score, monospace_match, width_score) + + return score + + +def find_best_match_in_candidates( + candidates: List[FontConfigPattern], scorer: Scorer, is_medium_face: bool +) -> Optional[FontConfigPattern]: + if not candidates: + return None + if len(candidates) == 1 and not is_medium_face and candidates[0].get('family') == candidates[0].get('full_name'): + # IBM Plex Mono does this, where the full name of the regular font + # face is the same as its family name + return None + candidates.sort(key=scorer) + return candidates[0] def find_best_match(family: str, bold: bool = False, italic: bool = False, monospaced: bool = True) -> FontConfigPattern: q = family_name_to_key(family) font_map = all_fonts_map(monospaced) - - def score(candidate: FontConfigPattern) -> Tuple[int, int, int]: - bold_score = abs((FC_WEIGHT_BOLD if bold else FC_WEIGHT_REGULAR) - candidate.get('weight', 0)) - italic_score = abs((FC_SLANT_ITALIC if italic else FC_SLANT_ROMAN) - candidate.get('slant', 0)) - monospace_match = 0 if candidate.get('spacing') == 'MONO' else 1 - width_score = abs(candidate.get('width', FC_WIDTH_NORMAL) - FC_WIDTH_NORMAL) - - return bold_score + italic_score, monospace_match, width_score - + scorer = create_scorer(bold, italic, monospaced) + is_medium_face = not bold and not italic # First look for an exact match - for selector in ('ps_map', 'full_map', 'family_map'): - candidates = font_map[selector].get(q) - if not candidates: - continue - if len(candidates) == 1 and (bold or italic) and candidates[0].get('family') == candidates[0].get('full_name'): - # IBM Plex Mono does this, where the full name of the regular font - # face is the same as its family name - continue - candidates.sort(key=score) - return candidates[0] + exact_match = ( + find_best_match_in_candidates(font_map['ps_map'].get(q, []), scorer, is_medium_face) or + find_best_match_in_candidates(font_map['full_map'].get(q, []), scorer, is_medium_face) or + find_best_match_in_candidates(font_map['family_map'].get(q, []), scorer, is_medium_face) + ) + if exact_match: + return exact_match # Use fc-match to see if we can find a monospaced font that matches family # When aliases are defined, spacing can cause the incorrect font to be @@ -135,6 +142,7 @@ def score(candidate: FontConfigPattern) -> Tuple[int, int, int]: tries = (dual_possibility, mono_possibility) if any_possibility == dual_possibility else (mono_possibility, dual_possibility) for possibility in tries: for key, map_key in (('postscript_name', 'ps_map'), ('full_name', 'full_map'), ('family', 'family_map')): + map_key = cast(FontCollectionMapType, map_key) val: Optional[str] = cast(Optional[str], possibility.get(key)) if val: candidates = font_map[map_key].get(family_name_to_key(val)) @@ -146,19 +154,83 @@ def score(candidate: FontConfigPattern) -> Tuple[int, int, int]: family_name_candidates = font_map['family_map'].get(family_name_to_key(candidates[0]['family'])) if family_name_candidates and len(family_name_candidates) > 1: candidates = family_name_candidates - return sorted(candidates, key=score)[0] + return sorted(candidates, key=scorer)[0] # Use fc-match with a generic family family = 'monospace' if monospaced else 'sans-serif' return fc_match(family, bold, italic) +def get_fine_grained_font( + spec: FontSpec, bold: bool = False, italic: bool = False, medium_font_spec: FontSpec = FontSpec(), + resolved_medium_font: Optional[FontConfigPattern] = None, monospaced: bool = True +) -> FontConfigPattern: + font_map = all_fonts_map(monospaced) + scorer = create_scorer(bold, italic, monospaced, prefer_variable=bool(spec.axes) or bool(spec.style)) + is_medium_face = not bold and not italic + if spec.postscript_name: + q = find_best_match_in_candidates(font_map['ps_map'].get(family_name_to_key(spec.postscript_name), []), scorer, is_medium_face) + if q: + return q + if spec.full_name: + q = find_best_match_in_candidates(font_map['full_map'].get(family_name_to_key(spec.full_name), []), scorer, is_medium_face) + if q: + return q + if spec.family: + candidates = font_map['family_map'].get(family_name_to_key(spec.family), []) + if spec.style: + qs = spec.style.lower() + candidates = [x for x in candidates if x['style'].lower() == qs] + q = find_best_match_in_candidates(candidates, scorer, is_medium_face) + if q: + return q + # Use fc-match with a generic family + family = 'monospace' if monospaced else 'sans-serif' + return fc_match(family, bold, italic) + + +def apply_variation_to_pattern(pat: FontConfigPattern, spec: FontSpec) -> FontConfigPattern: + if not pat['variable']: + return pat + + vd = Face(descriptor=pat).get_variable_data() + if spec.style: + q = spec.style.lower() + for i, ns in enumerate(vd['named_styles']): + if ns.get('psname') and ns['psname'].lower() == q: + pat['named_style'] = i + break + else: + for i, ns in enumerate(vd['named_styles']): + if ns['name'].lower() == q: + pat['named_style'] = i + break + tag_map, name_map = {}, {} + axes = [ax['default'] for ax in vd['axes']] + for i, ax in enumerate(vd['axes']): + tag_map[ax['tag']] = i + if ax['strid']: + name_map[ax['strid'].lower()] = i + changed = False + for axspec in spec.axes: + qname = axspec[0] + axis = tag_map.get(qname) + if axis is None: + axis = name_map.get(qname.lower()) + if axis is not None: + axes[axis] = axspec[1] + changed = True + if changed: + pat['axes'] = axes + return pat + + def get_font_from_spec( spec: FontSpec, bold: bool = False, italic: bool = False, medium_font_spec: FontSpec = FontSpec(), resolved_medium_font: Optional[FontConfigPattern] = None ) -> FontConfigPattern: if not spec.is_system: - raise NotImplementedError('TODO: Implement me') + return apply_variation_to_pattern(get_fine_grained_font(spec, bold, italic, medium_font_spec, resolved_medium_font), spec) family = spec.system if family == 'auto' and (bold or italic): assert resolved_medium_font is not None @@ -191,8 +263,17 @@ def descriptor(f: ListedFont) -> FontConfigPattern: return d +cache_for_variable_data_by_path: Dict[str, VariableData] = {} + + def get_variable_data_for_descriptor(f: ListedFont) -> VariableData: - return Face(descriptor=descriptor(f)).get_variable_data() + d = descriptor(f) + if not d['path']: + return Face(descriptor=d).get_variable_data() + ans = cache_for_variable_data_by_path.get(d['path']) + if ans is None: + ans = cache_for_variable_data_by_path[d['path']] = Face(descriptor=d).get_variable_data() + return ans def prune_family_group(g: List[ListedFont]) -> List[ListedFont]: diff --git a/kitty/fonts/render.py b/kitty/fonts/render.py index 0f7c760ea..bc53f8ea8 100644 --- a/kitty/fonts/render.py +++ b/kitty/fonts/render.py @@ -12,6 +12,7 @@ NUM_UNDERLINE_STYLES, Screen, create_test_font_group, + current_fonts, get_fallback_font, get_options, set_font_data, @@ -29,11 +30,9 @@ from kitty.utils import log_error if is_macos: - from .core_text import find_font_features from .core_text import font_for_family as font_for_family_macos from .core_text import get_font_files as get_font_files_coretext else: - from .fontconfig import find_font_features from .fontconfig import font_for_family as font_for_family_fontconfig from .fontconfig import get_font_files as get_font_files_fontconfig @@ -165,27 +164,19 @@ def descriptor_for_idx(idx: int) -> Tuple[FontObject, bool, bool]: return current_faces[idx] -def dump_faces(ftypes: List[str], indices: Dict[str, int]) -> None: - def face_str(f: Tuple[FontObject, bool, bool]) -> str: - fo = f[0] - if 'index' in fo: - return '{}:{}'.format(fo['path'], cast('FontConfigPattern', fo)['index']) - fo = cast('CoreTextFont', fo) - return fo['path'] - - log_error('Preloaded font faces:') - log_error('normal face:', face_str(current_faces[0])) - for ftype in ftypes: - if indices[ftype]: - log_error(ftype, 'face:', face_str(current_faces[indices[ftype]])) - si_faces = current_faces[max(indices.values())+1:] - if si_faces: - log_error('Symbol map faces:') - for face in si_faces: - log_error(face_str(face)) +def dump_font_debug() -> None: + cf = current_fonts() + log_error('Text fonts:') + for key, text in {'medium': 'Normal', 'bold': 'Bold', 'italic': 'Italic', 'bi': 'Bold-Italic'}.items(): + log_error(f' {text}:', cf[key].identify_for_debug()) # type: ignore + ss = cf['symbol'] + if ss: + log_error('Symbol map fonts:') + for s in ss: + log_error(' ' + s.identify_for_debug()) -def set_font_family(opts: Optional[Options] = None, override_font_size: Optional[float] = None, debug_font_matching: bool = False) -> None: +def set_font_family(opts: Optional[Options] = None, override_font_size: Optional[float] = None) -> None: global current_faces opts = opts or defaults sz = override_font_size or opts.font_size @@ -201,16 +192,10 @@ def set_font_family(opts: Optional[Options] = None, override_font_size: Optional sm = create_symbol_map(opts) ns = create_narrow_symbols(opts) num_symbol_fonts = len(current_faces) - before - font_features = {} - for face, _, _ in current_faces: - font_features[face['postscript_name']] = find_font_features(face['postscript_name']) - font_features.update(opts.font_features) - if debug_font_matching: - dump_faces(ftypes, indices) set_font_data( render_box_drawing, prerender_function, descriptor_for_idx, indices['bold'], indices['italic'], indices['bi'], num_symbol_fonts, - sm, sz, font_features, ns + sm, sz, opts.font_features.copy(), ns ) diff --git a/kitty/freetype.c b/kitty/freetype.c index 79fb1ca3a..353155790 100644 --- a/kitty/freetype.c +++ b/kitty/freetype.c @@ -265,13 +265,32 @@ face_from_descriptor(PyObject *descriptor, FONTS_DATA_HANDLE fg) { D(hinting, PyObject_IsTrue, true); D(hint_style, PyLong_AsLong, true); #undef D - Face *self = (Face *)Face_Type.tp_alloc(&Face_Type, 0); - if (self != NULL) { - int error = FT_New_Face(library, path, index, &(self->face)); - if(error) { Py_CLEAR(self); return set_load_error(path, error); } - if (!init_ft_face(self, PyDict_GetItemString(descriptor, "path"), hinting, hint_style, fg)) { Py_CLEAR(self); return NULL; } + RAII_PyObject(retval, Face_Type.tp_alloc(&Face_Type, 0)); + if (retval != NULL) { + Face *self = (Face *)retval; + int error; + if ((error = FT_New_Face(library, path, index, &(self->face)))) { self->face = NULL; set_load_error(path, error); } + if (!init_ft_face(self, PyDict_GetItemString(descriptor, "path"), hinting, hint_style, fg)) return NULL; + PyObject *ns = PyDict_GetItemString(descriptor, "named_style"); + if (ns) { + unsigned long index = PyLong_AsUnsignedLong(ns); + if (PyErr_Occurred()) return NULL; + if ((error = FT_Set_Named_Instance(self->face, index + 1))) return set_load_error(path, error); + } + PyObject *axes = PyDict_GetItemString(descriptor, "axes"); + if (axes) { + RAII_ALLOC(FT_Fixed, coords, malloc(sizeof(FT_Fixed) * PyList_GET_SIZE(axes))); + for (Py_ssize_t i = 0; i < PyList_GET_SIZE(axes); i++) { + PyObject *t = PyList_GET_ITEM(axes, i); + double val = PyFloat_AsDouble(t); + if (PyErr_Occurred()) return NULL; + coords[i] = (FT_Fixed)(val * 65536.0); + } + if ((error = FT_Set_Var_Design_Coordinates(self->face, PyList_GET_SIZE(axes), coords))) return set_load_error(path, error); + } } - return (PyObject*)self; + Py_XINCREF(retval); + return retval; } static PyObject* @@ -768,7 +787,7 @@ convert_named_style_to_python(Face *face, const FT_Var_Named_Style *src, FT_Var_ 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); + return Py_BuildValue("{sO sO sO}", "axis_values", axis_values, "name", name ? name : PyUnicode_FromString(""), "psname", psname ? psname : PyUnicode_FromString("")); } static PyObject* diff --git a/kitty/main.py b/kitty/main.py index 476b4e038..571f3de83 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -44,7 +44,7 @@ set_options, ) from .fonts.box_drawing import set_scale -from .fonts.render import set_font_family +from .fonts.render import dump_font_debug, set_font_family from .options.types import Options from .options.utils import DELETE_ENV_VAR from .os_window_size import edge_spacing, initial_window_size_func @@ -225,6 +225,8 @@ def _run_app(opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = (), wincls, wstate, load_all_shaders, disallow_override_title=bool(args.title), layer_shell_config=run_app.layer_shell_config) boss = Boss(opts, args, cached_values, global_shortcuts, talk_fd) boss.start(window_id, startup_sessions) + if args.debug_font_fallback: + dump_font_debug() if bad_lines or boss.misc_config_errors: boss.show_bad_config_lines(bad_lines, boss.misc_config_errors) boss.misc_config_errors = [] @@ -246,7 +248,7 @@ def __call__(self, opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] set_scale(opts.box_drawing_scale) set_options(opts, is_wayland(), args.debug_rendering, args.debug_font_fallback) try: - set_font_family(opts, debug_font_matching=args.debug_font_fallback) + set_font_family(opts) _run_app(opts, args, bad_lines, talk_fd) finally: set_options(None)