mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-07-03 11:12:30 +08:00
fontconfig's FcFontList omits FC_MATRIX from its object set (kitty/fontconfig.c), so a roman font that find_best_match finds there (e.g. Fira Code, which ships no italic, in both its static and variable builds) carries no synthetic-italic shear and its "italic" renders upright. A family that is not found is substituted, and when the substitute resolves through the listed faces those descriptors are equally matrix-less, so this attach covers them too. Only raw fc_match descriptors (runtime glyph-fallback faces via create_fallback_face, and find_best_match's last-resort return) already carry the matrix from substitution. The italic intent for the configured faces exists only during selection, not at face construction, so attach the matrix at the end of get_font_files: for an italic slot whose chosen face is upright and has no matrix, ask fc_match what fontconfig would do. fc_match returns a synthetic matrix only when there is no real italic to use (no italic face and no slanted named instance or variable slant axis), so a font that is already italic, static or variable, is never double-slanted. Face construction applies the matrix via FT_Set_Transform; the previous commit makes it survive the size specialization step the render path builds faces from. Only the matrix is taken, so selection is unchanged. FontConfigPattern declared matrix as a required key, but pattern_as_dict sets it only when the pattern has one, so declare it NotRequired. With that and narrowing on descriptor_type the attach needs no cast. Add a regression test (test_synthetic_italic_matrix): a roman no-italic font gets a non-identity matrix on its italic slot while a real-italic control does not, and the matrix survives specialize_font_descriptor. It asserts the invariant rather than the exact shear (the value is fontconfig's, version-dependent) and skips when the synthetic rule is inactive. Covers the four configured faces. Limitation: fc_match re-matches by family name, so under an uncommon config (a multi-face family key plus a user per-font FC_MATRIX rule keyed on width/style) it can attach a matrix computed for a different face; the 90-synthetic shear this targets is weight-independent and unaffected. A production version should re-match the selected face by path+index+slant.
554 lines
22 KiB
Python
554 lines
22 KiB
Python
#!/usr/bin/env python
|
|
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
|
|
|
|
from typing import TYPE_CHECKING, Any, Literal, TypedDict, Union
|
|
|
|
from kitty.constants import is_macos
|
|
from kitty.fast_data_types import ParsedFontFeature
|
|
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:
|
|
from kitty.fast_data_types import CTFace
|
|
from kitty.fast_data_types import Face as FT_Face
|
|
|
|
FontCollectionMapType = Literal['family_map', 'ps_map', 'full_map', 'variable_map']
|
|
FontMap = dict[FontCollectionMapType, dict[str, list[Descriptor]]]
|
|
Face = Union[FT_Face, CTFace]
|
|
def all_fonts_map(monospaced: bool) -> FontMap: ...
|
|
def create_scorer(bold: bool = False, italic: bool = False, monospaced: bool = True, prefer_variable: bool = False) -> Scorer: ...
|
|
def find_best_match(
|
|
family: str, bold: bool = False, italic: bool = False, monospaced: bool = True, ignore_face: Descriptor | None = None,
|
|
prefer_variable: bool = False,
|
|
) -> Descriptor: ...
|
|
def find_last_resort_text_font(bold: bool = False, italic: bool = False, monospaced: bool = True) -> Descriptor: ...
|
|
def face_from_descriptor(descriptor: Descriptor, font_sz_in_pts: float | None = None, dpi_x: float | None = None, dpi_y: float | None = None
|
|
) -> Face: ...
|
|
def is_monospace(descriptor: Descriptor) -> bool: ...
|
|
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
|
|
from kitty.fast_data_types import specialize_font_descriptor
|
|
if is_macos:
|
|
from kitty.fast_data_types import CTFace as Face
|
|
from kitty.fonts.core_text import (
|
|
all_fonts_map,
|
|
create_scorer,
|
|
find_best_match,
|
|
find_last_resort_text_font,
|
|
get_axis_values,
|
|
is_monospace,
|
|
is_variable,
|
|
set_axis_values,
|
|
set_named_style,
|
|
)
|
|
else:
|
|
from kitty.fast_data_types import Face
|
|
from kitty.fonts.fontconfig import (
|
|
all_fonts_map,
|
|
create_scorer,
|
|
find_best_match,
|
|
find_last_resort_text_font,
|
|
get_axis_values,
|
|
is_monospace,
|
|
is_variable,
|
|
set_axis_values,
|
|
set_named_style,
|
|
)
|
|
def face_from_descriptor(descriptor, font_sz_in_pts = None, dpi_x = None, dpi_y = None):
|
|
if font_sz_in_pts is not None:
|
|
descriptor = specialize_font_descriptor(descriptor, font_sz_in_pts, dpi_x, dpi_y)
|
|
return Face(descriptor=descriptor)
|
|
|
|
|
|
cache_for_variable_data_by_path: dict[str, VariableData] = {}
|
|
|
|
|
|
def clear_caches() -> None:
|
|
cache_for_variable_data_by_path.clear()
|
|
actually_variable_cache.clear()
|
|
|
|
|
|
attr_map = {(False, False): 'font_family', (True, False): 'bold_font', (False, True): 'italic_font', (True, True): 'bold_italic_font'}
|
|
|
|
|
|
class Event:
|
|
is_set: bool = False
|
|
|
|
|
|
class FamilyAxisValues:
|
|
regular_weight: float | None = None
|
|
regular_slant: float | None = None
|
|
regular_ital: float | None = None
|
|
regular_width: float | None = None
|
|
|
|
bold_weight: float | None = None
|
|
|
|
italic_slant: float | None = None
|
|
italic_ital: float | None = None
|
|
|
|
def get_wght(self, bold: bool, italic: bool) -> float | None:
|
|
return self.bold_weight if bold else self.regular_weight
|
|
|
|
def get_ital(self, bold: bool, italic: bool) -> float | None:
|
|
return self.italic_ital if italic else self.regular_ital
|
|
|
|
def get_slnt(self, bold: bool, italic: bool) -> float | None:
|
|
return self.italic_slant if italic else self.regular_slant
|
|
|
|
def get_wdth(self, bold: bool, italic: bool) -> float | None:
|
|
return self.regular_width
|
|
|
|
def get(self, tag: str, bold: bool, italic: bool) -> float | None:
|
|
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()
|
|
ans = cache_for_variable_data_by_path.get(d['path'])
|
|
if ans is None:
|
|
ans = cache_for_variable_data_by_path[d['path']] = face_from_descriptor(d).get_variable_data()
|
|
return ans
|
|
|
|
|
|
def get_variable_data_for_face(d: Face) -> VariableData:
|
|
path = d.path
|
|
if not path:
|
|
return d.get_variable_data()
|
|
ans = cache_for_variable_data_by_path.get(path)
|
|
if ans is None:
|
|
ans = cache_for_variable_data_by_path[path] = d.get_variable_data()
|
|
return ans
|
|
|
|
|
|
def find_best_match_in_candidates(
|
|
candidates: list[DescriptorVar], scorer: Scorer, is_medium_face: bool, ignore_face: DescriptorVar | None = None
|
|
) -> DescriptorVar | None:
|
|
if candidates:
|
|
for x in scorer.sorted_candidates(candidates):
|
|
if ignore_face is None or x != ignore_face:
|
|
return x
|
|
return None
|
|
|
|
|
|
def pprint(*a: Any, **kw: Any) -> None:
|
|
from pprint import pprint
|
|
pprint(*a, **kw)
|
|
|
|
|
|
def find_medium_variant(font: DescriptorVar) -> DescriptorVar:
|
|
font = font.copy()
|
|
vd = get_variable_data_for_descriptor(font)
|
|
for i, ns in enumerate(vd['named_styles']):
|
|
if ns['name'] == 'Regular':
|
|
set_named_style(ns['psname'] or ns['name'], font, vd)
|
|
return font
|
|
axis_values = {}
|
|
for i, ax in enumerate(vd['axes']):
|
|
tag = ax['tag']
|
|
for dax in vd['design_axes']:
|
|
if dax['tag'] == tag:
|
|
for x in dax['values']:
|
|
if x['format'] in (1, 2):
|
|
if x['name'] == 'Regular':
|
|
axis_values[tag] = x['value']
|
|
break
|
|
if axis_values:
|
|
set_axis_values(axis_values, font, vd)
|
|
return font
|
|
|
|
|
|
def get_bold_design_weight(dax: DesignAxis, ax: VariableAxis, regular_weight: float) -> float:
|
|
ans = regular_weight
|
|
candidates = []
|
|
for x in dax['values']:
|
|
if x['format'] in (1, 2):
|
|
if x['value'] > regular_weight:
|
|
candidates.append(x['value'])
|
|
if candidates:
|
|
ans = min(candidates)
|
|
return ans
|
|
|
|
|
|
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'):
|
|
keys = ('italic', 'oblique', 'slanted', 'slant') if italic else ('regular', 'normal', 'medium', 'upright')
|
|
else:
|
|
return default
|
|
priorities = {}
|
|
for x in dax['values']:
|
|
if x['format'] in (1, 2):
|
|
q = x['name'].lower()
|
|
try:
|
|
idx = keys.index(q)
|
|
except ValueError:
|
|
continue
|
|
priorities[x['value']] = idx
|
|
ans = default
|
|
if priorities:
|
|
ans = sorted(priorities, key=priorities.__getitem__)[0]
|
|
if bold and ax['tag'] == 'wght' and family_axis_values.regular_weight is not None and ans <= family_axis_values.regular_weight:
|
|
ans = get_bold_design_weight(dax, ax, family_axis_values.regular_weight)
|
|
return ans
|
|
|
|
|
|
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
|
|
# font file.
|
|
monospaced = is_monospace(medium)
|
|
unsorted = all_fonts_map(monospaced)['variable_map'][family_name_to_key(medium['family'])]
|
|
fonts = create_scorer(bold, italic, monospaced).sorted_candidates(unsorted)
|
|
vd = get_variable_data_for_descriptor(fonts[0])
|
|
ans = fonts[0].copy()
|
|
# now we need to specialise all axes in ans
|
|
axis_values = {}
|
|
dax_map = {dax['tag']: dax for dax in vd['design_axes']}
|
|
for ax in vd['axes']:
|
|
tag = ax['tag']
|
|
dax = dax_map.get(tag)
|
|
if dax is not None:
|
|
axis_values[tag] = get_design_value_for(dax, ax, bold, italic, family_axis_values)
|
|
if axis_values:
|
|
set_axis_values(axis_values, ans, vd)
|
|
return ans
|
|
|
|
|
|
def find_best_variable_face(spec: FontSpec, bold: bool, italic: bool, monospaced: bool, candidates: list[Descriptor]) -> Descriptor:
|
|
if spec.variable_name is not None:
|
|
q = spec.variable_name.lower()
|
|
for font in candidates:
|
|
vd = get_variable_data_for_descriptor(font)
|
|
if vd['variations_postscript_name_prefix'].lower() == q:
|
|
return font
|
|
if spec.style:
|
|
q = spec.style.lower()
|
|
for font in candidates:
|
|
vd = get_variable_data_for_descriptor(font)
|
|
for x in vd['named_styles']:
|
|
if x['psname'].lower() == q:
|
|
return font
|
|
for x in vd['named_styles']:
|
|
if x['name'].lower() == q:
|
|
return font
|
|
return create_scorer(bold, italic, monospaced).sorted_candidates(candidates)[0]
|
|
|
|
|
|
def get_fine_grained_font(
|
|
spec: FontSpec, bold: bool = False, italic: bool = False, family_axis_values: FamilyAxisValues = FamilyAxisValues(),
|
|
resolved_medium_font: Descriptor | None = None, monospaced: bool = True, match_is_more_specific_than_family: Event = Event()
|
|
) -> Descriptor:
|
|
font_map = all_fonts_map(monospaced)
|
|
is_medium_face = resolved_medium_font is None
|
|
scorer = create_scorer(bold, italic, monospaced)
|
|
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:
|
|
match_is_more_specific_than_family.is_set = True
|
|
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:
|
|
match_is_more_specific_than_family.is_set = True
|
|
return q
|
|
if spec.family:
|
|
key = family_name_to_key(spec.family)
|
|
# First look for a variable font
|
|
candidates = font_map['variable_map'].get(key, [])
|
|
if candidates:
|
|
q = candidates[0] if len(candidates) == 1 else find_best_variable_face(spec, bold, italic, monospaced, candidates)
|
|
q, applied = apply_variation_to_pattern(q, spec)
|
|
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, family_axis_values)
|
|
# Now look for any font
|
|
candidates = font_map['family_map'].get(key, [])
|
|
if candidates:
|
|
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
|
|
|
|
return find_last_resort_text_font(bold, italic, monospaced)
|
|
|
|
|
|
def apply_variation_to_pattern(pat: Descriptor, spec: FontSpec) -> tuple[Descriptor, bool]:
|
|
vd = face_from_descriptor(pat).get_variable_data()
|
|
pat = pat.copy()
|
|
if spec.style:
|
|
if set_named_style(spec.style, pat, vd):
|
|
return pat, True
|
|
tag_map, name_map = {}, {}
|
|
for i, ax in enumerate(vd['axes']):
|
|
tag_map[ax['tag']] = i
|
|
if ax['strid']:
|
|
name_map[ax['strid'].lower()] = ax['tag']
|
|
axis_values = {}
|
|
for axspec in spec.axes:
|
|
qname = axspec[0]
|
|
if qname in tag_map:
|
|
axis_values[qname] = axspec[1]
|
|
continue
|
|
tag = name_map.get(qname.lower())
|
|
if tag:
|
|
axis_values[tag] = axspec[1]
|
|
return pat, set_axis_values(axis_values, pat, vd)
|
|
|
|
|
|
def get_font_from_spec(
|
|
spec: FontSpec, bold: bool = False, italic: bool = False, family_axis_values: FamilyAxisValues = FamilyAxisValues(),
|
|
resolved_medium_font: Descriptor | None = None, match_is_more_specific_than_family: Event = Event()
|
|
) -> Descriptor:
|
|
if not spec.is_system:
|
|
ans = 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)
|
|
if spec.features:
|
|
ans = ans.copy()
|
|
ans['features'] = spec.features
|
|
return ans
|
|
family = spec.system or ''
|
|
if family == 'auto':
|
|
if bold or italic:
|
|
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, family_axis_values=family_axis_values)
|
|
if v is not None:
|
|
return v
|
|
else:
|
|
family = 'monospace'
|
|
return find_best_match(family, bold, italic, ignore_face=resolved_medium_font)
|
|
|
|
|
|
class FontFiles(TypedDict):
|
|
medium: Descriptor
|
|
bold: Descriptor
|
|
italic: Descriptor
|
|
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'] == path:
|
|
actually_variable_cache[path] = True
|
|
return True
|
|
actually_variable_cache[path] = False
|
|
return False
|
|
|
|
|
|
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)
|
|
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:
|
|
spec: FontSpec = getattr(opts, attr)
|
|
font = get_font_from_spec(spec, bold, italic, resolved_medium_font=medium_font, family_axis_values=family_axis_values)
|
|
# Set family axis values based on the values in font
|
|
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)
|
|
if spec.is_auto and not font.get('features') and medium_font.get('features'):
|
|
# Set font features based on medium face features
|
|
font = font.copy()
|
|
font['features'] = medium_font['features']
|
|
else:
|
|
font = medium_font
|
|
key = kd[(bold, italic)]
|
|
ans[key] = font
|
|
|
|
def apply_synthetic_matrix(font: Descriptor, bold: bool, italic: bool) -> Descriptor:
|
|
# fontconfig's FcFontList (used by find_best_match) omits FC_MATRIX from
|
|
# its object set, so a roman font found there carries no synthetic-italic
|
|
# shear and its "italic" renders upright. Fira Code is the case (it ships
|
|
# no italic), in both its static and variable builds. The italic intent
|
|
# exists only here at selection finalize, not at face construction, so
|
|
# recover the matrix now: for an italic slot whose chosen face is upright
|
|
# (no slant) and has no matrix yet, ask fc_match what fontconfig would do.
|
|
# fc_match returns a synthetic matrix only when there is no real italic to
|
|
# use (no italic face and no slanted named instance or variable slant
|
|
# axis); when a real italic exists it returns no matrix, so a font that is
|
|
# already italic, static or variable, is never double-slanted. Face construction applies the matrix
|
|
# via FT_Set_Transform; specialize_font_descriptor preserves it when the
|
|
# descriptor is sized for rendering. Only the matrix is taken, so
|
|
# selection is unchanged. Covers the four configured faces; fc_match
|
|
# re-matches by family name (see commit message).
|
|
if (italic and font['descriptor_type'] == 'fontconfig'
|
|
and not font.get('matrix') and not font.get('slant')):
|
|
from kitty.fast_data_types import FC_MONO
|
|
from kitty.fonts.fontconfig import fc_match
|
|
mtx = fc_match(font['family'], bold, italic, FC_MONO if is_monospace(font) else -1).get('matrix')
|
|
if mtx:
|
|
new_font = font.copy()
|
|
new_font['matrix'] = mtx
|
|
return new_font
|
|
return font
|
|
return {
|
|
'medium': ans['medium'], 'bold': ans['bold'],
|
|
'italic': apply_synthetic_matrix(ans['italic'], False, True),
|
|
'bi': apply_synthetic_matrix(ans['bi'], True, True),
|
|
}
|
|
|
|
|
|
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)
|
|
bd.update(b)
|
|
return ad == bd
|
|
|
|
|
|
def _get_named_style(axis_map: dict[str, float], vd: VariableData) -> NamedStyle | None:
|
|
defaults = {ax['tag']: ax['default'] for ax in vd['axes']}
|
|
for ns in vd['named_styles']:
|
|
if axis_values_are_equal(defaults, ns['axis_values'], axis_map):
|
|
return ns
|
|
return None
|
|
|
|
|
|
def get_named_style(face_or_descriptor: Face | Descriptor) -> NamedStyle | None:
|
|
if isinstance(face_or_descriptor, dict):
|
|
d: Descriptor = face_or_descriptor
|
|
vd = get_variable_data_for_descriptor(d)
|
|
if d['descriptor_type'] == 'fontconfig':
|
|
ns = d.get('named_style', -1)
|
|
if ns > -1 and ns < len(vd['named_styles']):
|
|
return vd['named_styles'][ns]
|
|
axis_map = {}
|
|
axes = vd['axes']
|
|
for i, val in enumerate(d.get('axes', ())):
|
|
if i < len(axes):
|
|
axis_map[axes[i]['tag']] = val
|
|
else:
|
|
axis_map = d.get('axis_map', {}).copy()
|
|
else:
|
|
face: Face = face_or_descriptor
|
|
vd = get_variable_data_for_face(face)
|
|
q = face.get_variation()
|
|
if q is None:
|
|
return None
|
|
axis_map = q
|
|
return _get_named_style(axis_map, vd)
|
|
|
|
|
|
def get_axis_map(face_or_descriptor: Face | Descriptor) -> dict[str, float]:
|
|
base_axis_map = {}
|
|
axis_map: dict[str, float] = {}
|
|
if isinstance(face_or_descriptor, dict):
|
|
d: Descriptor = face_or_descriptor
|
|
vd = get_variable_data_for_descriptor(d)
|
|
if d['descriptor_type'] == 'fontconfig':
|
|
ns = d.get('named_style', -1)
|
|
if ns > -1 and ns < len(vd['named_styles']):
|
|
base_axis_map = vd['named_styles'][ns]['axis_values'].copy()
|
|
axis_map = {}
|
|
axes = vd['axes']
|
|
for i, val in enumerate(d.get('axes', ())):
|
|
if i < len(axes):
|
|
axis_map[axes[i]['tag']] = val
|
|
|
|
else:
|
|
axis_map = d.get('axis_map', {}).copy()
|
|
else:
|
|
face: Face = face_or_descriptor
|
|
q = face.get_variation()
|
|
if q is not None:
|
|
axis_map = q
|
|
base_axis_map.update(axis_map)
|
|
return base_axis_map
|
|
|
|
|
|
def spec_for_face(family: str, face: Face) -> FontSpec:
|
|
v = face.get_variation()
|
|
features = tuple(map(ParsedFontFeature, face.applied_features().values()))
|
|
if v is None:
|
|
return FontSpec(family=family, postscript_name=face.postscript_name(), features=features)
|
|
vd = face.get_variable_data()
|
|
varname = vd['variations_postscript_name_prefix']
|
|
ns = get_named_style(face)
|
|
if ns is None:
|
|
axes = []
|
|
for key, val in get_axis_map(face).items():
|
|
axes.append((key, val))
|
|
return FontSpec(family=family, variable_name=varname, axes=tuple(axes), features=features)
|
|
return FontSpec(family=family, variable_name=varname, style=ns['psname'] or ns['name'], features=features)
|
|
|
|
|
|
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(name: str, d: Descriptor) -> None:
|
|
f = face_from_descriptor(d)
|
|
print(name, str(f))
|
|
features = f.get_features()
|
|
print(' Features :', features)
|
|
|
|
s('Medium :', ff['medium'])
|
|
print()
|
|
s('Bold :', ff['bold'])
|
|
print()
|
|
s('Italic :', ff['italic'])
|
|
print()
|
|
s('Bold-Italic:', ff['bi'])
|
|
|
|
|
|
def list_fonts(monospaced: bool = True) -> dict[str, list[dict[str, str]]]:
|
|
ans: dict[str, list[dict[str, str]]] = {}
|
|
for key, descriptors in all_fonts_map(monospaced)['family_map'].items():
|
|
entries = ans.setdefault(key, [])
|
|
for d in descriptors:
|
|
entries.append({'family': d['family'], 'psname': d['postscript_name'], 'path': d['path'], 'style': d['style']})
|
|
return ans
|
|
|
|
|
|
if __name__ == '__main__':
|
|
develop()
|