Get variable font selection working on coretext

This commit is contained in:
Kovid Goyal
2024-05-10 21:50:24 +05:30
parent 69de414ba1
commit b44ffb9815
8 changed files with 228 additions and 70 deletions

View File

@@ -111,6 +111,45 @@ dealloc(CTFace* self) {
Py_TYPE(self)->tp_free((PyObject*)self); Py_TYPE(self)->tp_free((PyObject*)self);
} }
static const char*
tag_to_string(uint32_t tag, uint8_t bytes[5]) {
bytes[0] = (tag >> 24) & 0xff;
bytes[1] = (tag >> 16) & 0xff;
bytes[2] = (tag >> 8) & 0xff;
bytes[3] = (tag) & 0xff;
bytes[4] = 0;
return (const char*)bytes;
}
static uint32_t
string_to_tag(const uint8_t *bytes) {
return (((uint32_t)bytes[0]) << 24) | (((uint32_t)bytes[1]) << 16) | (((uint32_t)bytes[2]) << 8) | bytes[3];
}
static void
add_variation_pair(const void *key_, const void *value_, void *ctx) {
PyObject *ans = ctx;
CFNumberRef key = key_, value = value_;
uint32_t tag; double val;
if (!CFNumberGetValue(key, kCFNumberSInt32Type, &tag)) return;
if (!CFNumberGetValue(value, kCFNumberDoubleType, &val)) return;
uint8_t tag_string[5];
tag_to_string(tag, tag_string);
RAII_PyObject(pyval, PyFloat_FromDouble(val));
if (pyval) PyDict_SetItemString(ans, (const char*)tag_string, pyval);
}
static PyObject*
variation_to_python(CFDictionaryRef v) {
if (!v) { Py_RETURN_NONE; }
RAII_PyObject(ans, PyDict_New());
if (!ans) return NULL;
CFDictionaryApplyFunction(v, add_variation_pair, ans);
if (PyErr_Occurred()) return NULL;
Py_INCREF(ans); return ans;
}
static PyObject* static PyObject*
font_descriptor_to_python(CTFontDescriptorRef descriptor) { font_descriptor_to_python(CTFontDescriptorRef descriptor) {
RAII_PyObject(path, get_path_for_font_descriptor(descriptor)); RAII_PyObject(path, get_path_for_font_descriptor(descriptor));
@@ -127,9 +166,12 @@ font_descriptor_to_python(CTFontDescriptorRef descriptor) {
get_number(traits, kCTFontWeightTrait, weight, kCFNumberFloatType); get_number(traits, kCTFontWeightTrait, weight, kCFNumberFloatType);
get_number(traits, kCTFontWidthTrait, width, kCFNumberFloatType); get_number(traits, kCTFontWidthTrait, width, kCFNumberFloatType);
get_number(traits, kCTFontSlantTrait, slant, kCFNumberFloatType); get_number(traits, kCTFontSlantTrait, slant, kCFNumberFloatType);
RAII_CoreFoundation(CFDictionaryRef, variation, CTFontDescriptorCopyAttribute(descriptor, kCTFontVariationAttribute)); RAII_CoreFoundation(CFDictionaryRef, cf_variation, CTFontDescriptorCopyAttribute(descriptor, kCTFontVariationAttribute));
RAII_PyObject(variation, variation_to_python(cf_variation));
if (!variation) return NULL;
#undef get_number #undef get_number
PyObject *ans = Py_BuildValue("{ss sOsOsOsOsO sOsOsOsOsOsOsO sfsfsfsk}", PyObject *ans = Py_BuildValue("{ss sOsOsOsOsO sOsOsOsOsOsOsO sfsfsfsk}",
"descriptor_type", "core_text", "descriptor_type", "core_text",
@@ -141,7 +183,7 @@ font_descriptor_to_python(CTFontDescriptorRef descriptor) {
"expanded", (symbolic_traits & kCTFontExpandedTrait) != 0 ? Py_True : Py_False, "expanded", (symbolic_traits & kCTFontExpandedTrait) != 0 ? Py_True : Py_False,
"condensed", (symbolic_traits & kCTFontCondensedTrait) != 0 ? Py_True : Py_False, "condensed", (symbolic_traits & kCTFontCondensedTrait) != 0 ? Py_True : Py_False,
"color_glyphs", (symbolic_traits & kCTFontColorGlyphsTrait) != 0 ? Py_True : Py_False, "color_glyphs", (symbolic_traits & kCTFontColorGlyphsTrait) != 0 ? Py_True : Py_False,
"variable", variation ? Py_True : Py_False, "variation", variation,
"weight", weight, "width", width, "slant", slant, "traits", symbolic_traits "weight", weight, "width", width, "slant", slant, "traits", symbolic_traits
); );
@@ -151,7 +193,7 @@ font_descriptor_to_python(CTFontDescriptorRef descriptor) {
static CTFontDescriptorRef static CTFontDescriptorRef
font_descriptor_from_python(PyObject *src) { font_descriptor_from_python(PyObject *src) {
CTFontSymbolicTraits symbolic_traits = 0; CTFontSymbolicTraits symbolic_traits = 0;
NSMutableDictionary *attrs = [NSMutableDictionary dictionary]; RAII_CoreFoundation(CFMutableDictionaryRef, ans, CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
PyObject *t = PyDict_GetItemString(src, "traits"); PyObject *t = PyDict_GetItemString(src, "traits");
if (t == NULL) { if (t == NULL) {
symbolic_traits = ( symbolic_traits = (
@@ -161,19 +203,33 @@ font_descriptor_from_python(PyObject *src) {
} else { } else {
symbolic_traits = PyLong_AsUnsignedLong(t); symbolic_traits = PyLong_AsUnsignedLong(t);
} }
NSDictionary *traits = @{(id)kCTFontSymbolicTrait:[NSNumber numberWithUnsignedInt:symbolic_traits]}; RAII_CoreFoundation(CFNumberRef, cf_symbolic_traits, CFNumberCreate(NULL, kCFNumberSInt32Type, &symbolic_traits));
attrs[(id)kCTFontTraitsAttribute] = traits; CFTypeRef keys[] = { kCTFontSymbolicTrait };
CFTypeRef values[] = { cf_symbolic_traits };
RAII_CoreFoundation(CFDictionaryRef, traits, CFDictionaryCreate(NULL, keys, values, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
CFDictionaryAddValue(ans, kCTFontTraitsAttribute, traits);
#define SET(x, attr) \ #define SET(x, attr) if ((t = PyDict_GetItemString(src, #x))) { \
t = PyDict_GetItemString(src, #x); \ RAII_CoreFoundation(CFStringRef, cs, CFStringCreateWithCString(NULL, PyUnicode_AsUTF8(t), kCFStringEncodingUTF8)); \
if (t) attrs[(id)attr] = @(PyUnicode_AsUTF8(t)); CFDictionaryAddValue(ans, attr, cs); }
SET(family, kCTFontFamilyNameAttribute); SET(family, kCTFontFamilyNameAttribute);
SET(style, kCTFontStyleNameAttribute); SET(style, kCTFontStyleNameAttribute);
SET(postscript_name, kCTFontNameAttribute); SET(postscript_name, kCTFontNameAttribute);
#undef SET #undef SET
if ((t = PyDict_GetItemString(src, "axis_map"))) {
return CTFontDescriptorCreateWithAttributes((CFDictionaryRef) attrs); RAII_CoreFoundation(CFMutableDictionaryRef, axis_map, CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
PyObject *key, *value; Py_ssize_t pos = 0;
while (PyDict_Next(t, &pos, &key, &value)) {
double val = PyFloat_AS_DOUBLE(value);
uint32_t tag = string_to_tag((const uint8_t*)PyUnicode_AsUTF8(key));
RAII_CoreFoundation(CFNumberRef, cf_tag, CFNumberCreate(NULL, kCFNumberSInt32Type, &tag));
RAII_CoreFoundation(CFNumberRef, cf_val, CFNumberCreate(NULL, kCFNumberDoubleType, &val));
CFDictionaryAddValue(axis_map, cf_tag, cf_val);
}
CFDictionaryAddValue(ans, kCTFontVariationAttribute, axis_map);
}
return CTFontDescriptorCreateWithAttributes(ans);
} }
static CTFontCollectionRef all_fonts_collection_data = NULL; static CTFontCollectionRef all_fonts_collection_data = NULL;

View File

@@ -397,7 +397,7 @@ class FontConfigPattern(TypedDict):
# The following two are used by C code to get a face from the pattern # The following two are used by C code to get a face from the pattern
named_style: NotRequired[int] named_style: NotRequired[int]
axes: NotRequired[List[float]] axes: NotRequired[Tuple[float, ...]]
def fc_list(spacing: int = -1, allow_bitmapped_fonts: bool = False, only_variable: bool = False) -> Tuple[FontConfigPattern, ...]: def fc_list(spacing: int = -1, allow_bitmapped_fonts: bool = False, only_variable: bool = False) -> Tuple[FontConfigPattern, ...]:
@@ -444,15 +444,14 @@ class CoreTextFont(TypedDict):
condensed: bool condensed: bool
color_glyphs: bool color_glyphs: bool
monospace: bool monospace: bool
variable: bool variation: Optional[Dict[str, float]]
weight: float weight: float
width: float width: float
slant: float slant: float
traits: int traits: int
# The following two are used by C code to get a face from the pattern # The following is used by C code to get a face from the pattern
named_style: NotRequired[int] axis_map: NotRequired[Dict[str, float]]
axes: NotRequired[List[float]]
class CTFace: class CTFace:

View File

@@ -138,6 +138,7 @@ class FontSpec(NamedTuple):
full_name: str = '' full_name: str = ''
system: str = '' system: str = ''
axes: Tuple[Tuple[str, float], ...] = () axes: Tuple[Tuple[str, float], ...] = ()
variable_name: str = ''
@property @property
def is_system(self) -> bool: def is_system(self) -> bool:

View File

@@ -23,14 +23,35 @@ if TYPE_CHECKING:
def find_last_resort_text_font(bold: bool = False, italic: bool = False, monospaced: bool = True) -> Descriptor: ... def find_last_resort_text_font(bold: bool = False, italic: bool = False, monospaced: bool = True) -> Descriptor: ...
def face_from_descriptor(descriptor: Descriptor) -> Face: ... def face_from_descriptor(descriptor: Descriptor) -> Face: ...
def is_monospace(descriptor: Descriptor) -> bool: ... 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: ...
else: else:
FontCollectionMapType = FontMap = None FontCollectionMapType = FontMap = None
if is_macos: if is_macos:
from kitty.fast_data_types import CTFace as Face 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, is_monospace from kitty.fonts.core_text import (
all_fonts_map,
create_scorer,
find_best_match,
find_last_resort_text_font,
is_monospace,
is_variable,
set_axis_values,
set_named_style,
)
else: else:
from kitty.fast_data_types import Face 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, is_monospace from kitty.fonts.fontconfig import (
all_fonts_map,
create_scorer,
find_best_match,
find_last_resort_text_font,
is_monospace,
is_variable,
set_axis_values,
set_named_style,
)
def face_from_descriptor(descriptor: Descriptor) -> Face: return Face(descriptor=descriptor) def face_from_descriptor(descriptor: Descriptor) -> Face: return Face(descriptor=descriptor)
@@ -70,9 +91,9 @@ def find_medium_variant(font: Descriptor) -> Descriptor:
vd = get_variable_data_for_descriptor(font) vd = get_variable_data_for_descriptor(font)
for i, ns in enumerate(vd['named_styles']): for i, ns in enumerate(vd['named_styles']):
if ns['name'] == 'Regular': if ns['name'] == 'Regular':
font['named_style'] = i set_named_style(ns['psname'], font, vd)
return font return font
font['axes'] = axes = [ax['default'] for ax in vd['axes']] axis_values = {}
for i, ax in enumerate(vd['axes']): for i, ax in enumerate(vd['axes']):
tag = ax['tag'] tag = ax['tag']
for dax in vd['design_axes']: for dax in vd['design_axes']:
@@ -80,8 +101,10 @@ def find_medium_variant(font: Descriptor) -> Descriptor:
for x in dax['values']: for x in dax['values']:
if x['format'] in (1, 2): if x['format'] in (1, 2):
if x['name'] == 'Regular': if x['name'] == 'Regular':
axes[i] = x['value'] axis_values[tag] = x['value']
break break
if axis_values:
set_axis_values(axis_values, font, vd)
return font return font
@@ -100,28 +123,51 @@ def get_design_value_for(dax: DesignAxis, default: float, bold: bool, italic: bo
def find_bold_italic_variant(medium: Descriptor, bold: bool, italic: bool) -> Descriptor: def find_bold_italic_variant(medium: Descriptor, bold: bool, italic: bool) -> Descriptor:
key = family_name_to_key(medium['family'])
monospaced = is_monospace(medium)
# we first pick the best font file for bold/italic if there are more than # 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 # 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 # weights in each, so we rely on the OS font matcher to give us the best
# font file. # font file.
fonts = all_fonts_map(monospaced)['variable_map'][key] monospaced = is_monospace(medium)
fonts = all_fonts_map(monospaced)['variable_map'][family_name_to_key(medium['family'])]
scorer = create_scorer(bold, italic, monospaced) scorer = create_scorer(bold, italic, monospaced)
fonts.sort(key=scorer) fonts.sort(key=scorer)
vd = get_variable_data_for_descriptor(fonts[0])
ans = fonts[0].copy() ans = fonts[0].copy()
# now we need to specialise all axes in ans # now we need to specialise all axes in ans
vd = get_variable_data_for_descriptor(ans) axis_values = {}
ans['axes'] = axes = [ax['default'] for ax in vd['axes']]
for i, ax in enumerate(vd['axes']): for i, ax in enumerate(vd['axes']):
tag = ax['tag'] tag = ax['tag']
for dax in vd['design_axes']: for dax in vd['design_axes']:
if dax['tag'] == tag: if dax['tag'] == tag:
axes[i] = get_design_value_for(dax, axes[i], bold, italic) axis_values[tag] = get_design_value_for(dax, ax['default'], bold, italic)
break break
if axis_values:
set_axis_values(axis_values, ans, vd)
return ans return ans
def find_best_variable_face(spec: FontSpec, bold: bool, italic: bool, monospaced: bool, candidates: List[Descriptor]) -> Descriptor:
if spec.variable_name:
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
scorer = create_scorer(bold, italic, monospaced)
candidates.sort(key=scorer)
return candidates[0]
def get_fine_grained_font( def get_fine_grained_font(
spec: FontSpec, bold: bool = False, italic: bool = False, medium_font_spec: FontSpec = FontSpec(), spec: FontSpec, bold: bool = False, italic: bool = False, medium_font_spec: FontSpec = FontSpec(),
resolved_medium_font: Optional[Descriptor] = None, monospaced: bool = True resolved_medium_font: Optional[Descriptor] = None, monospaced: bool = True
@@ -142,8 +188,7 @@ def get_fine_grained_font(
# First look for a variable font # First look for a variable font
candidates = font_map['variable_map'].get(key, []) candidates = font_map['variable_map'].get(key, [])
if candidates: if candidates:
candidates.sort(key=scorer) q = candidates[0] if len(candidates) == 1 else find_best_variable_face(spec, bold, italic, monospaced, candidates)
q = candidates[0]
q, applied = apply_variation_to_pattern(q, spec) q, applied = apply_variation_to_pattern(q, spec)
if applied: if applied:
return q return q
@@ -162,42 +207,26 @@ def get_fine_grained_font(
def apply_variation_to_pattern(pat: Descriptor, spec: FontSpec) -> Tuple[Descriptor, bool]: def apply_variation_to_pattern(pat: Descriptor, spec: FontSpec) -> Tuple[Descriptor, bool]:
if not pat['variable']:
return pat, False
vd = face_from_descriptor(pat).get_variable_data() vd = face_from_descriptor(pat).get_variable_data()
pat = pat.copy()
if spec.style: if spec.style:
q = spec.style.lower() if set_named_style(spec.style, pat, vd):
for i, ns in enumerate(vd['named_styles']): return pat, True
if ns['psname'].lower() == q:
pat = pat.copy()
pat['named_style'] = i
return pat, True
else:
for i, ns in enumerate(vd['named_styles']):
if ns['name'].lower() == q:
pat = pat.copy()
pat['named_style'] = i
return pat, True
tag_map, name_map = {}, {} tag_map, name_map = {}, {}
axes = [ax['default'] for ax in vd['axes']]
for i, ax in enumerate(vd['axes']): for i, ax in enumerate(vd['axes']):
tag_map[ax['tag']] = i tag_map[ax['tag']] = i
if ax['strid']: if ax['strid']:
name_map[ax['strid'].lower()] = i name_map[ax['strid'].lower()] = ax['tag']
changed = False axis_values = {}
for axspec in spec.axes: for axspec in spec.axes:
qname = axspec[0] qname = axspec[0]
axis = tag_map.get(qname) if qname in tag_map:
if axis is None: axis_values[qname] = axspec[1]
axis = name_map.get(qname.lower()) continue
if axis is not None: tag = name_map.get(qname.lower())
axes[axis] = axspec[1] if tag:
changed = True axis_values[tag] = axspec[1]
if changed: return pat, set_axis_values(axis_values, pat, vd)
pat = pat.copy()
pat['axes'] = axes
return pat, changed
def get_font_from_spec( def get_font_from_spec(
@@ -211,7 +240,7 @@ def get_font_from_spec(
if bold or italic: if bold or italic:
assert resolved_medium_font is not None assert resolved_medium_font is not None
family = resolved_medium_font['family'] family = resolved_medium_font['family']
if resolved_medium_font['variable']: if is_variable(resolved_medium_font):
v = find_bold_italic_variant(resolved_medium_font, bold, italic) v = find_bold_italic_variant(resolved_medium_font, bold, italic)
if v is not None: if v is not None:
return v return v

View File

@@ -1,7 +1,10 @@
#!/usr/bin/env python #!/usr/bin/env python
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import itertools
import operator
import re import re
from collections import defaultdict
from functools import lru_cache from functools import lru_cache
from typing import Dict, Generator, Iterable, List, Optional, Tuple from typing import Dict, Generator, Iterable, List, Optional, Tuple
@@ -11,7 +14,7 @@ from kitty.options.types import Options
from kitty.typing import CoreTextFont from kitty.typing import CoreTextFont
from kitty.utils import log_error from kitty.utils import log_error
from . import Descriptor, ListedFont, Score, Scorer from . import Descriptor, ListedFont, Score, Scorer, VariableData
attr_map = {(False, False): 'font_family', attr_map = {(False, False): 'font_family',
(True, False): 'bold_font', (True, False): 'bold_font',
@@ -24,6 +27,7 @@ FontMap = Dict[str, Dict[str, List[CoreTextFont]]]
def create_font_map(all_fonts: Iterable[CoreTextFont]) -> FontMap: def create_font_map(all_fonts: Iterable[CoreTextFont]) -> FontMap:
ans: FontMap = {'family_map': {}, 'ps_map': {}, 'full_map': {}, 'variable_map': {}} ans: FontMap = {'family_map': {}, 'ps_map': {}, 'full_map': {}, 'variable_map': {}}
vmap: Dict[str, List[CoreTextFont]] = defaultdict(list)
for x in all_fonts: for x in all_fonts:
f = (x['family'] or '').lower() f = (x['family'] or '').lower()
s = (x['style'] or '').lower() s = (x['style'] or '').lower()
@@ -31,8 +35,19 @@ def create_font_map(all_fonts: Iterable[CoreTextFont]) -> FontMap:
ans['family_map'].setdefault(f, []).append(x) ans['family_map'].setdefault(f, []).append(x)
ans['ps_map'].setdefault(ps, []).append(x) ans['ps_map'].setdefault(ps, []).append(x)
ans['full_map'].setdefault(f'{f} {s}', []).append(x) ans['full_map'].setdefault(f'{f} {s}', []).append(x)
if x['variable']: if x['variation'] is not None:
ans['variable_map'].setdefault(f, []).append(x) vmap[f].append(x)
# CoreText makes a separate descriptor for each named style in each
# variable font file. Keep only the default style descriptor, which has an
# empty variation dictionary. If no default exists, pick the one with the
# smallest variation dictionary size.
keyfunc = operator.itemgetter('path')
for k, v in vmap.items():
v.sort(key=keyfunc)
uniq_per_path = []
for _, g in itertools.groupby(v, keyfunc):
uniq_per_path.append(sorted(g, key=lambda x: len(x['variation'] or ()))[0])
ans['variable_map'][k] = uniq_per_path
return ans return ans
@@ -45,6 +60,10 @@ def is_monospace(descriptor: CoreTextFont) -> bool:
return descriptor['monospace'] return descriptor['monospace']
def is_variable(descriptor: CoreTextFont) -> bool:
return descriptor['variation'] is not None
def list_fonts() -> Generator[ListedFont, None, None]: def list_fonts() -> Generator[ListedFont, None, None]:
for fd in coretext_all_fonts(False): for fd in coretext_all_fonts(False):
f = fd['family'] f = fd['family']
@@ -53,14 +72,14 @@ def list_fonts() -> Generator[ListedFont, None, None]:
if not fn: if not fn:
fn = f'{f} {fd["style"]}'.strip() fn = f'{f} {fd["style"]}'.strip()
yield {'family': f, 'full_name': fn, 'postscript_name': fd['postscript_name'] or '', 'is_monospace': fd['monospace'], yield {'family': f, 'full_name': fn, 'postscript_name': fd['postscript_name'] or '', 'is_monospace': fd['monospace'],
'is_variable': fd['variable'], 'descriptor': fd, 'style': fd['style']} 'is_variable': is_variable(fd), 'descriptor': fd, 'style': fd['style']}
def create_scorer(bold: bool = False, italic: bool = False, monospaced: bool = True, prefer_variable: bool = False) -> Scorer: def create_scorer(bold: bool = False, italic: bool = False, monospaced: bool = True, prefer_variable: bool = False) -> Scorer:
def score(candidate: Descriptor) -> Score: def score(candidate: Descriptor) -> Score:
assert candidate['descriptor_type'] == 'core_text' assert candidate['descriptor_type'] == 'core_text'
variable_score = 0 if prefer_variable and candidate['variable'] else 1 variable_score = 0 if prefer_variable and candidate['variation'] is not None else 1
style_match = 1 if candidate['bold'] == bold and candidate[ style_match = 1 if candidate['bold'] == bold and candidate[
'italic' 'italic'
] == italic else 0 ] == italic else 0
@@ -161,3 +180,26 @@ def prune_family_group(g: List[ListedFont]) -> List[ListedFont]:
return True return True
return False return False
return [x for x in g if is_ok(descriptor(x))] return [x for x in g if is_ok(descriptor(x))]
def set_axis_values(tag_map: Dict[str, float], font: CoreTextFont, vd: VariableData) -> bool:
known_axes = {ax['tag'] for ax in vd['axes']}
previous = font.get('axis_map', {})
new = previous.copy()
for tag in known_axes:
val = tag_map.get(tag)
if val is not None:
new[tag] = val
font['axis_map'] = new
return new != previous
def set_named_style(name: str, font: CoreTextFont, vd: VariableData) -> bool:
q = name.lower()
for i, ns in enumerate(vd['named_styles']):
if ns['psname'].lower() == q:
return set_axis_values(ns['axis_values'], font, vd)
for i, ns in enumerate(vd['named_styles']):
if ns['name'].lower() == q:
return set_axis_values(ns['axis_values'], font, vd)
return False

View File

@@ -17,7 +17,7 @@ from kitty.fast_data_types import (
from kitty.fast_data_types import fc_match as fc_match_impl from kitty.fast_data_types import fc_match as fc_match_impl
from kitty.typing import FontConfigPattern from kitty.typing import FontConfigPattern
from . import Descriptor, ListedFont, Score, Scorer, family_name_to_key from . import Descriptor, ListedFont, Score, Scorer, VariableData, family_name_to_key
FontCollectionMapType = Literal['family_map', 'ps_map', 'full_map', 'variable_map'] FontCollectionMapType = Literal['family_map', 'ps_map', 'full_map', 'variable_map']
FontMap = Dict[FontCollectionMapType, Dict[str, List[FontConfigPattern]]] FontMap = Dict[FontCollectionMapType, Dict[str, List[FontConfigPattern]]]
@@ -54,6 +54,10 @@ def is_monospace(descriptor: FontConfigPattern) -> bool:
return descriptor['spacing'] in ('MONO', 'DUAL') return descriptor['spacing'] in ('MONO', 'DUAL')
def is_variable(descriptor: FontConfigPattern) -> bool:
return descriptor['variable']
def list_fonts(only_variable: bool = False) -> Generator[ListedFont, None, None]: def list_fonts(only_variable: bool = False) -> Generator[ListedFont, None, None]:
for fd in fc_list(only_variable=only_variable): for fd in fc_list(only_variable=only_variable):
f = fd.get('family') f = fd.get('family')
@@ -65,7 +69,7 @@ def list_fonts(only_variable: bool = False) -> Generator[ListedFont, None, None]
fn = f'{f} {fd.get("style", "")}'.strip() fn = f'{f} {fd.get("style", "")}'.strip()
yield { yield {
'family': f, 'full_name': fn, 'postscript_name': str(fd.get('postscript_name', '')), 'family': f, 'full_name': fn, 'postscript_name': str(fd.get('postscript_name', '')),
'is_monospace': is_monospace(fd), 'descriptor': fd, 'is_variable': fd.get('variable', False), 'is_monospace': is_monospace(fd), 'descriptor': fd, 'is_variable': is_variable(fd),
'style': fd['style'], 'style': fd['style'],
} }
@@ -161,3 +165,29 @@ def prune_family_group(g: List[ListedFont]) -> List[ListedFont]:
return d['variable'] or d['path'] not in variable_paths return d['variable'] or d['path'] not in variable_paths
return [x for x in g if is_ok(descriptor(x))] return [x for x in g if is_ok(descriptor(x))]
def set_named_style(name: str, font: FontConfigPattern, vd: VariableData) -> bool:
q = name.lower()
for i, ns in enumerate(vd['named_styles']):
if ns['psname'].lower() == q:
font['named_style'] = i
return True
for i, ns in enumerate(vd['named_styles']):
if ns['name'].lower() == q:
font['named_style'] = i
return True
return False
def set_axis_values(tag_map: Dict[str, float], font: FontConfigPattern, vd: VariableData) -> bool:
axes = list(font.get('axes', ())) or [ax['default'] for ax in vd['axes']]
changed = False
for i, ax in enumerate(vd['axes']):
val = tag_map.get(ax['tag'])
if val is not None:
changed = True
axes[i] = val
if changed:
font['axes'] = tuple(axes)
return changed

View File

@@ -288,15 +288,16 @@ face_from_descriptor(PyObject *descriptor, FONTS_DATA_HANDLE fg) {
if ((error = FT_Set_Named_Instance(self->face, index + 1))) return set_load_error(path, error); if ((error = FT_Set_Named_Instance(self->face, index + 1))) return set_load_error(path, error);
} }
PyObject *axes = PyDict_GetItemString(descriptor, "axes"); PyObject *axes = PyDict_GetItemString(descriptor, "axes");
if (axes && PyList_GET_SIZE(axes)) { Py_ssize_t sz;
RAII_ALLOC(FT_Fixed, coords, malloc(sizeof(FT_Fixed) * PyList_GET_SIZE(axes))); if (axes && (sz = PyTuple_GET_SIZE(axes))) {
for (Py_ssize_t i = 0; i < PyList_GET_SIZE(axes); i++) { RAII_ALLOC(FT_Fixed, coords, malloc(sizeof(FT_Fixed) * sz));
PyObject *t = PyList_GET_ITEM(axes, i); for (Py_ssize_t i = 0; i < sz; i++) {
PyObject *t = PyTuple_GET_ITEM(axes, i);
double val = PyFloat_AsDouble(t); double val = PyFloat_AsDouble(t);
if (PyErr_Occurred()) return NULL; if (PyErr_Occurred()) return NULL;
coords[i] = (FT_Fixed)(val * 65536.0); 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); if ((error = FT_Set_Var_Design_Coordinates(self->face, sz, coords))) return set_load_error(path, error);
} }
} }
Py_XINCREF(retval); Py_XINCREF(retval);

View File

@@ -1402,7 +1402,7 @@ def parse_font_spec(spec: str) -> FontSpec:
k, sep, v = item.partition('=') k, sep, v = item.partition('=')
if sep != '=': if sep != '=':
raise ValueError(f'The font specification: {spec} is not valid as {item} does not contain an =') raise ValueError(f'The font specification: {spec} is not valid as {item} does not contain an =')
if k in ('family', 'style', 'full_name', 'postscript_name'): if k in ('family', 'style', 'full_name', 'postscript_name', 'variable_name'):
defined[k] = v defined[k] = v
else: else:
try: try: