From d205b147ebd0ed2559d33c8898d8a3968e230a99 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 19 May 2024 10:22:40 +0530 Subject: [PATCH] Improve auto selection of variable faces --- kitty/fonts/common.py | 127 ++++++++++++++++++++++++++++---------- kitty/fonts/core_text.py | 4 ++ kitty/fonts/fontconfig.py | 18 ++++++ kitty_tests/fonts.py | 8 ++- 4 files changed, 123 insertions(+), 34 deletions(-) diff --git a/kitty/fonts/common.py b/kitty/fonts/common.py index db86db320..8efff38d6 100644 --- a/kitty/fonts/common.py +++ b/kitty/fonts/common.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union from kitty.constants import is_macos -from kitty.fonts import Descriptor, DescriptorVar, DesignAxis, FontSpec, NamedStyle, Scorer, VariableData, family_name_to_key +from kitty.fonts import Descriptor, DescriptorVar, DesignAxis, FontSpec, NamedStyle, Scorer, VariableAxis, VariableData, family_name_to_key from kitty.options.types import Options if TYPE_CHECKING: @@ -26,6 +26,7 @@ if TYPE_CHECKING: def is_variable(descriptor: Descriptor) -> bool: ... def set_named_style(name: str, font: Descriptor, vd: VariableData) -> bool: ... def set_axis_values(tag_map: Dict[str, float], font: Descriptor, vd: VariableData) -> bool: ... + def get_axis_values(font: Descriptor, vd: VariableData) -> Dict[str, float]: ... else: FontCollectionMapType = FontMap = None if is_macos: @@ -35,6 +36,7 @@ else: create_scorer, find_best_match, find_last_resort_text_font, + get_axis_values, is_monospace, is_variable, set_axis_values, @@ -47,6 +49,7 @@ else: create_scorer, find_best_match, find_last_resort_text_font, + get_axis_values, is_monospace, is_variable, set_axis_values, @@ -63,6 +66,47 @@ class Event: is_set: bool = False +class FamilyAxisValues: + regular_weight: Optional[float] = None + regular_slant: Optional[float] = None + regular_ital: Optional[float] = None + regular_width: Optional[float] = None + + bold_weight: Optional[float] = None + + italic_slant: Optional[float] = None + italic_ital: Optional[float] = None + + def get_wght(self, bold: bool, italic: bool) -> Optional[float]: + return self.bold_weight if bold else self.regular_weight + + def get_ital(self, bold: bool, italic: bool) -> Optional[float]: + return self.italic_ital if italic else self.regular_ital + + def get_slnt(self, bold: bool, italic: bool) -> Optional[float]: + return self.italic_slant if italic else self.regular_slant + + def get_wdth(self, bold: bool, italic: bool) -> Optional[float]: + return self.regular_width + + def get(self, tag: str, bold: bool, italic: bool) -> Optional[float]: + f = getattr(self, f'get_{tag}', None) + return None if f is None else f(bold, italic) + + def set_regular_values(self, axis_values: Dict[str, float]) -> None: + self.regular_weight = axis_values.get('wght') + self.regular_width = axis_values.get('wdth') + self.regular_ital = axis_values.get('ital') + self.regular_slant = axis_values.get('slnt') + + def set_bold_values(self, axis_values: Dict[str, float]) -> None: + self.bold_weight = axis_values.get('wght') + + def set_italic_values(self, axis_values: Dict[str, float]) -> None: + self.italic_ital = axis_values.get('ital') + self.italic_slant = axis_values.get('slnt') + + def get_variable_data_for_descriptor(d: Descriptor) -> VariableData: if not d['path']: return face_from_descriptor(d).get_variable_data() @@ -119,7 +163,11 @@ def find_medium_variant(font: DescriptorVar) -> DescriptorVar: return font -def get_design_value_for(dax: DesignAxis, default: float, bold: bool, italic: bool) -> float: +def get_design_value_for(dax: DesignAxis, ax: VariableAxis, bold: bool, italic: bool, family_axis_values: FamilyAxisValues) -> float: + family_val = family_axis_values.get(ax['tag'], bold, italic) + if family_val is not None and ax['minimum'] <= family_val <= ax['maximum']: + return family_val + default = ax['default'] if dax['tag'] == 'wght': keys = ('semibold', 'bold', 'heavy', 'black') if bold else ('regular', 'medium') elif dax['tag'] in ('ital', 'slnt'): @@ -133,7 +181,7 @@ def get_design_value_for(dax: DesignAxis, default: float, bold: bool, italic: bo return default -def find_bold_italic_variant(medium: Descriptor, bold: bool, italic: bool) -> Descriptor: +def find_bold_italic_variant(medium: Descriptor, bold: bool, italic: bool, family_axis_values: FamilyAxisValues) -> Descriptor: # we first pick the best font file for bold/italic if there are more than # one. For example SourceCodeVF has Italic and Upright faces with variable # weights in each, so we rely on the OS font matcher to give us the best @@ -149,7 +197,7 @@ def find_bold_italic_variant(medium: Descriptor, bold: bool, italic: bool) -> De tag = ax['tag'] for dax in vd['design_axes']: if dax['tag'] == tag: - axis_values[tag] = get_design_value_for(dax, ax['default'], bold, italic) + axis_values[tag] = get_design_value_for(dax, ax, bold, italic, family_axis_values) break if axis_values: set_axis_values(axis_values, ans, vd) @@ -177,7 +225,7 @@ def find_best_variable_face(spec: FontSpec, bold: bool, italic: bool, monospaced def get_fine_grained_font( - spec: FontSpec, bold: bool = False, italic: bool = False, + spec: FontSpec, bold: bool = False, italic: bool = False, family_axis_values: FamilyAxisValues = FamilyAxisValues(), resolved_medium_font: Optional[Descriptor] = None, monospaced: bool = True, match_is_more_specific_than_family: Event = Event() ) -> Descriptor: font_map = all_fonts_map(monospaced) @@ -203,7 +251,7 @@ def get_fine_grained_font( if applied: match_is_more_specific_than_family.is_set = True return q - return find_medium_variant(q) if resolved_medium_font is None else find_bold_italic_variant(resolved_medium_font, bold, italic) + return find_medium_variant(q) if resolved_medium_font is None else find_bold_italic_variant(resolved_medium_font, bold, italic, family_axis_values) # Now look for any font candidates = font_map['family_map'].get(key, []) if candidates: @@ -241,11 +289,11 @@ def apply_variation_to_pattern(pat: Descriptor, spec: FontSpec) -> Tuple[Descrip def get_font_from_spec( - spec: FontSpec, bold: bool = False, italic: bool = False, + spec: FontSpec, bold: bool = False, italic: bool = False, family_axis_values: FamilyAxisValues = FamilyAxisValues(), resolved_medium_font: Optional[Descriptor] = None, match_is_more_specific_than_family: Event = Event() ) -> Descriptor: if not spec.is_system: - return get_fine_grained_font(spec, bold, italic, resolved_medium_font, + return get_fine_grained_font(spec, bold, italic, resolved_medium_font=resolved_medium_font, family_axis_values=family_axis_values, match_is_more_specific_than_family=match_is_more_specific_than_family) family = spec.system or '' if family == 'auto': @@ -253,7 +301,7 @@ def get_font_from_spec( assert resolved_medium_font is not None family = resolved_medium_font['family'] if is_variable(resolved_medium_font) or is_actually_variable_despite_fontconfigs_lies(resolved_medium_font): - v = find_bold_italic_variant(resolved_medium_font, bold, italic) + v = find_bold_italic_variant(resolved_medium_font, bold, italic, family_axis_values=family_axis_values) if v is not None: return v else: @@ -268,13 +316,22 @@ class FontFiles(TypedDict): bi: Descriptor +actually_variable_cache: Dict[str, bool] = {} + + def is_actually_variable_despite_fontconfigs_lies(d: Descriptor) -> bool: if d['descriptor_type'] != 'fontconfig': return False + path = d['path'] + ans = actually_variable_cache.get(path) + if ans is not None: + return ans m = all_fonts_map(is_monospace(d))['variable_map'] for x in m.get(family_name_to_key(d['family']), ()): - if x['path'] == d['path']: + if x['path'] == path: + actually_variable_cache[path] = True return True + actually_variable_cache[path] = False return False @@ -282,13 +339,19 @@ def get_font_files(opts: Options) -> FontFiles: ans: Dict[str, Descriptor] = {} match_is_more_specific_than_family = Event() medium_font = get_font_from_spec(opts.font_family, match_is_more_specific_than_family=match_is_more_specific_than_family) - if not match_is_more_specific_than_family.is_set and ( - is_variable(medium_font) or is_actually_variable_despite_fontconfigs_lies(medium_font)): + medium_font_is_variable = is_variable(medium_font) or is_actually_variable_despite_fontconfigs_lies(medium_font) + if not match_is_more_specific_than_family.is_set and medium_font_is_variable: medium_font = find_medium_variant(medium_font) + family_axis_values = FamilyAxisValues() + if medium_font_is_variable: + family_axis_values.set_regular_values(get_axis_values(medium_font, get_variable_data_for_descriptor(medium_font))) kd = {(False, False): 'medium', (True, False): 'bold', (False, True): 'italic', (True, True): 'bi'} for (bold, italic), attr in attr_map.items(): if bold or italic: - font = get_font_from_spec(getattr(opts, attr), bold, italic, resolved_medium_font=medium_font) + font = get_font_from_spec(getattr(opts, attr), bold, italic, resolved_medium_font=medium_font, family_axis_values=family_axis_values) + if not (bold and italic) and (is_variable(medium_font) or is_actually_variable_despite_fontconfigs_lies(medium_font)): + av = get_axis_values(font, get_variable_data_for_descriptor(font)) + (family_axis_values.set_italic_values if italic else family_axis_values.set_bold_values)(av) else: font = medium_font key = kd[(bold, italic)] @@ -296,25 +359,6 @@ def get_font_files(opts: Options) -> FontFiles: return {'medium': ans['medium'], 'bold': ans['bold'], 'italic': ans['italic'], 'bi': ans['bi']} -def develop(family: str = '') -> None: - import sys - family = family or sys.argv[-1] - from kitty.options.utils import parse_font_spec - 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)) - - print('Medium :', s(ff['medium'])) - print() - print('Bold :', s(ff['bold'])) - print() - print('Italic :', s(ff['italic'])) - print() - print('Bold-Italic:', s(ff['bi'])) - - def axis_values_are_equal(defaults: Dict[str, float], a: Dict[str, float], b: Dict[str, float]) -> bool: ad, bd = defaults.copy(), defaults.copy() ad.update(a) @@ -397,5 +441,24 @@ def spec_for_face(family: str, face: Face) -> FontSpec: return FontSpec(family=family, variable_name=varname, style=ns['psname'] or ns['name']) +def develop(family: str = '') -> None: + import sys + family = family or sys.argv[-1] + from kitty.options.utils import parse_font_spec + 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)) + + print('Medium :', s(ff['medium'])) + print() + print('Bold :', s(ff['bold'])) + print() + print('Italic :', s(ff['italic'])) + print() + print('Bold-Italic:', s(ff['bi'])) + + if __name__ == '__main__': develop() diff --git a/kitty/fonts/core_text.py b/kitty/fonts/core_text.py index 9c2c982b6..0f809d21a 100644 --- a/kitty/fonts/core_text.py +++ b/kitty/fonts/core_text.py @@ -249,3 +249,7 @@ def set_named_style(name: str, font: CoreTextFont, vd: VariableData) -> bool: if ns['name'].lower() == q: return set_axis_values(ns['axis_values'], font, vd) return False + + +def get_axis_values(font: CoreTextFont, vd: VariableData) -> Dict[str, float]: + return font.get('axis_map', {}) diff --git a/kitty/fonts/fontconfig.py b/kitty/fonts/fontconfig.py index 0a78f4e64..0fbec5483 100644 --- a/kitty/fonts/fontconfig.py +++ b/kitty/fonts/fontconfig.py @@ -271,3 +271,21 @@ def set_axis_values(tag_map: Dict[str, float], font: FontConfigPattern, vd: Vari font['axes'] = tuple(axes) lift_axes_to_named_style_if_possible(font, vd) return changed + + +def get_axis_values(font: FontConfigPattern, vd: VariableData) -> Dict[str, float]: + ans: Dict[str, float] = {} + ns = font.get('named_style') + if ns is not None: + if ns > -1 and ns < len(vd['named_styles']): + ans = vd['named_styles'][ns]['axis_values'] + + axis_values = font.get('axes', ()) + for i, ax in enumerate(vd['axes']): + tag = ax['tag'] + if i < len(axis_values): + ans[tag] = axis_values[i] + else: + if tag not in ans: + ans[tag] = ax['default'] + return ans diff --git a/kitty_tests/fonts.py b/kitty_tests/fonts.py index 5f68fe617..2413ce1c3 100644 --- a/kitty_tests/fonts.py +++ b/kitty_tests/fonts.py @@ -95,12 +95,16 @@ class Selection(BaseTest): if has('operator mono', allow_missing_in_ci=True): both('operator mono', 'OperatorMono-Medium', 'OperatorMono-Bold', 'OperatorMono-MediumItalic', 'OperatorMono-BoldItalic') + # Test variable font selection + if has('SourceCodeVF'): opts = Options() opts.font_family = parse_font_spec('family="SourceCodeVF" variable_name="SourceCodeUpright" style="Black"') - d = get_font_files(opts)['medium'] - face = face_from_descriptor(d) + ff = get_font_files(opts) + face = face_from_descriptor(ff['medium']) self.ae(get_named_style(face)['name'], 'Black') + face = face_from_descriptor(ff['italic']) + self.ae(get_named_style(face)['name'], 'Black Italic') if has('cascadia code'): opts = Options() opts.font_family = parse_font_spec('family="cascadia code"')