mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-16 05:27:50 +02:00
freetype: do not apply the fontconfig size-fixup matrix to color glyphs
ee937bdd1b routed FC_MATRIX through the cairo font matrix so synthetic
slant reaches color glyphs. But FC_MATRIX is also how fontconfig encodes
the pixel-size fixup of fixed-size faces. Noto Color Emoji is a ~109px
bitmap strike and fontconfig hands consumers a matrix scaling it to the
requested size (factor = requested_px / strike_px). cairo_set_font_size()
already brings the strike to the requested size, so feeding that matrix
into cairo_set_font_matrix() in apply_cairo_font_size() scales it down
again by the fixup factor. At terminal cell sizes that is a large shrink,
up to ~9x for small cells (easing to 1x as the cell nears the strike), so
color emoji render as a dot; fit_cairo_glyph() only shrinks, so it never
grows them back.
Only the shear carries synthetic slant; the diagonal is the size, which
cairo_set_font_size() and fit_cairo_glyph() already handle. Apply only
the shear and fall through to cairo_set_font_size() when there is no
shear. Pure-slant matrices are unchanged. This carries only the shear to
color glyphs, so a non-uniform diagonal scale from a hand-built FC_MATRIX
is dropped on that path; the stock fixup is uniform, so dropping it is
the intended behaviour.
Add a regression test that renders a color emoji and checks it fills its
cells, skipped unless a fixed-size color font with a fontconfig fixup
matrix is present.
Fixes #10144
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -390,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 encodes the pixel-size fixup of fixed-size color faces (such
|
||||
# as Noto Color Emoji, a ~109px bitmap strike) as FC_MATRIX. That scale
|
||||
# must not reach the cairo font matrix, where glyph size is owned by
|
||||
# cairo_set_font_size() + fit_cairo_glyph(); applying it there shrinks the
|
||||
# glyph by the fixup factor (requested_px / strike_px), and fit_cairo_glyph()
|
||||
# only shrinks so it never grows it back (ee937bdd1b). The shrink ranges
|
||||
# from ~9x at tiny cells to ~1x near the strike; at 48pt/96dpi (~64px) the
|
||||
# bug gives ~0.28 coverage versus ~0.84 when correct.
|
||||
if is_macos:
|
||||
self.skipTest('this is a fontconfig FC_MATRIX issue, not present on macOS')
|
||||
from kitty.fonts.fontconfig import fc_match
|
||||
emoji = fc_match('emoji', False, False, 0)
|
||||
if not (emoji.get('color') and emoji.get('matrix')):
|
||||
# Only a fixed-size color face that fontconfig gives a fixup matrix can
|
||||
# trigger this. A scalable COLR emoji font has none, so a pass there
|
||||
# would be a false negative rather than a real check.
|
||||
self.skipTest('no fixed-size color emoji font with a fontconfig fixup matrix')
|
||||
cells = render_string('\U0001F40D', 'monospace', 48.0, 96.0)[2]
|
||||
pixels = array.array('I', b''.join(cells))
|
||||
coverage = sum(1 for p in pixels if p) / max(len(pixels), 1)
|
||||
self.assertGreater(coverage, 0.5, f'color emoji coverage {coverage:.2f} too low, likely shrunk (#10144)')
|
||||
|
||||
def test_shaping(self):
|
||||
|
||||
def ss(text, font=None):
|
||||
|
||||
Reference in New Issue
Block a user