Skip to content

Verifying a Change

Sanghyuk Jung edited this page Oct 2, 2026 · 1 revision

Verifying a change

Check every change at three levels: the bytes in the file, the shaping, and the rendering. Keep a copy of the four TTFs from before the change to compare against.

pip install fonttools brotli uharfbuzz
pip install freetype-py      # for raster checks; a virtualenv keeps it out of the system

1. Which tables changed

from fontTools.ttLib import TTFont

for name in ["D2Coding-Regular", "D2Coding-Bold", "D2Codingligature-Regular", "D2Codingligature-Bold"]:
    old = TTFont(f"before/{name}.ttf")
    new = TTFont(f"fonts/ttf/{name}.ttf")
    changed = sorted(t for t in old.reader.keys() if old.reader[t] != new.reader[t])
    print(name, changed)

The list should contain the tables your fix needs plus head (checksum and modified time), and nothing else.

To list the glyphs whose bytes changed, compare raw glyf slices through loca:

la, lb = old["loca"], new["loca"]
ra, rb = old.reader["glyf"], new.reader["glyf"]
changed = [n for i, n in enumerate(old.getGlyphOrder())
           if ra[la[i]:la[i + 1]] != rb[lb[i]:lb[i + 1]]]

The standard and ligature builds must still match:

for weight in ["Regular", "Bold"]:
    std = TTFont(f"fonts/ttf/D2Coding-{weight}.ttf")
    lig = TTFont(f"fonts/ttf/D2Codingligature-{weight}.ttf")
    assert all(std.reader[t] == lig.reader[t] for t in ["glyf", "loca", "hmtx", "cmap", "maxp", "hhea"])

cmap

Compare every subtable, not only the best Unicode one, and compare GIDs rather than glyph names (names are invented on load and can change):

for s1, s2 in zip(old["cmap"].tables, new["cmap"].tables):
    removed = set(s1.cmap) - set(s2.cmap)
    moved = [hex(k) for k, v in s2.cmap.items()
             if k in s1.cmap and old.getGlyphID(s1.cmap[k]) != new.getGlyphID(v)]
    print(s1.platformID, s1.platEncID, len(removed), moved)

2. Shaping with HarfBuzz

site/data.js holds the list of ligatures the page shows, discovered from GSUB and verified with HarfBuzz. Shape each one in several contexts with the old and new font and compare glyph IDs and advances:

import json, re
import uharfbuzz as hb

def load(path):
    return hb.Font(hb.Face(hb.Blob.from_file_path(path)))

def shape(font, text, features, script="Latn"):
    buf = hb.Buffer()
    buf.add_str(text)
    buf.guess_segment_properties()
    buf.script = script
    hb.shape(font, buf, features)
    return [(i.codepoint, p.x_advance) for i, p in zip(buf.glyph_infos, buf.glyph_positions)]

ligatures = json.loads(re.search(r"const LIGATURES = (\[.*?\]);", open("site/data.js").read()).group(1))
old, new = load("before/D2Codingligature-Regular.ttf"), load("fonts/ttf/D2Codingligature-Regular.ttf")
for lig in ligatures:
    for text in [lig, f"a{lig}b", f"x {lig} y", f"({lig})", lig + lig, f"가 {lig} 나"]:
        for features in [{}, {"calt": False, "liga": False}]:
            assert shape(old, text, features) == shape(new, text, features), text

Set script="Hang" to see what an engine that shapes a Korean line as one Hangul run does. Since liga was added, the result should equal the Latin one.

3. Rasterizing with FreeType

Hinting problems show at specific sizes. Measure where each glyph's bitmap sits relative to the baseline across a range of ppem:

import freetype

face = freetype.Face("fonts/ttf/D2Coding-Regular.ttf")
for ch in "inmі":
    bottoms = {}
    for ppem in range(12, 25):
        face.set_pixel_sizes(0, ppem)
        face.load_char(ch, freetype.FT_LOAD_RENDER | freetype.FT_LOAD_TARGET_NORMAL)
        bottoms[ppem] = face.glyph.bitmap_top - face.glyph.bitmap.rows
    print(ch, bottoms)

A glyph that sits on the baseline prints 0 at every size. Issue #107 shows up as a 1 at 18 ppem for i in Regular.

4. Rendering in a browser

Regenerate the web fonts and the page data, serve the site, and look:

cd site
python3 tools/build-data.py ../fonts/ttf
python3 -m http.server 8000

For a quick check of specific characters, put a small HTML file next to site/ that loads site/fonts/D2Coding-Regular.woff2 and shows the characters on a striped background one column wide, then take a headless screenshot:

google-chrome --headless=new --hide-scrollbars --window-size=900,300 \
  --virtual-time-budget=5000 --screenshot=check.png http://localhost:8000/check.html

Two things to know:

  • Add a query string to the font URL (D2Coding-Regular.woff2?v=2) so the browser does not reuse a cached copy.
  • If D2 Coding is installed on the machine, characters missing from the web font fall back to the installed copy. After the control characters were unmapped they still appeared in the screenshot, drawn by an older installed D2 Coding. Check the web font's cmap before concluding a change did not work.

After pushing to master, the Deploy specimen page workflow publishes site/. Compare the live web fonts with your local ones:

curl -s https://naver.github.io/d2-coding-font/fonts/D2Coding-Regular.woff2 | sha256sum
sha256sum site/fonts/D2Coding-Regular.woff2

5. fontbakery

Google Fonts checks submissions with fontbakery. Individual checks are worth reading even without running the whole profile; for example control_chars fails for any glyph named uni0001 to uni001F other than uni000D.

Clone this wiki locally