From b3e7c3e71760ef4eb0327f0b917ffffa491d1ddb Mon Sep 17 00:00:00 2001 From: Strykar <2946372+Strykar@users.noreply.github.com> Date: Sun, 10 May 2026 02:48:29 +0530 Subject: [PATCH] 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. --- kitty/fontconfig.c | 10 +++++++++ kitty/freetype.c | 55 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/kitty/fontconfig.c b/kitty/fontconfig.c index 143150702..a6ef9707e 100644 --- a/kitty/fontconfig.c +++ b/kitty/fontconfig.c @@ -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; diff --git a/kitty/freetype.c b/kitty/freetype.c index a1ec1e1b7..7da2b1dab 100644 --- a/kitty/freetype.c +++ b/kitty/freetype.c @@ -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;