Read FC_MATRIX from fontconfig

pattern_as_dict() in fontconfig.c never read FC_MATRIX, so any per-font
transform set by fontconfig was silently dropped. fontconfig ships a
default rule (90-synthetic.conf) that applies a slant matrix to any
roman-only font when italic is requested, which is why italic CJK has
been rendering upright in kitty.

Read the matrix, carry it on the descriptor as a 4-tuple of doubles,
apply it once in face_from_descriptor() via FT_Set_Transform, also
inform HarfBuzz via hb_font_set_synthetic_slant + hb_ft_font_changed
so shaping reflects the slanted rendering. Extend
face_equals_descriptor() to compare the matrix so the per-FontGroup
fallback cache returns the right face when upright and italic share a
font file.

The FT transform is sticky on the face, so subsequent FT_Load_Glyph
calls inherit it with no per-call overhead, and the per-Face glyph
atlas cache stays correct because the matrix is set at init and never
changes. Pure shears (xx=1, yy=1) preserve horizontal advance and do
not disturb monospace cell width.

The HB synthetic_slant call is gated on HB_VERSION_ATLEAST(4,0,0) since
setup.py allows down to 1.5.0. hb_ft_font_changed runs unconditionally
to invalidate any populated caches.

Refs #9857, #9700.
This commit is contained in:
Strykar 2026-05-10 02:48:29 +05:30
parent 21d8b2bcc0
commit b3e7c3e717
No known key found for this signature in database
GPG key ID: E4A8486E91D492EA
2 changed files with 65 additions and 0 deletions

View file

