How to automate name table cleanup in your build pipeline
- Step 1Fetch the tool schema from the API — `GET /api/v1/tools/name-table-cleaner` with your API key returns the tool descriptor including its (empty) options list and runner-backed execution shape. Use it to confirm the tool takes a file and no options — there is nothing to configure, so your payload is just the font.
- Step 2Pair the runner once — Install and pair `@jadapps/runner` on the build machine. The API/MCP layer dispatches the job to your local runner; the font binary stays on your infrastructure and never touches JAD servers. POSTing content to `/api/v1/tools/name-table-cleaner/run` returns 400 with pairing instructions — that's intentional.
- Step 3Or port the keep-list locally with fontTools — If you'd rather not depend on the runner, replicate the logic: load the font, keep only name records where platformID==3, platEncID==1, langID==0x409, and nameID in {1,2,4,5,6,13,14}, drop the rest, and save. This matches the tool byte-for-byte in intent.
- Step 4Walk the fonts directory — Iterate every `.ttf`/`.otf` (decompress `.woff`/`.woff2` first if your sources are web fonts). Apply the keep-list, write `<stem>.cleaned.ttf`. Skip anything that already has exactly the 7 records to keep the step idempotent.
- Step 5Validate the output — Re-open each cleaned font and assert nameID 1 (family) and 4 (full name) resolve to non-empty strings under (3, 1, 0x409). If a source font had no Windows-English records, the conversion should raise — treat that as a build failure, not a silent skip.
- Step 6Compress last — Because the tool (and a faithful port) outputs uncompressed TTF, add a final WOFF2 step. In CI that's a fontTools `woff2.compress` call or the JAD [ttf-to-woff2](/font-tools/ttf-to-woff2) runner step. Compressing last means every byte removed upstream stays removed.
API / runner surface for name-table-cleaner
Font tools are runner-backed: the API exposes the schema, the paired runner executes locally, uploads never reach JAD.
| Call | Method | Behaviour |
|---|---|---|
/api/v1/tools/name-table-cleaner | GET | Returns tool descriptor: category font, runner-backed execution, options list (empty — the tool has no configurable fields) |
/api/v1/tools/name-table-cleaner/run | POST | Upload-free by design — returns 400 with pairing instructions; real execution happens on your paired @jadapps/runner |
| @jadapps/runner (local) | — | Decompresses the font if needed, rebuilds the name table, returns the cleaned TTF; the binary never leaves your machine |
The keep-list to replicate in code
A record survives only if it matches the address AND has a kept nameID. This is the exact rule the live tool uses.
| Condition | Required value | If not matched |
|---|---|---|
| platformID | 3 (Windows) | Record dropped |
| platEncID (encodingID) | 1 (Unicode BMP) | Record dropped |
| langID | 0x409 (English-US) | Record dropped |
| nameID | one of 1, 2, 4, 5, 6, 13, 14 | Record dropped |
| Total kept records == 0 | (must be > 0) | Raise / fail — do not emit a nameless font |
Tier limits relevant to a batch CI run
File-size and batch limits by tier (font family). Single font weights are tiny, so file size is rarely the constraint; batch count matters for parallel CI.
| Tier | Max file size | Batch files |
|---|---|---|
| Free | 5 MB | 1 |
| Pro | 50 MB | 20 |
| Pro + Media | 200 MB | 100 |
| Developer | 1 GB | unlimited |
| Enterprise | unlimited | unlimited |
Cookbook
Drop-in recipes: a faithful fontTools port of the exact keep-list, a fontkit/Node equivalent, and the CI wiring. The Python version is the one to copy if you want to match the tool without the runner.
fontTools port — exact keep-list
ExampleThis reproduces the tool's behaviour in Python: keep only (3, 1, 0x409) records for nameIDs 1, 2, 4, 5, 6, 13, 14, output a TTF, and raise if nothing survives.
from fontTools.ttLib import TTFont
KEEP_IDS = {1, 2, 4, 5, 6, 13, 14}
def clean_name_table(in_path, out_path):
f = TTFont(in_path) # handles .ttf/.otf; for woff set flavor None on save
name = f["name"]
kept = [r for r in name.names
if r.platformID == 3 and r.platEncID == 1
and r.langID == 0x409 and r.nameID in KEEP_IDS]
if not kept:
raise SystemExit("No Windows English name records to keep")
name.names = kept
f.flavor = None # output uncompressed sfnt (TTF)
f.save(out_path)
clean_name_table("Brand-Regular.ttf", "Brand-Regular.cleaned.ttf")fontkit / Node read-back validation
ExampleAfter cleaning, re-open and assert the identity records survived. Use this as a CI gate.
// node, using fontkit
const fontkit = require('fontkit');
const font = fontkit.openSync('Brand-Regular.cleaned.ttf');
if (!font.familyName) throw new Error('family (nameID 1) missing');
if (!font.fullName) throw new Error('full name (nameID 4) missing');
console.log('OK:', font.familyName, '/', font.fullName);Batch a fonts directory then compress
ExampleWalk every weight, clean, then WOFF2-compress. Mirrors the tool's TTF-only output plus a separate compression step.
#!/usr/bin/env bash
set -euo pipefail
for f in src/fonts/*.ttf; do
stem=$(basename "$f" .ttf)
python clean_name_table.py "$f" "build/${stem}.cleaned.ttf"
# compress last
python -c "from fontTools.ttLib import woff2; \
woff2.compress('build/${stem}.cleaned.ttf', 'public/fonts/${stem}.woff2')"
doneIdempotency guard
ExampleSkip fonts already reduced to the 7 keep-list records so re-runs are no-ops and CI stays fast.
from fontTools.ttLib import TTFont
KEEP_IDS = {1, 2, 4, 5, 6, 13, 14}
def already_clean(path):
name = TTFont(path)["name"].names
return all(r.platformID == 3 and r.platEncID == 1
and r.langID == 0x409 and r.nameID in KEEP_IDS
for r in name) and len(name) <= len(KEEP_IDS)
# in the loop: if already_clean(f): continueAPI schema fetch (confirm no options)
ExampleConfirm the tool takes a file and no options before wiring the runner payload. Useful as a one-time check in setup.
curl -s -H "Authorization: Bearer $JAD_API_KEY" \ https://jadapps.example/api/v1/tools/name-table-cleaner | jq '.input.options' # -> [] (empty: the tool has no configurable fields) # Execution is runner-backed; POST /run returns 400 + pairing steps.
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.
POSTing the font to /run returns 400
By designThe /api/v1/tools/name-table-cleaner/run endpoint is upload-free. JAD's API/MCP layer never receives file content — every font tool executes through your paired @jadapps/runner locally. A POST with content returns 400 with pairing instructions. Build your CI around the runner, not around uploading bytes to JAD.
Tool always outputs uncompressed TTF
By designWhether you feed it a WOFF2 or a TTF, the output is an uncompressed .cleaned.ttf. There is no compression step inside the tool. In CI you must add WOFF2 compression afterwards (fontTools woff2.compress or the ttf-to-woff2 runner step), or you'll ship a larger file than you started with.
Font with no Windows-English records fails the build
Error (intended)If a source font carries only Mac-platform or non-English records, filtering leaves zero records and the tool raises No Windows English name records to keep. Your CI should treat this as a hard failure, not a skip — silently shipping the original would leave the bloat, and silently shipping a nameless font would break matching. Fix the source font (add a (3, 1, 0x409) record) and re-run.
Forgetting to decompress WOFF/WOFF2 in a fontTools port
Handle in codeThe live tool decompresses WOFF/WOFF2 automatically. A naive fontTools port that opens a .woff2 directly works (fontTools reads it) but you must set font.flavor = None before saving to emit a TTF rather than re-wrapping as WOFF2. Forgetting this produces a WOFF2 output, not the TTF the tool would give you.
head.checkSumAdjustment differs from a strict rebuild
Cosmetic mismatchThe live tool does not recompute head.checkSumAdjustment after editing. fontTools' save() does recompute it. So a byte-diff between the tool's output and your fontTools port will show a different head checksum even when the name table is identical. Both render fine; if you assert byte-equality in tests, exclude the head checksum or normalise both through a validator.
Variable-font axis names disappear
Removed by designnameIDs 256+ (variable-font axis and instance labels) are not in the keep-list, so cleaning a variable font drops them. The fvar axes still work, but design tools that read the labels show blanks. If your CI ships variable fonts meant to be edited downstream, exclude them from the name-table-clean step or add 256+ to your port's keep-list deliberately.
Free-tier batch limit of 1
Tier limitOn the free tier the font batch limit is 1 file at a time and 5 MB per file. For a multi-weight CI run you'll want Pro (20 files, 50 MB) or higher. A faithful local fontTools port has no such limit — it runs on your machine — which is another reason teams often port the logic for unattended builds.
Re-run produces zero dropped records
Expected (idempotent)Running the clean step twice on the same font is safe: the second pass finds the same 7 records, keeps them, and drops nothing. The metrics show Records dropped == 0 and ~0% size reduction. Use the idempotency guard above to skip the work entirely and keep CI fast.
Frequently asked questions
Does JAD's API receive my font file?
No. The API/MCP layer is upload-free. GET /api/v1/tools/name-table-cleaner returns only the tool schema; the actual transform runs on your paired @jadapps/runner on your own machine. POSTing content to the /run endpoint returns 400 with pairing instructions. The font binary, including embedded licensing strings, never touches JAD servers.
What options does the tool accept in an automated payload?
None. The schema lists an empty options array — there are no configurable fields. The keep-list (platform 3, encoding 1, lang 0x409, nameIDs 1/2/4/5/6/13/14) is fixed in code. Your payload is just the font; there's nothing to parameterise.
How do I reproduce the exact behaviour in CI without the runner?
Port the keep-list with fontTools (Python) or fontkit (Node): keep only name records where platformID==3, platEncID==1, langID==0x409, and nameID is in {1, 2, 4, 5, 6, 13, 14}; drop the rest; output an uncompressed TTF; and raise if nothing survives. The fontTools recipe in the cookbook above matches the tool's intent line for line.
Why does my port's output differ byte-for-byte from the tool's?
Most likely the head.checkSumAdjustment field. The live tool doesn't recompute it after editing; fontTools' save() does. The name tables are identical, but the head checksum differs. Both are valid for every real consumer. If you diff outputs in tests, normalise the head checksum or compare the decoded name records instead of raw bytes.
Do I need to decompress WOFF2 before processing?
The live tool does it for you. In a fontTools port, you can open a .woff2 directly (fontTools handles it), but you must set font.flavor = None before saving to emit a TTF rather than re-compressing as WOFF2. The tool's contract is always-TTF output, so match that and add a separate WOFF2 step at the end of your pipeline.
Where should the clean step sit in a TTF -> WOFF2 build?
Early. Clean the name table on the TTF/OTF source, then strip hinting with hinting-stripper, subset with font-subsetter, and compress to WOFF2 last with ttf-to-woff2. Removing name-table bytes before compression means the final WOFF2 is marginally smaller, and compressing last preserves every upstream saving.
Will the clean step ever silently break a font?
No — it's designed to fail loudly. If filtering would leave zero name records, it raises rather than emit a nameless font. Make your CI treat that error as a build failure. The only 'silent' changes are intentional removals (copyright, localised strings, variable-font axis names), which you should be aware of before adopting the step.
How do I validate the cleaned font in CI?
Re-open each output and assert nameID 1 (family) and 4 (full name) resolve under (3, 1, 0x409). With fontkit that's font.familyName and font.fullName; with fontTools, read font['name'].getDebugName(1) and (4). A missing family name means the clean went wrong (or the source had no Windows-English record) and the build should fail.
Can I keep additional nameIDs or languages in my port?
Yes — it's your code. To keep copyright (0) for licence compliance, add 0 to your keep set. To keep French for a European desktop deployment, add 0x40C to the language test. The live JAD tool can't do this (its keep-list is fixed), which is one reason teams port the logic when they need a different policy.
What are the batch and size limits if I do use the runner?
Font family limits: free is 5 MB/file and 1 file per batch; Pro is 50 MB and 20 files; Pro+Media 200 MB and 100; Developer 1 GB and unlimited batch; Enterprise unlimited. Font weights are tiny, so size is rarely the issue — batch count is what governs how many weights you can clean per Pro-tier run.
Is the operation idempotent?
Yes. A font already reduced to the 7 keep-list records passes through unchanged — the same 7 records are kept and nothing is dropped (0% reduction). Add an idempotency guard that skips fonts already in the canonical shape to keep re-runs fast, especially in incremental CI.
How do I monitor savings across the design system over time?
The tool reports Records before/kept/dropped and a size-reduction percentage per font. In a port, log those numbers per weight and sum them. Compare the final WOFF2 sizes (after compression) build-over-build — that's the number that actually reaches users. Pair with font-metadata-extractor to spot-check that identity records survived.
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.