Skip to content

Font File Primer

Sanghyuk Jung edited this page Oct 2, 2026 · 2 revisions

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.

Units

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.

Glyphs and glyph IDs

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.

Glyph names

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.

Tables

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.

cmap

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).

Control characters

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.

hmtx: widths

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.

GSUB, lookups and features

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). aalt uses 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.

calt and liga

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.

Hinting

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.

Version records

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.

Clone this wiki locally