mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-15 21:17:49 +02:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a685af8c35 | ||
|
|
ba6b652850 | ||
|
|
fc23ef57ec | ||
|
|
d4106ef2db | ||
|
|
9393e29793 | ||
|
|
e4bce6485b | ||
|
|
fbd4e5ad1f | ||
|
|
48ee061454 | ||
|
|
aa4e94cef5 | ||
|
|
8a39929c15 | ||
|
|
8be2a10b29 | ||
|
|
779a49acde |
@@ -78,11 +78,11 @@
|
||||
},
|
||||
|
||||
{
|
||||
"name": "sqlite 3.53.0",
|
||||
"name": "sqlite 3.53.2",
|
||||
"unix": {
|
||||
"file_extension": "tar.gz",
|
||||
"hash": "sha256:851e9b38192fe2ceaa65e0baa665e7fa06230c3d9bd1a6a9662d02380d73365a",
|
||||
"urls": ["https://www.sqlite.org/2026/{name}-autoconf-3530000.{file_extension}"]
|
||||
"hash": "sha256:588ad51949419a56ebe81fe56193d510c559eb94c9a57748387860b5d3069316",
|
||||
"urls": ["https://www.sqlite.org/2026/{name}-autoconf-3530200.{file_extension}"]
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -173,6 +173,13 @@ consumption to do the same tasks.
|
||||
Detailed list of changes
|
||||
-------------------------------------
|
||||
|
||||
0.47.4 [2026-06-15]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Linux: Fix a regression in the previous release that broke rendering of bitmap color fonts (:pull:`10145`)
|
||||
|
||||
- Linux: Allow fake italics defined via a matrix in fontconfig settings to work for fonts like Fira Code that do not ship with an italic face (:pull:`10120`)
|
||||
|
||||
0.47.3 [2026-06-12]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class Version(NamedTuple):
|
||||
|
||||
appname: str = 'kitty'
|
||||
kitty_face = '🐱'
|
||||
version: Version = Version(0, 47, 3)
|
||||
version: Version = Version(0, 47, 4)
|
||||
str_version: str = '.'.join(map(str, version))
|
||||
_plat = sys.platform.lower()
|
||||
is_macos: bool = 'darwin' in _plat
|
||||
|
||||
@@ -458,6 +458,10 @@ specialize_font_descriptor(PyObject *base_descriptor, double font_sz_in_pts, dou
|
||||
if (axes) {
|
||||
if (PyDict_SetItemString(ans, "axes", axes) != 0) return NULL;
|
||||
}
|
||||
PyObject *matrix = PyDict_GetItemString(base_descriptor, "matrix");
|
||||
if (matrix) {
|
||||
if (PyDict_SetItemString(ans, "matrix", matrix) != 0) return NULL;
|
||||
}
|
||||
PyObject *ff = PyDict_GetItemString(ans, "fontfeatures");
|
||||
if (ff && PyList_GET_SIZE(ff)) {
|
||||
for (Py_ssize_t i = 0; i < PyList_GET_SIZE(ff); i++) {
|
||||
|
||||
@@ -401,7 +401,38 @@ def get_font_files(opts: Options) -> FontFiles:
|
||||
font = medium_font
|
||||
key = kd[(bold, italic)]
|
||||
ans[key] = font
|
||||
return {'medium': ans['medium'], 'bold': ans['bold'], 'italic': ans['italic'], 'bi': ans['bi']}
|
||||
|
||||
def apply_synthetic_matrix(font: Descriptor, bold: bool, italic: bool) -> Descriptor:
|
||||
# fontconfig's FcFontList (used by find_best_match) omits FC_MATRIX from
|
||||
# its object set, so a roman font found there carries no synthetic-italic
|
||||
# shear and its "italic" renders upright. Fira Code is the case (it ships
|
||||
# no italic), in both its static and variable builds. The italic intent
|
||||
# exists only here at selection finalize, not at face construction, so
|
||||
# recover the matrix now: for an italic slot whose chosen face is upright
|
||||
# (no slant) and has no matrix yet, 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); when a real italic exists it returns no matrix, so a font that is
|
||||
# already italic, static or variable, is never double-slanted. Face construction applies the matrix
|
||||
# via FT_Set_Transform; specialize_font_descriptor preserves it when the
|
||||
# descriptor is sized for rendering. Only the matrix is taken, so
|
||||
# selection is unchanged. Covers the four configured faces; fc_match
|
||||
# re-matches by family name (see commit message).
|
||||
if (italic and font['descriptor_type'] == 'fontconfig'
|
||||
and not font.get('matrix') and not font.get('slant')):
|
||||
from kitty.fast_data_types import FC_MONO
|
||||
from kitty.fonts.fontconfig import fc_match
|
||||
mtx = fc_match(font['family'], bold, italic, FC_MONO if is_monospace(font) else -1).get('matrix')
|
||||
if mtx:
|
||||
new_font = font.copy()
|
||||
new_font['matrix'] = mtx
|
||||
return new_font
|
||||
return font
|
||||
return {
|
||||
'medium': ans['medium'], 'bold': ans['bold'],
|
||||
'italic': apply_synthetic_matrix(ans['italic'], False, True),
|
||||
'bi': apply_synthetic_matrix(ans['bi'], True, True),
|
||||
}
|
||||
|
||||
|
||||
def axis_values_are_equal(defaults: dict[str, float], a: dict[str, float], b: dict[str, float]) -> bool:
|
||||
|
||||
@@ -878,18 +878,26 @@ apply_cairo_font_size(Face *self, unsigned sz_px) {
|
||||
// on self->face in face_from_descriptor. cairo owns FT_Set_Transform on
|
||||
// its face and derives it from the font matrix on every render
|
||||
// (_cairo_ft_unscaled_font_set_scale in cairo-ft-font.c), so the only
|
||||
// channel that reaches glyph rasterization is the cairo font matrix
|
||||
// itself. Encode FC_MATRIX there.
|
||||
// FT_Matrix is xx,xy,yx,yy (row-major); cairo_matrix_init takes
|
||||
// xx,yx,xy,yy. Same matrix, transposed argument order.
|
||||
if (!self->has_matrix) { cairo_set_font_size(self->cairo.cr, sz_px); return; }
|
||||
// channel that reaches glyph rasterization is the cairo font matrix.
|
||||
//
|
||||
// FC_MATRIX is overloaded. Besides synthetic slant, fontconfig also encodes
|
||||
// the pixel size fixup of fixed-size faces here, as a pure diagonal scale
|
||||
// (Noto Color Emoji is matched with [0.1147 0; 0 0.1147]). The color path
|
||||
// already sizes glyphs with cairo_set_font_size() + fit_cairo_glyph(), and
|
||||
// fit_cairo_glyph() only shrinks, so feeding the fixup scale in here shrinks
|
||||
// color emoji with no way to recover (#10144). Honor only the shear that
|
||||
// carries synthetic slant and leave the size to cairo_set_font_size().
|
||||
if (!self->has_matrix || self->matrix.xx == 0 || self->matrix.yy == 0) {
|
||||
cairo_set_font_size(self->cairo.cr, sz_px); return;
|
||||
}
|
||||
double shear_xy = (double)self->matrix.xy / (double)self->matrix.xx;
|
||||
double shear_yx = (double)self->matrix.yx / (double)self->matrix.yy;
|
||||
if (shear_xy == 0 && shear_yx == 0) { cairo_set_font_size(self->cairo.cr, sz_px); return; }
|
||||
// FT_Matrix is xx,xy,yx,yy (row-major); cairo_matrix_init takes xx,yx,xy,yy.
|
||||
// The diagonal is unit scale (size is handled above); apply only the shear.
|
||||
double s = (double)sz_px;
|
||||
double xx = self->matrix.xx / 65536.0;
|
||||
double xy = self->matrix.xy / 65536.0;
|
||||
double yx = self->matrix.yx / 65536.0;
|
||||
double yy = self->matrix.yy / 65536.0;
|
||||
cairo_matrix_t m;
|
||||
cairo_matrix_init(&m, xx * s, yx * s, xy * s, yy * s, 0, 0);
|
||||
cairo_matrix_init(&m, s, shear_yx * s, shear_xy * s, s, 0, 0);
|
||||
cairo_set_font_matrix(self->cairo.cr, &m);
|
||||
}
|
||||
|
||||
|
||||
@@ -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''
|
||||
@@ -355,6 +390,30 @@ class Rendering(FontBaseTest):
|
||||
self.assertGreater(w, 64)
|
||||
self.assertGreater(h, 64)
|
||||
|
||||
def test_color_emoji_not_shrunk(self):
|
||||
# Regression test for https://github.com/kovidgoyal/kitty/issues/10144.
|
||||
# fontconfig gives fixed-size color faces (e.g. Noto Color Emoji) a
|
||||
# pixel-size fixup encoded as FC_MATRIX. That scale must not reach the
|
||||
# cairo font matrix used for color glyphs; applying it shrinks color emoji
|
||||
# to a dot (ee937bdd1b). Render the same font two ways at the same size:
|
||||
# from its fontconfig descriptor, which carries the fixup matrix, and from
|
||||
# its file path, which does not. A correct build renders both at the same
|
||||
# size; the bug shrinks the descriptor one. Comparing the two is
|
||||
# environment-independent since only the matrix differs.
|
||||
if is_macos:
|
||||
self.skipTest('FC_MATRIX is a fontconfig feature, not used on macOS')
|
||||
from kitty.fonts.fontconfig import fc_match
|
||||
desc = dict(fc_match('emoji', False, False, 0))
|
||||
if not (desc.get('color') and desc.get('matrix')):
|
||||
self.skipTest('no fixed-size color emoji font with a fontconfig fixup matrix')
|
||||
with_matrix = face_from_descriptor(desc)
|
||||
with_matrix.set_size(64, 96, 96)
|
||||
without_matrix = create_face(desc['path'])
|
||||
without_matrix.set_size(64, 96, 96)
|
||||
_, mw, mh = with_matrix.render_codepoint(0x1F40D)
|
||||
_, rw, rh = without_matrix.render_codepoint(0x1F40D)
|
||||
self.assertGreater(mh, 0.5 * rh, f'color emoji shrunk by FC_MATRIX: {mh}px vs {rh}px (#10144)')
|
||||
|
||||
def test_shaping(self):
|
||||
|
||||
def ss(text, font=None):
|
||||
|
||||
Reference in New Issue
Block a user