How to generate design tokens from glyph inspector output
- Step 1Parse the font with opentype.js — Load the sfnt and call `opentype.parse(buffer)`. For WOFF2/WOFF inputs, decompress to an sfnt first (wawoff2 for WOFF2, pako/zlib for WOFF) — the browser tool does the same before parsing.
- Step 2Walk the GlyphSet to the cap — Iterate `i` from 0 to `Math.min(font.glyphs.length, 5000)`. Mirror the safety cap if you want exact parity; raise it deliberately if you genuinely need every glyph of a huge font and accept the larger payload.
- Step 3Build a record per glyph — Per glyph capture `index`, `name ?? null`, `unicode` as `U+XXXX` (or null), `advanceWidth`, the `getBoundingBox()` extents, and `getPath(0, em, em).toPathData(2)` for the path. Wrap path building in try/catch so a contour-less glyph yields `""`, not a crash.
- Step 4Filter to drawable icons — For tokens you usually want only real icons: drop index 0 (`.notdef`) and records with an empty `svgPath` or `null` unicode, keeping PUA-encoded glyphs.
- Step 5Format as tokens — Emit W3C Design Tokens: `{ "icons": { "home": { "$value": "\\e001", "$type": "string" } } }`, deriving the escape from `unicode`. Style Dictionary consumes this directly; or emit one SVG component per icon from `svgPath`.
- Step 6Wire into prebuild and version it — Add a `tokens:icons` script and run it before build. Hash the source font bytes and stamp that into the tokens file so consumers can tell when the icon set changed. Commit the generated JSON for downstream packages.
Mirroring the inspector in Node
The browser handler's steps and the equivalent Node calls. Replicate these for output that matches the tool exactly.
| Inspector step | Node equivalent | Note |
|---|---|---|
| Decode WOFF2/WOFF → sfnt | wawoff2.decompress / pako.inflate per table | TTF/OTF need no decode; pass the buffer straight in |
| Parse sfnt | opentype.parse(arrayBuffer) | Throws on damaged table directory — wrap in try/catch |
| Read total + cap | const limit = Math.min(font.glyphs.length, 5000) | 5,000 is the tool's safety cap, not a tier limit |
| Per-glyph path | g.getPath(0, em, em).toPathData(2) | em = font.unitsPerEm; baseline at y = em; 2 decimals |
| Bounding box | g.getBoundingBox() → x1/y1/x2/y2 | Falls back to glyph table extents if non-finite |
| Codepoint | g.unicode != null ? formatU(g.unicode) : null | Primary codepoint only — not every mapping |
Output target options from one walk
What you can emit from the same GlyphRecord set, and the field each target consumes.
| Target | Driven by | Shape |
|---|---|---|
| W3C Design Tokens | name + unicode | { "$value": "\\e001", "$type": "string" } |
| Style Dictionary input | name + unicode | Nested icons.<name>.value JSON |
| CSS classes | name + unicode | .icon-home::before{content:"\e001"} |
| SVG components | svgPath + viewBox | One .svg/.tsx per icon — font-free |
| Catalogue JSON | whole record | Pass-through, same as the tool's download |
Cookbook
Node snippets that reproduce or consume the inspector's output. em is font.unitsPerEm.
Reproduce the inspector walk in Node
ExampleThe minimal loop that yields the same GlyphRecord shape the browser tool emits, capped at 5,000.
import opentype from "opentype.js";
const font = opentype.loadSync("icons.ttf");
const em = font.unitsPerEm;
const limit = Math.min(font.glyphs.length, 5000);
const U = (cp) => "U+" + cp.toString(16).toUpperCase().padStart(4,"0");
const glyphs = [];
for (let i = 0; i < limit; i++) {
const g = font.glyphs.get(i);
let svgPath = "";
try { svgPath = g.getPath(0, em, em).toPathData(2) ?? ""; } catch {}
glyphs.push({
index: i, name: g.name ?? null,
unicode: g.unicode != null ? U(g.unicode) : null,
advance: g.advanceWidth ?? null, svgPath,
viewBox: `0 0 ${g.advanceWidth || em} ${em}`,
});
}Emit W3C design tokens
ExampleKeep only named, PUA-encoded icons and write a tokens file Style Dictionary can read. The CSS escape is derived from unicode.
const tokens = { icons: {} };
for (const g of glyphs) {
if (!g.name || !g.unicode) continue;
const hex = g.unicode.slice(2).toLowerCase();
tokens.icons[g.name] = { $value: "\\" + hex, $type: "string" };
}
fs.writeFileSync("icons.tokens.json", JSON.stringify(tokens, null, 2));
// → { "icons": { "home": { "$value": "\\e001", "$type": "string" } } }Generate one SVG component per icon
ExampleSkip the icon font entirely: each record's svgPath + viewBox becomes a tree-shakeable SVG file.
for (const g of glyphs) {
if (!g.name || !g.svgPath) continue;
const svg = `<svg viewBox="${g.viewBox}" xmlns="http://www.w3.org/2000/svg">`
+ `<path d="${g.svgPath}"/></svg>`;
fs.writeFileSync(`icons/${g.name}.svg`, svg);
}
// icons/home.svg, icons/user.svg, ... — no font needed.Version tokens by the font hash
ExampleStamp a content hash of the source font into the tokens so consumers detect changes deterministically.
import { createHash } from "node:crypto";
const bytes = fs.readFileSync("icons.ttf");
const version = createHash("sha256").update(bytes)
.digest("hex").slice(0, 12);
tokens.$meta = { source: "icons.ttf", version };
// Same font in → same version out, on every machine.Drive the JAD runner instead of a script
ExampleIf the runner is paired, POST the font to the local endpoint and read back the same JSON the tool produces — processed on the developer's machine, never uploaded.
# Discover the option schema (glyph-inspector has none): curl http://127.0.0.1:9789/api/v1/tools/glyph-inspector # (the public schema lists this tool with an empty options array) # Run it locally against a font file: curl -X POST \ http://127.0.0.1:9789/v1/tools/glyph-inspector/run \ -F file=@icons.woff2 # → <stem>.glyphs.json saved locally by the runner.
Edge cases and what actually happens
Every row below was probed against the live API. Some documented requirements (alphabetical axis order, numerical tuple order) are not actually enforced in practice — useful to know if you've been blaming the wrong thing for a 400.
Script doesn't mirror the 5,000-glyph cap
Output divergenceThe browser tool stops at GLYPH_INSPECT_SAFETY_CAP = 5000. If your Node walk iterates the full font.glyphs.length, your tokens will include glyphs the tool omits — fine if intentional, surprising if you expected byte-for-byte parity. Decide deliberately: cap at 5,000 for parity, or raise it and accept the larger payload for huge fonts.
Treating `unicode` as a list of codepoints
Primary onlyEach record carries the glyph's primary codepoint, not every codepoint that maps to it. If your token generator assumes one token per codepoint and reads only unicode, aliased codepoints get no token. For exhaustive codepoint coverage, read font.tables.cmap.glyphIndexMap directly rather than relying on the per-glyph unicode.
Glyph with no contours crashes path building
Guard with try/catchCalling toPathData on a contour-less or unusual glyph can throw. The inspector wraps it and returns "". Reproduce that guard in your script — without it, one odd glyph (space, a malformed entry) aborts the whole token build instead of yielding an empty path.
Names are null because post was stripped
Names missingIf the build strips the post table, g.name is null and name-keyed tokens collapse (multiple null keys, or skipped icons). Detect it up front: if most records have name: null, fail the build with a clear message, or fall back to icon-<hex> names derived from unicode. Confirm with the font-metadata-extractor.
WOFF2 input but no decompressor wired
Parse failureopentype.js parses sfnt (TTF/OTF) directly but not WOFF2/WOFF. If your CI fonts are WOFF2, decompress with wawoff2 (Brotli) first; WOFF needs pako/zlib per table. Without that step, parse throws on the compressed bytes. The browser tool does this decode before parsing — replicate it.
Non-deterministic token output across machines
Idempotency breakObject-key order, float formatting, or unsorted iteration can make the tokens JSON differ run-to-run, producing noisy diffs. Pin precision to 2 decimals (as the tool does via toPathData(2)), sort keys before serialising, and stamp a font-hash version. Same font in → same JSON out is the goal.
Runner not paired / endpoint refused
Connection errorThe 127.0.0.1:9789 endpoint only exists when the JAD runner is installed and paired. If it isn't running, the POST fails with a connection refused. Fall back to the in-script opentype walk, or start the runner. The runner path keeps the font local; it isn't a hosted API.
Variable font baked at the wrong instance
Default masteropentype reads default-axis outlines, so a variable icon font yields its default weight's paths. If your tokens need a specific weight, instance the font first (e.g. fonttools varLib.instancer, or the variable-font-freezer) before the walk, or your SVG components will all be the default weight.
Frequently asked questions
Can I reproduce the inspector exactly in a script?
Yes. Parse the sfnt with opentype.js, iterate glyphs from index 0 to min(font.glyphs.length, 5000), and per glyph capture index, name ?? null, the primary unicode as U+XXXX, advanceWidth, getBoundingBox() extents, and getPath(0, em, em).toPathData(2) for the path (em = font.unitsPerEm). Wrap path building in try/catch so contour-less glyphs yield "". That mirrors the tool's GlyphRecord shape field-for-field.
What's the 5,000 number and should my script use it?
It's GLYPH_INSPECT_SAFETY_CAP, the tool's hard limit on serialised records — applied on every tier, not a paywall. For output that matches the browser tool, cap your loop at 5,000 too. Raise it only if you deliberately need every glyph of a very large font and accept a bigger payload. Either way, report total_glyphs alongside sampled like the tool does.
How do I turn records into Style Dictionary tokens?
Keep named, encoded icons and build a nested object: tokens.icons[name] = { $value: "\\" + hex, $type: "string" }, where hex is the lowercase hex from the record's unicode (U+E001 → e001). Write it as JSON; Style Dictionary reads that structure directly and your platform transforms produce the CSS/Swift/Android outputs.
What if the font has no glyph names?
If post/CFF names were stripped, name is null and name-keyed tokens break. Detect it (most records null) and either fail the build with a clear error or fall back to icon-<hex> names derived from unicode. The durable fix is to re-export the icon font keeping glyph names — coordinate with whoever owns the icon build.
Can I generate React/SVG components instead of an icon font?
Yes, and many teams prefer it for tree-shaking. Each record's svgPath + viewBox is a complete outline; emit one .svg or .tsx per icon with the path inlined. That drops the icon font binary entirely — no PUA escapes, no font loading, just components. The inspector's path output is exactly the input you need.
How do I make the output deterministic for code review?
Pin path precision to 2 decimals (the tool uses toPathData(2)), sort object keys before serialising, iterate glyphs in index order, and stamp a SHA-256 hash of the source font into the file. Same font bytes then produce byte-identical tokens on every machine, so a diff means the icon set actually changed.
Do I have to write a script at all?
No. If the JAD runner is paired, POST the font to http://127.0.0.1:9789/v1/tools/glyph-inspector/run and read back the same <stem>.glyphs.json the tool produces. The font is processed locally on the developer's machine and never uploaded. glyph-inspector takes no options, so the run is just the file.
How do I handle WOFF2 inputs in CI?
Decompress before parsing: wawoff2 (Brotli) for WOFF2, pako/zlib per table for WOFF — opentype.js parses sfnt, not the compressed wrappers. The browser tool does this decode step first; replicate it or your parse call throws on the compressed bytes. TTF and OTF need no decode.
Why does my script include glyphs the tool doesn't?
Almost always because you didn't apply the 5,000-glyph cap, so you walked the full glyph set while the tool stopped at 5,000. The other common cause is not filtering .notdef (index 0) and empty-path glyphs. Apply the same cap and filters to match the tool's output.
Can I get every codepoint that maps to a glyph, not just the primary?
Not from the per-glyph unicode field — that's the primary mapping only. Read font.tables.cmap.glyphIndexMap directly to enumerate every codepoint → glyph-index pair, then group by glyph index. That's the same data the character-coverage-map uses to score coverage against 346 Unicode blocks.
How do I version the tokens so consumers know when icons changed?
Hash the source font's bytes (SHA-256) and stamp a short prefix into the tokens file's metadata. Because the same font produces the same hash on any machine, consumers can compare versions deterministically and rebuild only when the icon set actually changed. Don't version by timestamp — that churns on every run.
Is the runner endpoint a hosted API I can call from a server?
No — it's the local runner on 127.0.0.1:9789, paired to the developer's machine. It exists to keep font processing local and private, not as a cloud service. For server-side automation, run the opentype walk directly in your CI process; for a developer's local prebuild, the runner endpoint is convenient and keeps the font off any network.
Privacy first
Every JAD Font tool runs entirely in your browser using opentype.js and the wawoff2 WASM Brotli encoder. Your fonts never leave your device — verified by zero outbound network requests during processing.