Repository navigation
Font File Primer
A TrueType font is a set of tables, each identified by a four letter tag. Each table has one job. This page covers the ones that matter for D2 Coding, with the actual values from the shipped files.
Every coordinate and width is in font units. D2 Coding uses 1000 units per em
(head.unitsPerEm). At a 14px font size, 1000 units are 14px, so 500 units are 7px.
A glyph is one drawn shape: A, 가, the ⇒ ligature, an empty space. The font
stores glyphs in a fixed order and every other table refers to a glyph by its position in
that order, the glyph ID (GID).
| Build | Glyphs |
|---|---|
| Regular, standard and ligature | 26,189 |
| Bold, standard and ligature | 26,167 |
Regular and Bold are separate drawings with different glyph sets, so the same GID does not mean the same glyph across weights.
Because other tables point at GIDs, a new glyph is appended at the end of the glyph
order. Inserting one in the middle shifts every later GID, and every table that refers to
them (cmap, hmtx, loca, GSUB, GDEF) would have to be rewritten. The wave dash
glyph uni301C added in 1.3.4 is the last glyph for this reason.
The shipped files use post table format 3, which stores no glyph names. Tools such as
fontTools invent names when they load the font, from the cmap (uniAC00, arrowright)
or from the GID (glyph03941). Those names can change when the cmap changes: after
U+0009 was unmapped, fontTools started calling the shared space glyph uni000D instead of
uni0009. The UFO sources keep their own fixed names, so match glyphs between a UFO and a
binary by position in public.glyphOrder, not by name.
| Tag | What it holds | D2 Coding specifics |
|---|---|---|
cmap |
character code to glyph | four subtables, see below |
glyf |
outlines | quadratic TrueType contours |
loca |
where each glyph starts inside glyf
|
long offsets |
hmtx |
advance width and left side bearing per glyph | 500 for half width, 1000 for full width |
hhea |
horizontal header |
advanceWidthMax 1000, ascent 850, descent -230 |
OS/2 |
metrics and flags for Windows | version 3, xAvgCharWidth 500, PANOSE proportion 9 (monospaced) |
post |
PostScript data | format 3 (no names), isFixedPitch 1 |
head |
global header |
fontRevision, modified time, checksum |
name |
names and strings | family, version string (name ID 5), copyright |
maxp |
glyph count and hinting limits | |
gasp |
when to hint and smooth | {8: 2, 16: 1, 65535: 3} |
cvt , fpgm, prep
|
hinting programs shared by all glyphs | |
GSUB |
glyph substitution (ligatures) | ligature build only |
GDEF, GPOS
|
glyph classes, positioning | ligature build only; GPOS has no features |
The standard and ligature builds have byte identical glyf, loca, hmtx, cmap,
maxp and hhea. The ligature build adds GSUB, GDEF and GPOS. Keep it that way:
see Editing the binaries.
The cmap maps character codes to glyphs. A text engine looks up each character here. If
there is no entry, the character is treated as missing from this font: it is either not
drawn or drawn from a fallback font.
U+0041 A -> glyph A
U+AC00 가 -> glyph uniAC00
U+223C ∼ -> glyph similar
U+301C 〜 -> glyph uni301C (its own glyph since 1.3.4; it used to share "similar")
U+000A (LF) -> nothing (unmapped since 1.3.5, see below)
D2 Coding has four subtables:
| Platform, encoding | Format | Covers |
|---|---|---|
| 0, 3 (Unicode BMP) | 4 | 19,936 code points |
| 1, 0 (Mac Roman) | 0 | 256 single byte codes |
| 1, 3 (Mac Korean) | 2 | KS X 1001 double byte codes |
| 3, 1 (Windows Unicode BMP) | 4 | same data as 0, 3 |
The 0,3 and 3,1 records point at one shared subtable. The Mac subtables are legacy, but
if you remap a character, check whether its Mac code is mapped too. The wave dash is
0xA1AD in Mac Korean, and 1.3.4 remapped it there as well.
Several characters can share one glyph. The space U+0020 and the carriage return U+000D share one empty glyph (the tab U+0009 did too until it was unmapped).
U+0001 to U+001F used to point at DOS (code page 437) style pictures: ◙ for the line
feed, ♂ ♀ ♪ and so on. Some text engines look control characters up, and Keynote drew
◙ at the end of every paragraph (issue #93). Since 1.3.5 only U+0000 and
U+000D are mapped in that range. The pictures are still in glyf with no mapping, so no GID
moved. fontbakery's control_chars check, used for Google Fonts, accepts only U+0000 and
U+000D.
Every glyph has an advance width: how far the pen moves after drawing it. In D2 Coding it is 500 or 1000. Of the mapped code points, 2,171 are 500 wide, 17,763 are 1000 wide, and two (U+0000 and U+FEFF) are zero width. The whole grid depends on this; see Width policy.
The left side bearing (lsb) in hmtx must equal the glyph's xMin in glyf. When you move
an outline sideways, update both.
After the cmap has turned characters into glyphs, the text engine runs the GSUB rules
over the glyph sequence. That is how -> becomes one arrow shape.
glyphs: [ - ] [ > ]
GSUB rule: "-" followed by ">" -> arrow pieces
result: [ → ]
GSUB has two layers:
-
Lookups are the rules themselves. The ligature build has 205: 124 chaining
contextual lookups (type 6) that decide where a ligature applies, 80 single
substitutions (type 1), 79 of which are called by those chaining lookups, and 1
alternate substitution (type 3).
aaltuses the remaining single substitution and the alternate substitution. - Features are four letter labels on groups of lookups. Applications switch features on and off by label, never single lookups.
Features are registered per script (and language). D2 Coding registers its features
under DFLT, latn (plus ten language systems), cyrl, grek, zinh and zyyy. There is
no hang record; a Hangul run falls back to DFLT.
| Tag | Name | Usual meaning |
|---|---|---|
liga |
standard ligatures | join letters into one shape, such as fi
|
calt |
contextual alternates | change a glyph based on its neighbours |
aalt |
access all alternates | lets an app offer alternates in a picker |
Both liga and calt are on by default in practically every application. Coding fonts
usually put their ligatures in calt because the rules have to look at the context: ->
becomes an arrow, but --> is handled differently.
D2 Coding had its ligatures only in calt. That broke on lines shaped as Hangul: before
shaping, the engine decides the script of each run of text, and the Hangul shaper in
HarfBuzz switches calt off. Uniscribe does the same. A line such as <!-- 주석 --> can be
shaped as one Hangul run, and then it lost every ligature (issue #73). In Chrome the
ligatures did apply on such lines, so the specimen page never showed the problem.
Since 1.3.5, the same lookups are registered under both calt and liga.
The Hangul shaper keeps liga, so the ligatures apply on those lines too. The cost: to turn
ligatures off, switch off both calt and liga. VS Code's
"editor.fontLigatures": false does both; CSS font-variant-ligatures: none does both.
At small pixel sizes an outline does not land on whole pixels. Hinting is a small
program per glyph, written in TrueType instructions, that moves outline points onto the
pixel grid before rasterizing. The shared parts are in fpgm (functions), prep (run at
each size) and cvt (shared values such as stem widths). The per glyph programs sit
next to the outline in glyf.
ppem (pixels per em) is the font size in pixels. 18 ppem means 1000 units are 18 pixels.
A DELTAP instruction nudges one point by a fraction of a pixel at one specific ppem.
Fixes for single sizes are made this way. Issue #107 is one: Regular i, і and ѝ have
-1 px corrections at 19 and 20 ppem but none at 18, so they sit 1 px above the baseline
at exactly 18 ppem.
gasp tells the rasterizer when to apply hinting and smoothing: {8: 2, 16: 1, 65535: 3}
means smoothing without gridfit up to 8 ppem, gridfit without smoothing from 9 to 16 ppem,
and both above 16 ppem.
Hinting survives in the UFO sources: cvt , fpgm and prep are in lib.plist under
public.truetype.instructions, and each glyph's program is in its .glif.
| Where | 1.3.4 |
|---|---|
name ID 5 |
Version 1.3.4; Build 20261002 |
head.fontRevision |
1.004 (fixed point, so it reads back as 1.003998) |
UFO fontinfo.plist
|
versionMajor 1, versionMinor 4, openTypeNameVersion
|
The pattern is 1.00N for 1.3.N: 1.3.2 was 1.002, 1.3.3 was 1.003. tools/release.py
prints these records but does not write them; set them when you patch the binaries.
Maintainer notes
Repository