fonts: attach synthetic-italic FC_MATRIX to found roman faces

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.
This commit is contained in:
Strykar
2026-06-08 14:54:37 +05:30
parent 779a49acde
commit 8be2a10b29
3 changed files with 68 additions and 2 deletions

View File

@@ -173,6 +173,41 @@ class Selection(BaseTest):
self.ae(face_from_descriptor(ff['medium']).applied_features(), {'dlig': 'dlig', 'test': 'test=3'})
self.ae(face_from_descriptor(ff['bold']).applied_features(), {'dlig': 'dlig', 'test': 'test=3'})
def test_synthetic_italic_matrix(self):
# A roman-only font that find_best_match finds (e.g. Fira Code, which ships
# no italic face) must get fontconfig's synthetic-italic FC_MATRIX
# (90-synthetic.conf) attached, so its italic renders slanted rather than
# upright; real-italic faces must not. The shear value is fontconfig's, not
# ours, so assert the invariant (a non-identity matrix is present), not the
# exact tuple, for cross-config stability.
if is_macos:
self.skipTest('synthetic-italic FC_MATRIX is a fontconfig feature')
from kitty.fonts.fontconfig import FC_MONO, fc_match
names = set(all_fonts_map(True)['family_map']) | set(all_fonts_map(True)['variable_map'])
if family_name_to_key('fira code') not in names:
self.skipTest('Fira Code not installed')
# Probe fc_match directly so we can tell "environment lacks the rule" (skip)
# from "code did not attach the matrix" (fail).
if fc_match('Fira Code', False, True, FC_MONO).get('matrix') is None:
self.skipTest('fontconfig 90-synthetic.conf not active; no synthetic-italic matrix')
opts = Options()
opts.font_family = parse_font_spec('Fira Code')
ff = get_font_files(opts)
self.assertIsNone(ff['medium'].get('matrix')) # upright stays upright
mi = ff['italic'].get('matrix')
self.assertIsNotNone(mi) # roman, no italic -> sheared
self.assertNotEqual(mi[1], 0.0) # actually slanted, not identity
# Faces are built from a size-specialized descriptor at render time; the
# matrix must survive specialize_font_descriptor or the glyphs render
# upright despite the descriptor above being correct.
from kitty.fast_data_types import specialize_font_descriptor
sd = specialize_font_descriptor(dict(ff['italic']), 12.0, 96.0, 96.0)
self.ae(sd.get('matrix'), mi)
if family_name_to_key('liberation mono') in names: # real-italic control
opts.font_family = parse_font_spec('Liberation Mono')
self.assertIsNone(get_font_files(opts)['italic'].get('matrix'))
def block_helpers(s, sprites, cell_width, cell_height):
mr = {}
actual = b''