mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-19 06:54:58 +02:00
Get variable font selection working on coretext
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user