mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-08 14:18:26 +02:00
Better scoring for malformed fonts with weird weight ranges
This commit is contained in:
@@ -81,21 +81,58 @@ class Score(NamedTuple):
|
|||||||
width_score: int
|
width_score: int
|
||||||
|
|
||||||
|
|
||||||
|
class WeightRange(NamedTuple):
|
||||||
|
minimum: float = 99999
|
||||||
|
maximum: float = -99999
|
||||||
|
medium: float = -99999
|
||||||
|
bold: float = -99999
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_valid(self) -> bool:
|
||||||
|
return self.minimum != wr.minimum and self.maximum != wr.maximum and self.medium != wr.medium and self.bold != wr.bold
|
||||||
|
|
||||||
|
wr = WeightRange()
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
|
def weight_range_for_family(family: str) -> WeightRange:
|
||||||
|
faces = all_fonts_map(True)['family_map'].get(family_name_to_key(family), ())
|
||||||
|
mini, maxi, medium, bold = wr.minimum, wr.maximum, wr.medium, wr.bold
|
||||||
|
for face in faces:
|
||||||
|
w = face['weight']
|
||||||
|
mini, maxi = min(w, mini), max(w, maxi)
|
||||||
|
s = face['style'].lower()
|
||||||
|
if not s:
|
||||||
|
continue
|
||||||
|
s = s.split()[0]
|
||||||
|
if s == 'semibold':
|
||||||
|
bold = w
|
||||||
|
elif s == 'bold' and bold == wr.bold:
|
||||||
|
bold = w
|
||||||
|
elif s == 'medium':
|
||||||
|
medium = w
|
||||||
|
elif s == 'regular' and medium == wr.medium:
|
||||||
|
medium = w
|
||||||
|
return WeightRange(mini, maxi, medium, bold)
|
||||||
|
|
||||||
|
|
||||||
class CTScorer(Scorer):
|
class CTScorer(Scorer):
|
||||||
medium_weight: float = 0.
|
weight_range: Optional[WeightRange] = None
|
||||||
bold_weight: float = 0.3
|
|
||||||
|
|
||||||
def score(self, candidate: Descriptor) -> Score:
|
def score(self, candidate: Descriptor) -> Score:
|
||||||
assert candidate['descriptor_type'] == 'core_text'
|
assert candidate['descriptor_type'] == 'core_text'
|
||||||
variable_score = 0 if self.prefer_variable and candidate['variation'] is not None else 1
|
variable_score = 0 if self.prefer_variable and candidate['variation'] is not None else 1
|
||||||
bold_score = candidate['weight'] # -1 to 1 with 0 being normal
|
bold_score = candidate['weight'] # -1 to 1 with 0 being normal
|
||||||
if bold_score < 0: # thinner than normal, reject
|
if self.weight_range is None:
|
||||||
bold_score = 2.0
|
if bold_score < 0: # thinner than normal, reject
|
||||||
|
bold_score = 2.0
|
||||||
|
else:
|
||||||
|
if self.bold:
|
||||||
|
# prefer semibold=0.3 to full bold = 0.4
|
||||||
|
bold_score = abs(bold_score - 0.3)
|
||||||
else:
|
else:
|
||||||
if self.bold:
|
anchor = self.weight_range.bold if self.bold else self.weight_range.medium
|
||||||
# prefer semibold=0.3 to full bold = 0.4
|
bold_score = abs(bold_score - anchor)
|
||||||
bold_score = abs(bold_score - 0.3)
|
|
||||||
italic_score = candidate['slant'] # -1 to 1 with 0 being upright < 0 being backward slant, abs(slant) == 1 implies 30 deg rotation
|
italic_score = candidate['slant'] # -1 to 1 with 0 being upright < 0 being backward slant, abs(slant) == 1 implies 30 deg rotation
|
||||||
if self.italic:
|
if self.italic:
|
||||||
if italic_score < 0:
|
if italic_score < 0:
|
||||||
@@ -107,6 +144,12 @@ class CTScorer(Scorer):
|
|||||||
return Score(variable_score, bold_score + italic_score, monospace_match, 0 if is_regular_width else 1)
|
return Score(variable_score, bold_score + italic_score, monospace_match, 0 if is_regular_width else 1)
|
||||||
|
|
||||||
def sorted_candidates(self, candidates: Sequence[DescriptorVar], dump: bool = False) -> List[DescriptorVar]:
|
def sorted_candidates(self, candidates: Sequence[DescriptorVar], dump: bool = False) -> List[DescriptorVar]:
|
||||||
|
self.weight_range = None
|
||||||
|
families = {x['family'] for x in candidates}
|
||||||
|
if len(families) == 1:
|
||||||
|
wr = weight_range_for_family(next(iter(families)))
|
||||||
|
if wr.is_valid and wr.minimum < 0 and wr.maximum <= 0: # Operator Mono is an example of this craziness
|
||||||
|
self.weight_range = wr
|
||||||
candidates = sorted(candidates, key=self.score)
|
candidates = sorted(candidates, key=self.score)
|
||||||
if dump:
|
if dump:
|
||||||
print(self)
|
print(self)
|
||||||
@@ -115,7 +158,6 @@ class CTScorer(Scorer):
|
|||||||
print(x['postscript_name'], f'bold={x["bold"]}', f'italic={x["italic"]}', f'weight={x["weight"]:.2f}', f'slant={x["slant"]:.2f}')
|
print(x['postscript_name'], f'bold={x["bold"]}', f'italic={x["italic"]}', f'weight={x["weight"]:.2f}', f'slant={x["slant"]:.2f}')
|
||||||
print(self.score(x))
|
print(self.score(x))
|
||||||
print()
|
print()
|
||||||
self.medium_weight, self.bold_weight = CTScorer.medium_weight, CTScorer.bold_weight
|
|
||||||
return candidates
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
import sys
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import Dict, Generator, List, Literal, NamedTuple, Optional, Sequence, Tuple, cast
|
from typing import Dict, Generator, List, Literal, NamedTuple, Optional, Sequence, Tuple, cast
|
||||||
|
|
||||||
@@ -83,35 +84,79 @@ def fc_match(family: str, bold: bool, italic: bool, spacing: int = FC_MONO) -> F
|
|||||||
|
|
||||||
class Score(NamedTuple):
|
class Score(NamedTuple):
|
||||||
variable_score: int
|
variable_score: int
|
||||||
style_score: int
|
style_score: float
|
||||||
monospace_score: int
|
monospace_score: int
|
||||||
width_score: int
|
width_score: int
|
||||||
|
|
||||||
|
|
||||||
|
class WeightRange(NamedTuple):
|
||||||
|
minimum: int = sys.maxsize
|
||||||
|
maximum: int = -1
|
||||||
|
medium: int = -1
|
||||||
|
bold: int = -1
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_valid(self) -> bool:
|
||||||
|
return self.minimum != wr.minimum and self.maximum != wr.maximum and self.medium != wr.medium and self.bold != wr.bold
|
||||||
|
|
||||||
|
wr = WeightRange()
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
|
def weight_range_for_family(family: str) -> WeightRange:
|
||||||
|
faces = all_fonts_map(True)['family_map'].get(family_name_to_key(family), ())
|
||||||
|
mini, maxi, medium, bold = wr.minimum, wr.maximum, wr.medium, wr.bold
|
||||||
|
for face in faces:
|
||||||
|
w = face['weight']
|
||||||
|
mini, maxi = min(w, mini), max(w, maxi)
|
||||||
|
s = face['style'].lower()
|
||||||
|
if not s:
|
||||||
|
continue
|
||||||
|
s = s.split()[0]
|
||||||
|
if s == 'semibold':
|
||||||
|
bold = w
|
||||||
|
elif s == 'bold' and bold == wr.bold:
|
||||||
|
bold = w
|
||||||
|
elif s == 'medium':
|
||||||
|
medium = w
|
||||||
|
elif s == 'regular' and medium == wr.medium:
|
||||||
|
medium = w
|
||||||
|
return WeightRange(mini, maxi, medium, bold)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class FCScorer(Scorer):
|
class FCScorer(Scorer):
|
||||||
|
|
||||||
medium_weight: int = FC_WEIGHT_REGULAR
|
weight_range: Optional[WeightRange] = None
|
||||||
bold_weight: int = FC_WEIGHT_BOLD
|
|
||||||
|
|
||||||
def score(self, candidate: Descriptor) -> Score:
|
def score(self, candidate: Descriptor) -> Score:
|
||||||
assert candidate['descriptor_type'] == 'fontconfig'
|
assert candidate['descriptor_type'] == 'fontconfig'
|
||||||
variable_score = 0 if self.prefer_variable and candidate['variable'] else 1
|
variable_score = 0 if self.prefer_variable and candidate['variable'] else 1
|
||||||
bold_score = abs((self.bold_weight if self.bold else self.medium_weight) - candidate['weight'])
|
if self.weight_range is None:
|
||||||
|
bold_score = abs((FC_WEIGHT_BOLD if self.bold else FC_WEIGHT_REGULAR) - candidate['weight'])
|
||||||
|
else:
|
||||||
|
bold_score = abs((self.weight_range.bold if self.bold else self.weight_range.medium) - candidate['weight'])
|
||||||
italic_score = abs((FC_SLANT_ITALIC if self.italic else FC_SLANT_ROMAN) - candidate['slant'])
|
italic_score = abs((FC_SLANT_ITALIC if self.italic else FC_SLANT_ROMAN) - candidate['slant'])
|
||||||
monospace_match = 0
|
monospace_match = 0
|
||||||
if self.monospaced:
|
if self.monospaced:
|
||||||
monospace_match = 0 if candidate.get('spacing') == 'MONO' else 1
|
monospace_match = 0 if candidate.get('spacing') == 'MONO' else 1
|
||||||
width_score = abs(candidate['width'] - FC_WIDTH_NORMAL)
|
width_score = abs(candidate['width'] - FC_WIDTH_NORMAL)
|
||||||
return Score(variable_score, bold_score + italic_score, monospace_match, width_score)
|
return Score(variable_score, bold_score / 1000 + italic_score / 110, monospace_match, width_score)
|
||||||
|
|
||||||
def sorted_candidates(self, candidates: Sequence[DescriptorVar], dump: bool = False) -> List[DescriptorVar]:
|
def sorted_candidates(self, candidates: Sequence[DescriptorVar], dump: bool = False) -> List[DescriptorVar]:
|
||||||
|
self.weight_range = None
|
||||||
|
families = {x['family'] for x in candidates}
|
||||||
|
if len(families) == 1:
|
||||||
|
wr = weight_range_for_family(next(iter(families)))
|
||||||
|
if wr.is_valid and wr.maximum < 100: # Operator Mono is an example of this craziness
|
||||||
|
self.weight_range = wr
|
||||||
candidates = sorted(candidates, key=self.score)
|
candidates = sorted(candidates, key=self.score)
|
||||||
if dump:
|
if dump:
|
||||||
print(self)
|
print(self)
|
||||||
for x in candidates:
|
for x in candidates:
|
||||||
assert x['descriptor_type'] == 'fontconfig'
|
assert x['descriptor_type'] == 'fontconfig'
|
||||||
print(x['postscript_name'], f'weight={x["weight"]}', f'slant={x["slant"]}')
|
print(x['postscript_name'], f'weight={x["weight"]}', f'slant={x["slant"]}')
|
||||||
|
print(self.score(x))
|
||||||
print()
|
print()
|
||||||
return candidates
|
return candidates
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class Selection(BaseTest):
|
|||||||
if has_hack:
|
if has_hack:
|
||||||
both('hack', 'Hack-Regular', 'Hack-Bold', 'Hack-Italic', 'Hack-BoldItalic')
|
both('hack', 'Hack-Regular', 'Hack-Bold', 'Hack-Italic', 'Hack-BoldItalic')
|
||||||
if has_operator_mono:
|
if has_operator_mono:
|
||||||
both('operator mono', 'Hack-Regular', 'Hack-Bold', 'Hack-Italic', 'Hack-BoldItalic')
|
both('operator mono', 'OperatorMono-Medium', 'OperatorMono-Bold', 'OperatorMono-MediumItalic', 'OperatorMono-BoldItalic')
|
||||||
|
|
||||||
|
|
||||||
class Rendering(BaseTest):
|
class Rendering(BaseTest):
|
||||||
|
|||||||
Reference in New Issue
Block a user