# Glyph · Malte Therkildsen

> TrueType outlines, variable axes and a rasterizer, reading the font this site is set in. Every line in the page is drawn twice, by Glyph and by your browser, with where they differ.

Source: https://therkildsen.dev/writing/glyph

[Writing](https://therkildsen.dev/writing)/Glyph
W05NotesEngines from nothing

# Glyph, a font renderer written from nothing

Glyph reads a font file and draws its letters without the browser’s help: the tables, the outlines, the axes of a variable font, and the pixels. It was written from an empty file and the specifications, and it reads the font this page is set in. Below, every line is drawn twice, once by Glyph and once by your browser, from the same bytes.

PostedKind**Notes**Series**Engines from nothing**

        Text
        Weight
        Size

        Glyph

        Your browser

        Where they differ, made four times stronger

      ·

    Both sides read the same file, the Inter this site is set in, served as WOFF. Your browser draws each letter with its canvas, and Glyph draws the same letters at the same weight and optical size. Each letter stands on a whole pixel and is not kerned, on both sides.

## A table of tables

      A TrueType font is a directory of tables, each named by four letters. `cmap` maps characters to glyph numbers. `loca` says where each glyph’s outline starts in `glyf`. `hmtx` gives each glyph its advance, the distance to the next letter. A variable font adds `fvar`, which names its axes, and `gvar`, which says how far every point moves along them.

      WOFF wraps the same tables for the web and squeezes each one with zlib on its own, so Glyph hands them to Thaw, the PNG decoder from the note before, whose inflate is the same DEFLATE. These are the tables of the file this page loaded, with how many bytes each took on the network and in memory.

        ·

## Points on and off the curve

      An outline in `glyf` is a list of contours, and a contour is a list of points, each marked on or off the curve. Two points on the curve are joined by a straight line. A point off the curve pulls the line between its neighbors into a quadratic Bézier. When two points off the curve stand next to each other, the font saves a point: the one on the curve between them is implied, halfway, and never stored. The coordinates themselves are stored as differences from the point before, in one or two bytes each, with flags saying which.

              Letter
               Points

            Weight
            Optical size

            The tuples in gvar

        Filled dots are on the curve, rings are off it, and small dots are the implied points between two rings. The dashed outline is the letter where the font was drawn, at the lightest weight and smallest optical size, and the red lines show how far each point has moved from there.

## Moving the points

      A variable font stores one outline and the ways it can change. A position on the axes is first normalized, so the default is zero and the ends are minus one and one, and then bent by `avar`, which lets the designer space the weights evenly to the eye rather than by the numbers. Inter’s own bend is slight: it leaves optical size straight and nudges the weight axis by the smallest step the file can store.

      Each glyph has a few tuples in `gvar`. A tuple is a set of deltas, one per point, and a peak: the place on the axes where the deltas count in full. Away from the peak a tuple counts less, falling to nothing at the edge of its region, and the weights of all the axes are multiplied. Every point moves by the sum of the deltas, each times its tuple’s weight. The list beside the letter shows its tuples and how much each counts where the sliders stand.

      To save space, a tuple may leave points out. Those move with their neighbors on the same contour: a point between two that moved, on one axis at a time, gets a delta interpolated by where it lies between them, and a point outside them takes the delta of the nearer one. The specification calls this interpolation of untouched points. The first version of Glyph got one corner of it wrong. When the two neighbors have the same coordinate, their delta counts only if they agree, wherever the point between them lies. Glyph had tested for the point lying outside them first, and moved the top of the J too far at the boldest weight and largest optical size.

## From outline to pixels

      The first rasterizer added up areas. Each edge of the outline adds to the pixels it crosses the area between it and the pixel’s edge, signed by its direction, and a running sum along the row turns those areas into coverage. It is exact for a simple shape and fast, and it drew Inter’s e wrong.

Nonzero

Even and odd

        The same outline of e, filled by the two rules a path can be filled by. Under even and odd, the part of the bar that runs into the bowl turns hollow, because it is inside the outline twice.

      Inter’s e is one contour that crosses itself: the bar runs on into the bowl, so a strip of it lies inside the outline twice. Areas add up to two there, and a pixel on the edge of that strip is counted darker than it is. Type designers draw letters this way on purpose, and a variable font keeps the overlaps, because removing them would break the way the points move.

      So Glyph now draws as the browser’s own rule says, by counting. Every row of pixels is crossed by sixteen lines. On each line, Glyph finds where the edges cross it, sorts them, and walks along adding one for an edge going up and taking one for an edge going down. Wherever the count is not zero, the line is inside the letter, and each pixel gets the exact part of its width the inside covers. Down, the result is sampled sixteen times; across, it is exact.

## Held against Chromium

      The command draws every character in the font at five sizes and seven weights three ways: by Chromium’s canvas from the font file, by Skia, Chromium’s graphics library, filling the outline Glyph read, and by Glyph alone. Where two drawings of a pixel differ by more than a tenth, it asks Chromium itself which is right: `isPointInPath` tests a single point against the outline exactly, and the pixel is sampled with it on a grid that shares no row with Glyph’s.

      $ node web/notes/glyph/canvas.mjs
Chromium 141.0.7390.37, Inter as this site serves it: 89,588 bytes of WOFF, 196,280 once Thaw has unpacked its 19 tables
774 glyphs, 291 characters, axes opsz 14 to 32 and wght 400 to 700
every character at 12, 16, 24, 32, 48 px and weights 400 to 700 in steps of 50

Glyph against Chromium drawing the text
1,533,458 inked pixels, 51.2 % the same, 63.1 % within 2/255, mean difference 0.0172
  at 12 px: 39.7 % within 2/255, mean difference 0.0261
  at 16 px: 41.8 % within 2/255, mean difference 0.0264
  at 24 px: 55.1 % within 2/255, mean difference 0.0197
  at 32 px: 62.9 % within 2/255, mean difference 0.0170
  at 48 px: 71.5 % within 2/255, mean difference 0.0141
51,860 off by more than a tenth: the samples side with Glyph on 51,858 and with Chromium on 2
  the most: ※ at 48 px, weight 400: Glyph 0.24, Chromium 1, the samples 0.24

Chromium against itself: fillText against Skia filling the same outline
1,505,798 inked pixels, 61.5 % the same, 75.0 % within 2/255, mean difference 0.0067
9,160 off by more than a tenth: the samples side with Skia on 5,280 and with fillText on 3,880

Glyph against Skia filling the same outline
1,533,385 inked pixels, 54.1 % the same, 68.4 % within 2/255, mean difference 0.0158
48,781 off by more than a tenth: the samples side with Glyph on 48,778 and with Skia on 3
  the most: ※ at 48 px, weight 400: Glyph 0.24, Skia 1, the samples 0.24

The optical size: at 48 px, fillText against Skia filling Glyph's outline
  at opsz 32, which Glyph takes from the size: mean difference 0.0050
  at opsz 14, the font's default:              mean difference 0.2283

Why: Skia filling the outline, against the samples, on every pixel at an edge, at 16 and 48 px, weight 400
  as curves, cut up by Skia:      50,716 pixels, mean difference 0.0372, 4,329 off by more than a tenth
  cut into lines by Glyph first:  38,330 pixels, mean difference 0.0215, 243 off by more than a tenth

## Where the browser and Glyph part

      The drawings agree closely but not exactly, and the run shows on whose side the difference lies. Where they differ by more than a tenth, the exact test sides with Glyph nearly every time. The cause is the last block of the run. Skia does not fill a curve as a curve: it cuts it into straight pieces first, and coarsely, to be fast. Given the same outline already cut finely into lines, Skia’s pixels come close to the exact ones. Chromium is not wrong here, it chose speed, and at the size of text the difference is a shade on one pixel of an edge.

      The second thing the run shows is that a canvas does not leave the optical size alone. Asked for Inter at a size, Chromium sets the optical size axis to that size, so large text gets the tighter, finer letters the designer drew for it. Glyph does the same, and the run shows what happens if it does not.

## What it does not have

      Glyph reads TrueType outlines and nothing else. It leaves out the PostScript outlines of an OpenType CFF font, color fonts and , whose tables are packed with Brotli. It does not hint, so small text is not snapped to the pixel grid, which is also how a canvas draws it. It does not shape: no kerning from `GPOS`, no ligatures or alternates from `GSUB`, and no right to left. A line is its letters side by side. It is written to be read and to be right.

      The source is in the portfolio’s repository under `web/notes/glyph/`: `glyph.js` for the font, the outlines, the variations and the rasterizer, `canvas.mjs` for the command, `glyph.test.mjs` for the tests. Inter is Rasmus Andersson’s, under the SIL Open Font License.

[Newer · W06Crank, a WebAssembly interpreter written from nothing](https://therkildsen.dev/writing/crank)[Older · W04Thaw, a PNG decoder written from nothing](https://therkildsen.dev/writing/thaw)
