All posts

3 min read

The screenshot cell was half measured and half guessed

Pane screenshots came out stretched. Two rounds of font maths got the cell shape wrong. The fix asks the host terminal for its real cell size in pixels.

GGGaurav 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. That is exactly 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 came 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 were measured from the same font file, with 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. I turned the frame off, so the PNG is exactly the grid, and held it 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 because it guessed. Round two measured every number and was still wrong, because it measured the wrong thing. A terminal's cell is not the font's cell. The terminal picks its own: the width from its own rounding of the advance at its pixel size, the height from its own leading rules, both snapped to whole 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 only means the glyph sits centred in it, which the renderer already did. So the aspect correction only ever grows 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. Taking the expected value from the same source as the code 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.