How to generate the fonttools command for ci variable-font subsetting
- Step 1Run the optimiser on your source font — Upload the variable font once to capture the baseline `fonttools_command` and per-axis report. Keep the JSON in your repo as the documented intent for the font step.
- Step 2Lift the command into your build script — Copy `fonttools_command` into your CI job. Replace the trailing filename with the path your pipeline uses, and add an output flag per fontTools' CLI if you want a deterministic output name.
- Step 3Install fontTools in the CI image — Add `pip install fonttools` to your build setup. varLib.instancer is part of fontTools — no extra package needed for basic axis pinning and range limiting.
- Step 4Gate on Best axis savings — Read the **Best axis savings** metric. If it's 0%, the instance ladder already spans the full design space — skip the trim step to avoid needless work.
- Step 5Re-compress to WOFF2 in the same job — After instancing, pipe the TTF through [ttf-to-woff2](/font-tools/ttf-to-woff2) (or wawoff2 in your build) so the deployed asset is the compressed, trimmed font.
- Step 6Diff the report on font updates — When the upstream font changes, re-run the optimiser and diff the new `<font>.axis-ranges.json` against the committed one. A changed axis range or savings figure flags an instance-ladder change you should review before shipping.
Pipeline step mapping
Which part of the workflow each tool/command owns. The optimiser plans; fontTools executes.
| Stage | Tool / command | Runs where |
|---|---|---|
| Plan ranges + emit command | Axis Range Optimiser (reads fvar) | Browser (once, or as a check) |
| Apply axis trim | fonttools varLib.instancer | CI build server (Python) |
| Compress to WOFF2 | ttf-to-woff2 / wawoff2 | CI build server or browser |
| Wire up @font-face | font-face-generator | Once, committed to repo |
| Verify glyph coverage | character-coverage-map | Spot-check after trim |
Report fields a CI job can act on
The JSON report and metrics expose values a script can read to make decisions.
| Value | Type | Pipeline use |
|---|---|---|
fonttools_command | string | The command to exec in the build step |
axes[].range_savings_pct | number | Per-axis gate: skip pinning an axis already at 0% |
| Best axis savings (metric) | percentage | Top-level gate: skip the whole step if 0% |
note | string | Documents why trimming runs on the desktop, not in-browser |
filename <font>.axis-ranges.json | string | Commit and diff to detect upstream font changes |
Cookbook
Scripting the emitted command into common CI setups. The optimiser supplies the ranges; fontTools and your build runner do the rest.
npm build script
ExampleCapture the command once, then bake it into package.json. Re-run the optimiser when the font updates to refresh the ranges.
// package.json
"scripts": {
"font:trim": "fonttools varLib.instancer wght=400:700 wdth=100:100 src/fonts/Brand.ttf -o build/Brand-trim.ttf",
"font:woff2": "woff2_compress build/Brand-trim.ttf",
"font:build": "npm run font:trim && npm run font:woff2"
}Makefile target
ExampleA make rule that only rebuilds the font when the source changes. The instancer command is the one the optimiser emitted.
build/Brand-trim.woff2: src/fonts/Brand.ttf fonttools varLib.instancer wght=400:700 src/fonts/Brand.ttf -o build/Brand-trim.ttf woff2_compress build/Brand-trim.ttf mv build/Brand-trim.woff2 $@
GitHub Actions step
ExampleInstall fontTools, then run the emitted command. Pin the fontTools version for reproducible CI.
- name: Trim variable font axes
run: |
pip install fonttools==4.* fontTools[woff]
fonttools varLib.instancer wght=400:700 src/fonts/Brand.ttf -o Brand-trim.ttf
python -m fontTools.ttLib.woff2 compress Brand-trim.ttfGating the step on Best axis savings
ExampleParse the report's metric and skip the trim entirely when there's nothing to gain — avoids burning CI minutes on a no-op.
best=$(jq -r '[.axes[].range_savings_pct] | max' Brand.axis-ranges.json)
if [ "$best" -gt 0 ]; then
fonttools varLib.instancer $(jq -r '.fonttools_command | sub("fonttools varLib.instancer ";"")' Brand.axis-ranges.json)
else
echo "No axis slack — skipping trim"
fiDiffing the report on a font bump
ExampleWhen the upstream font updates, re-running the optimiser and diffing the JSON surfaces any change to the instance ladder before it ships.
# after updating src/fonts/Brand.ttf # re-run optimiser, save new report, then: git diff --no-index committed/Brand.axis-ranges.json new/Brand.axis-ranges.json # review changed recommended_min/max or range_savings_pct
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.
Static font reaches the CI font step
Error: not a variable fontIf an already-static font is passed through the planner it throws This font has no fvar table — it isn't a variable font. Guard the pipeline so the optimiser only runs on variable inputs.
No file in the planning step
Error: upload requiredThe tool throws Upload a variable font with no file. In CI, the equivalent is a missing source path — fail fast in the build script before invoking fontTools.
Best axis savings is 0%
Skip the trim stepWhen the instance ladder spans the full design space, every axis reports 0% and instancing the recommended ranges is a no-op. Gate the CI step on the metric so you don't waste a build.
fontTools not installed on the runner
fail: command not foundThe emitted command assumes fonttools is on PATH. Add pip install fonttools (with fontTools[woff] if you compress WOFF2 via fontTools) to the CI image, and pin a version for reproducibility.
Recommended range narrower than CSS usage
Clipping after instancingThe command reflects the named-instance span, not your CSS. If your site sets a coordinate outside that span, the trimmed font clips it. Edit the committed command's tag=min:max to your real range before merging.
Upstream font update changes the ladder
Report diff flags itA vendor font bump can add or move named instances, changing the recommended ranges. Diffing the new <font>.axis-ranges.json against the committed one catches this in review before a surprising trim ships.
Expecting the tool to output the font in CI
JSON onlyThe optimiser never emits a font — only JSON plus the command. Your pipeline must run fontTools to produce the asset; the tool's note field documents exactly why.
Source over the tier limit at plan time
400: file too largeIf you use the hosted tool to plan, files over the Pro 50 MB limit are rejected (Developer raises it to 1 GB). fontTools itself has no such limit on the build server — the cap is only on the browser planning step.
Frequently asked questions
Can the tool subset the font inside my CI directly?
No. It plans the ranges and emits a fonttools varLib.instancer command. Your CI runs that command with Python's fontTools to produce the trimmed font — browser-side variable subsetting isn't feasible.
Why use it in a pipeline if it doesn't write the font?
Because the planning is deterministic: the same font yields the same command and the same JSON report. That gives you a reviewable, diffable source of truth for the font step, with the heavy lifting delegated to fontTools.
How do I make the build skip the trim when it's pointless?
Gate on the Best axis savings metric (or the max of axes[].range_savings_pct). If it's 0%, the instance ladder already spans the full design space — skip the step.
What do I install on the CI runner?
pip install fonttools. varLib.instancer ships with fontTools. Add the fontTools[woff] extra if you also compress WOFF2 via fontTools; otherwise use wawoff2/woff2_compress.
How do I keep CI reproducible?
Pin the fontTools version (e.g. fonttools==4.x) and commit the emitted command and the <font>.axis-ranges.json report. The optimiser's output is deterministic for a given font, so pinning the toolchain makes the whole step reproducible.
What if the upstream font changes?
Re-run the optimiser and diff the new report against the committed one. A changed recommended range or savings figure signals the instance ladder moved — review before shipping the new trim.
Does the recommendation account for my actual CSS usage?
No — it's based on the font's named instances. If your CSS uses coordinates outside that span, edit the tag=min:max fragment in your build command so the trimmed font doesn't clip them.
Where does WOFF2 compression fit?
After instancing. Pipe the trimmed TTF through ttf-to-woff2 or your build's woff2 compressor as the next step, then deploy the compressed asset.
What output do I commit to the repo?
The <font>.axis-ranges.json report (for diffing) and the build command itself. The trimmed font is a build artifact produced by fontTools in CI.
What tier and size limits apply to the hosted planning step?
Pro tier; 5 MB Free, 50 MB Pro, 1 GB Developer. fontTools on your build server has no such limit — the cap applies only when planning via the browser tool.
Is my font sent to a server during planning?
No. The fvar parse runs in your browser. The report and command are generated locally from bytes that never leave the machine.
Should I verify glyph coverage after the trim?
Yes as a spot-check. Pinning axes removes variation data, not glyphs, but running character-coverage-map on the output confirms every codepoint still resolves.
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.