From labelled boxes to a font, step by step. The code is in digitype/regen.py (cells,
samples, averaging, fonts), island.py and seed.py (the seed), and grow.py (growing it
back).
A typewriter prints every character in a cell of the same width (the pitch). A box labelled with one character is one cell. A box labelled with a run of text gets one cell per character:
Even grid (fallback), when glyphs run together: the pitch is measured from the row’s rhythm (the autocorrelation of its column profile), the grid placed so its boundaries fall in the gaps, and the text fitted to the grid the same way, allowing extra or missing spaces. The older scoring below is the last resort.
Each cell’s width in pixels is its unit: the scale it was typed at. A single-glyph box doesn’t show the pitch, so it takes the unit of runs on the same page, or the pitch setting at the page’s dpi.
Letters that sit on the line give a baseline point: the bottom of their ink. Every cell’s baseline then comes from a Theil-Sen line (the median of the slopes between every pair of points, then the median intercept) through those points: robust, so one odd letter can’t tip it.
The slope gives each cell its tilt. A single-glyph box fits the same kind of line through boxes beside it on the page.
For each cell, a window is cut from the page: the cell widened by expand left and expand right, and vertically from two units above the baseline to most of a unit below it, whatever the box size. The window is thresholded (Otsu) and split into pieces of connected ink. A piece belongs to the cell when:
: and =);' " ^), the marks are the glyph.The line above and below, and the letters beside it, have their centres elsewhere. Letters that touch are cut apart at the thinnest column near the cell’s edge when a piece reaches into the next cell’s middle.
Soft edges. Instead of keeping the black/white cut, the sample keeps the scan’s grey levels around the kept pieces: 0 for paper, 1 for full ink. How dark an edge pixel is pins the true edge down to a fraction of a pixel, so even a single sample has a smooth edge.
Tilt correction turns the sample level about its baseline point by its line’s tilt.
All the samples of a character are combined:
The averaged map is lightly blurred (more when there are few samples, fading as samples are added: averaging n samples smooths an edge on its own), enlarged with cubic interpolation (Detail), and traced where at least half of the samples have ink. That outline is the Average font’s glyph.
The Seed font rebuilds each letter from its skeleton.
Imagine lowering the letter into water a little at a time. The shoreline at each level is the letter thinned by that much. Where shore from two sides meets and goes under, it leaves a trail; the depth at which each trail point went under is the stroke’s half-width there. The trail plus the depth is the letter’s seed, and a disc of that radius at every trail point gives the letter back.
In practice (island.py):
Then (seed.py) each branch becomes a stroke: its centreline smoothed and simplified, and
its half-width fitted as a straight line along it (leaving out the extra depth where strokes
meet, which belongs to the junction).
grow.py puts a disc of the stroke’s half-width at every point of each centreline, joins
neighbouring discs into tapered segments, and adds a disc at each junction. Weight scales
every half-width and Add adds to it, so one seed can grow a lighter or bolder cut.
Stroke ends come back round, and corners slightly soft: restoring flat terminals and sharp corners is planned.
Placement. As typed keeps each glyph where it sat in its cell (the face’s own placement). Centred moves it so its ink is centred in the cell, measured on the letter’s main ink (pieces under 3% of it don’t count).
The font file (fontTools): one glyph per character, every advance one cell (600 units on a
1000-unit em), outer contours clockwise, post.isFixedPitch and PANOSE monospaced, and the
full name table Windows requires (family, style, unique id, full name, version, PostScript
name).