Skip to content

fix(paint): draw raster text at the exact size with a sub-pixel pen - #408

Merged
tannevaled merged 2 commits into
mainfrom
fix/raster-subpixel-text
Nov 9, 2026
Merged

tannevaled merged 2 commits into
mainfrom
fix/raster-subpixel-text

Conversation

@tannevaled

@tannevaled tannevaled commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

What

The raster painter drew text on the whole-pixel grid (face at int(size + 0.5), pen advanced by whole-pixel advances, start at int(it.X)) while layout and the PDF painter use the exact advances; PR #146 left drawText that way on purpose. Each word started in the right place but came out up to 2px short, so the gaps looked uneven and too wide (#407). Not a #405 regression: at the default weight the old output is byte-identical to v0.45.9.

  • drawTextAt: float pen, advance = the glyph's font-unit advance at the exact size plus letter-spacing; each glyph drawn at the nearest quarter pixel with opentype.Face.GlyphMaskIndexAt (new in go-opentype/opentype v0.21.0, the lowest shared library; Face: a glyph mask at a fractional pen position go-opentype/opentype#66, docs: refresh bench corpus and fidelity notes after PR #63-66 #67). drawText stays as an integer wrapper.
  • Fonts.drawFace: face at the exact size (NewFaceAt) with the same weight instancing, cached by size. Layout metrics keep the whole-pixel face, so line boxes do not move.
  • Four goldens move, by sub-pixel word placement only (checked by eye on inline_decoration.png).

Evidence

  • engine.TestRasterTextMatchesChromeWords: 30 words (Open Sans variable, lorem text) at 16, 22.4 and 30.8 px, weights 400 and 700, starts within 0.5 px of a Google Chrome headless screenshot of the same file, widths within 0.6 px (a width is the difference of two coverage-estimated edges, about a quarter pixel of noise each). Before the change the test fails with 20 reported differences (and one row with a word missing); after it, it passes. Observed differences are at most 0.31 px on starts.
  • paint.TestDrawFaceExactSizeCache; paint stays at 100 percent.
  • Allocations (exact), BenchmarkRenderInlineFontFace: 1659 -> 1990 allocs/op (+331), 6340993 -> 6450799 B/op (+1.7 percent). The cost is rasterising each glyph at up to four sub-pixel phases on first use (cached per face afterwards). Timings are deferred: the 1-minute load was 11.9, above 8 (a noisy 13.8 -> 14.3 ms/op is not evidence).
  • go-opentype's rasteriser supersamples 4x horizontally, so more than four phases would not be distinguishable.

Closes #407

Co-Authored-By: Claude Sonnet 5.5 noreply@anthropic.com

🤖 Generated with Claude Code

tannevaled and others added 2 commits November 9, 2026 13:50
drawText built its face at the whole-pixel size and advanced the pen by
whole-pixel advances, while layout (and the PDF painter) use the exact ones,
so every word came out up to 2px short and the gaps between words looked
uneven. It now draws with a face at the exact size (Font.NewFaceAt) and a
float pen, placing each glyph at the nearest quarter pixel through
opentype.Face.GlyphMaskIndexAt (go-opentype/opentype v0.21.0). Layout
metrics keep the whole-pixel face, so line boxes do not move. Four goldens
move by sub-pixel word placement only.

Closes #407

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Refs #407

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 3b78ab6 into main Nov 9, 2026
13 checks passed
@tannevaled
tannevaled deleted the fix/raster-subpixel-text branch November 9, 2026 12:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

paint: the raster painter draws text on the whole-pixel grid, so word gaps look uneven

1 participant