Back to the blog

The cell was half measured and half guessed

Two rounds of obviously correct font maths in the screenshot renderer, both wrong. The number that settled it did not come from a font file. It came from holding the picture up against a running terminal.

Gaurav Gosain

tuios can save a capture of a pane as a PNG: the daemon hands over the resolved grid of cells, and a renderer rasterises it with a real font. The report against it was short. Captures looked horizontally stretched.

The renderer's cell came from two sources that did not know about each other:

// cellSize derives the grid cell from the primary face: the advance of "M"
// wide, a terminal-ish 1.25 line height tall.

The width was measured, the advance of the letter M in the actual face. The height was fs.size * 1.25, a number I had typed because it looked like a line height. Half measured and half guessed, and the guess set the shape.

Round one: measure the other half

JetBrainsMono's own metrics are a 0.600 em advance in a 1.320 em line box, a ratio of 0.455. kitty on this machine draws that font in a 10 by 22 pixel cell, which is that ratio exactly. The half-guessed cell measured 0.486. Every capture came out about seven percent wider per cell than the screen it pictured, uniformly, which is precisely what "horizontally stretched" looks like to someone holding the picture next to their own terminal.

The fix was satisfying in the way that makes you stop checking. The height now comes from the face's own line box, ascent minus descent plus line gap, the same box kitty and ghostty size their cells from. Both halves of the cell measured, from the same font file, no invented constants anywhere. I shipped it as done.

Round two: the font was the wrong oracle

The next round of reports came from the preview panel, and this time I measured the output against a real kitty instead of against the font: frame turned off, so the PNG is exactly the grid, held against the host cell for cell. The host's cell was 9 by 20 pixels, a ratio of 0.450. The picture's cell was 5.76 by 14.4, a ratio of 0.400. Every column of the picture was eleven percent narrower than the column it pictured.

the terminal (kitty, 9 x 20 px)
the capture (5.76 x 14.4 px)
host 0.45capture 0.4-11.1% narrower per cell
The same ten columns at the measured ratios. Round one came out wider than the screen, round two narrower, and both errors are uniform, so nothing inside the picture looks broken. Only holding it against the terminal shows the shape is wrong. The two rounds were measured against different kitty configurations, which is why the host's own ratio differs between them.

Round one had been wrong for guessing. Round two was wrong with every number measured, because they were measured from the wrong thing. A terminal's cell is not the font's cell. The width the terminal uses is whatever it chose, from its own rounding of the advance at its own pixel size, its cell height from its own leading rules, both snapped to integer pixels. The face's advance and line box agree with that choice only by luck, and on this machine, at this size, they did not.

Both rounds have the same shape. The obviously correct move, twice, was to read the answer off the font, and the font was never the authority. The terminal is.

Ask the thing that decided

The host already knows its cell in pixels and reports it when asked, and tuios already asks: it is how the client places kitty graphics. So the capture path now carries the host's cell shape to the renderer, and the raster's cell is grown to match it.

Grown, never shrunk. A cell narrower than the advance runs neighbouring glyphs into each other, and a cell shorter than the line box clips them. A cell wider than the advance costs a glyph centred in it, which the renderer already did. So the aspect correction only ever widens one axis:

if want := ch * f.CellAspect; want > cw {
    return want, ch
}
return cw, cw / f.CellAspect

Measured again the same way, the capture and the host agree to four decimal places: 0.4545 against 0.4545.

What I keep from this

The tests that came out of this take their expected values from something other than the code under test: the PNG's own header, a cell size forced through the environment, the panel's footer rule as it was actually drawn. That is the trap this feature fell into twice. Both wrong cells were internally consistent, derived from real data by defensible arithmetic, and would have passed any test built from the same font metrics that produced them.

The claim being made was never about the font. It was "the picture is the shape of the screen", and there is exactly one oracle for that claim: a running terminal, measured. Round one taught me not to guess. It took round two to teach me that measuring the wrong authority is guessing with more steps.