@ -41,6 +41,7 @@ static struct {PyObject *face, *descriptor;} builtin_nerd_font = {0};
#define FcPatternAddInteger dynamically_loaded_fc_symbol.PatternAddInteger
#define FcPatternCreate dynamically_loaded_fc_symbol.PatternCreate
#define FcPatternGetBool dynamically_loaded_fc_symbol.PatternGetBool
#define FcPatternGetMatrix dynamically_loaded_fc_symbol.PatternGetMatrix
#define FcPatternAddCharSet dynamically_loaded_fc_symbol.PatternAddCharSet
#define FcConfigAppFontAddFile dynamically_loaded_fc_symbol.ConfigAppFontAddFile
@ -66,6 +67,7 @@ static struct {
FcBool (*PatternAddInteger) (FcPattern *p, const char *object, int i);
FcPattern * (*PatternCreate) (void);
FcResult (*PatternGetBool) (const FcPattern *p, const char *object, int n, FcBool *b);
FcResult (*PatternGetMatrix) (const FcPattern *p, const char *object, int n, FcMatrix **m);
FcBool (*PatternAddCharSet) (FcPattern *p, const char *object, const FcCharSet *c);
FcBool (*ConfigAppFontAddFile) (FcConfig *config, const FcChar8 *file);
} dynamically_loaded_fc_symbol = {0};
@ -117,6 +119,7 @@ load_fontconfig_lib(void) {
LOAD_FUNC(PatternAddInteger);
LOAD_FUNC(PatternCreate);
LOAD_FUNC(PatternGetBool);
LOAD_FUNC(PatternGetMatrix);
LOAD_FUNC(PatternAddCharSet);
LOAD_FUNC(ConfigAppFontAddFile);
}
@ -210,6 +213,13 @@ pattern_as_dict(FcPattern *pat) {
B(FC_OUTLINE, outline);
B(FC_COLOR, color);
E(FC_SPACING, spacing, pyspacing);
{
FcMatrix *mtx = NULL;
if (FcPatternGetMatrix(pat, FC_MATRIX, 0, &mtx) == FcResultMatch && mtx) {
RAII_PyObject(t, Py_BuildValue("(dddd)", mtx->xx, mtx->xy, mtx->yx, mtx->yy));
if (!t || PyDict_SetItemString(ans, "matrix", t) != 0) return NULL;
}
}
Py_INCREF(ans);
return ans;

View file

@ -66,6 +66,8 @@ typedef struct {
PyObject *name_lookup_table;
FontFeatures font_features;
unsigned short dark_palette_index, light_palette_index, palettes_scanned;
FT_Matrix matrix;
bool has_matrix;
} Face;
PyTypeObject Face_Type;
@ -284,6 +286,29 @@ set_load_error(const char *path, int error) {
return NULL;
}
static bool
read_matrix_from_descriptor(PyObject *descriptor, FT_Matrix *out, bool *out_has) {
*out_has = false;
PyObject *mt = PyDict_GetItemString(descriptor, "matrix");
if (!mt || !PyTuple_Check(mt) || PyTuple_GET_SIZE(mt) != 4) return true;
double v[4];
for (int i = 0; i < 4; i++) {
v[i] = PyFloat_AsDouble(PyTuple_GET_ITEM(mt, i));
if (PyErr_Occurred()) return false;
if (!isfinite(v[i])) {
PyErr_SetString(PyExc_ValueError, "matrix contains non-finite value");
return false;
}
}
if (v[0] == 1.0 && v[1] == 0.0 && v[2] == 0.0 && v[3] == 1.0) return true;
out->xx = (FT_Fixed)(v[0] * 0x10000);
out->xy = (FT_Fixed)(v[1] * 0x10000);
out->yx = (FT_Fixed)(v[2] * 0x10000);
out->yy = (FT_Fixed)(v[3] * 0x10000);
*out_has = true;
return true;
}
bool
face_equals_descriptor(PyObject *face_, PyObject *descriptor) {
Face *face = (Face*)face_;
@ -292,6 +317,13 @@ face_equals_descriptor(PyObject *face_, PyObject *descriptor) {
if (PyObject_RichCompareBool(face->path, t, Py_EQ) != 1) return false;
t = PyDict_GetItemString(descriptor, "index");
if (t && PyLong_AsLong(t) != face->face->face_index) return false;
FT_Matrix dmat = {0};
bool d_has = false;
if (!read_matrix_from_descriptor(descriptor, &dmat, &d_has)) { PyErr_Clear(); return false; }
if (d_has != face->has_matrix) return false;
if (d_has && (
face->matrix.xx != dmat.xx || face->matrix.xy != dmat.xy ||
face->matrix.yx != dmat.yx || face->matrix.yy != dmat.yy)) return false;
return true;
}
@ -339,6 +371,29 @@ face_from_descriptor(PyObject *descriptor, FONTS_DATA_HANDLE fg) {
if ((error = FT_Set_Var_Design_Coordinates(self->face, sz, coords))) return set_load_error(path, error);
}
if (!create_features_for_face(postscript_name_for_face((PyObject*)self), PyDict_GetItemString(descriptor, "features"), &self->font_features)) return NULL;
if (!read_matrix_from_descriptor(descriptor, &self->matrix, &self->has_matrix)) return NULL;
if (self->has_matrix) {
FT_Set_Transform(self->face, &self->matrix, NULL);
if (self->harfbuzz_font) {
#if HB_VERSION_ATLEAST(4,0,0)
// Inform HarfBuzz so shaping (mark positioning, cluster
// boundaries) matches the slanted rendering. The HB API
// models a horizontal shear ratio, which equals xy/xx for
// matrix [[xx,xy],[yx,yy]]. Both operands are FT_Fixed at
// the same 16.16 scale so the factor cancels in the
// division. Guard against xx==0 (degenerate transform).
// Pure scales (s,0,0,s) yield slant=0, which is correct.
// Rotations and shear+rotation composites produce a slant
// value HB can't represent faithfully, but stock fontconfig
// only ever emits horizontal shear here.
if (self->matrix.xx != 0) {
float slant = (float)self->matrix.xy / (float)self->matrix.xx;
hb_font_set_synthetic_slant(self->harfbuzz_font, slant);
}
#endif
hb_ft_font_changed(self->harfbuzz_font);
}
}
}
Py_XINCREF(retval);
return retval;