How to latin filter edge cases and quirks reference
- Step 1Check coverage before you subset — Run [character-coverage-map](/font-tools/character-coverage-map) (it scores against 346 real Unicode blocks) to confirm the font actually has Latin Basic + Latin-1 glyphs. This pre-empts the 'Subset would be empty' error and the silent missing-glyph case in one step.
- Step 2Identify the outline flavour — Use [font-format-identifier](/font-tools/font-format-identifier) or [font-metadata-extractor](/font-tools/font-metadata-extractor) to see if the font is TrueType (`glyf`) or CFF (PostScript). CFF/OTF can fail the opentype.js writer — if so, plan to convert to TTF outlines first.
- Step 3Decide whether kerning loss is acceptable — If the font is for body text, the dropped `GPOS` rarely shows. If it's a display face, audit the pairs you care about with [kerning-pair-auditor](/font-tools/kerning-pair-auditor) before committing — the Latin Filter can't preserve them.
- Step 4Run the filter and read the metrics panel — Watch `Glyphs kept` (should be ≤192 including .notdef) and the note `Kerning + OT layout features dropped; outlines preserved`. A `Glyphs kept` of 1 means only .notdef survived — the subset is effectively empty and the tool will have errored.
- Step 5Re-compress and re-test — Convert the TTF to WOFF2 with [ttf-to-woff2](/font-tools/ttf-to-woff2), then render your real content. The TTF being bigger than a WOFF2 input is expected; the win shows after compression.
- Step 6Escalate to the right tool when blocked — Empty subset → [character-coverage-map](/font-tools/character-coverage-map). CFF writer failure → convert to TTF, or [character-whitelist-builder](/font-tools/character-whitelist-builder) with fewer glyphs. Kerning required → harfbuzz subsetter (see [the batch guide](/font-tools/guides/latin-filter-batch-script)). Need Extended-A → [font-subsetter](/font-tools/font-subsetter).
Error vs expected-behaviour quick reference
What actually happens for each scenario, and whether it's a hard error or designed behaviour. Status colour follows the renderer convention.
| Scenario | What happens | Type |
|---|---|---|
| Font has no Latin glyphs | Throws 'Subset would be empty — no glyphs match (Latin)' | Error |
| CFF / PostScript outlines (OTF) | May throw in opentype.js writer | Error |
| File > tier size limit | Throws 'exceeds the … tier per-job limit' | Error |
| WOFF2 input | Decompressed, subset, written as TTF | Expected |
| Kerning / ligatures | Dropped (GPOS/GSUB not rebuilt) | By design |
| Variable font | Flattened to default instance | By design |
| Font lacks a specific Latin-1 char (ð/þ) | Glyph silently not kept; falls back at render | By design |
| Extended-A char requested (œ, ł) | Out of range; never kept | By design |
The kept range — and what's deliberately outside it
The fixed Latin preset. Anyone hunting a 'missing' glyph should check it against this first.
| Codepoints | Status | Notes |
|---|---|---|
U+0020–U+007E | Kept | Printable ASCII (95 chars) |
U+00A0–U+00FF | Kept | Latin-1 Supplement incl. NBSP, accents (96 chars) |
U+0000–U+001F, U+007F, U+0080–U+009F | Never kept | Control codes / C1 block — not glyphs |
U+0100–U+024F (Extended-A/B) | Never kept | œ ł ğ ş — use font-subsetter latin-ext |
U+0152, U+0153 (Œ, œ) | Never kept | French ligature lives in Extended-A |
.notdef (glyph 0) | Always kept | Required by spec; counts toward 'Glyphs kept' |
Result metrics decoder
Each field in the result panel and what an unexpected value tells you.
| Metric | Normal | If it looks wrong |
|---|---|---|
| Glyphs kept | ≤192 (incl. .notdef) | = 1 → only .notdef survived (effectively empty) |
| Glyphs in source | any | Very high (10k+) and tiny kept → mostly non-Latin font |
| Subset (KB) | < Source | > WOFF2 input → expected (TTF is uncompressed) |
| Note | 'Kerning + OT layout features dropped' | Always present — confirms layout loss |
Cookbook
Worked examples of the quirks, with the actual error text or metrics where the tool produces them. All processing is local.
Empty subset on a CJK-only font
ExamplePointing the Latin Filter at a font with no Latin coverage triggers the empty-subset guard. The fix is to check coverage first, or use a font that actually has Latin glyphs.
Input: SourceHanSans-Regular.otf (CJK, no Latin)
Result: Error -
"Subset would be empty - no glyphs in this font match
the requested charset (Latin). Run the Character
Coverage Map tool first to confirm coverage."
Fix: character-coverage-map confirms 0 glyphs in
U+0020-00FF -> this font can't be Latin-filtered.CFF/OTF writer failure
ExampleAn OTF with PostScript outlines can fail when opentype.js serialises the subset. The error names the cause and the workaround.
Input: BrandSerif.otf (CFF outlines)
Result: Error -
"Subsetting failed in opentype.js's font writer (...).
This typically affects fonts with CFF (PostScript)
outlines or non-standard tables. Convert to TTF first
using a desktop tool, or use the Character Whitelist
Builder with a smaller charset."
Fix: FontForge -> Generate as TTF (TrueType outlines)
-> re-run Latin Filter.WOFF2 in, bigger TTF out
ExampleUploading an already-compressed WOFF2 returns a larger uncompressed TTF. Reading this as a regression is the most common misunderstanding.
Input: Inter.woff2 22 KB Output: Inter.latin.ttf 48 KB (!?) Metrics: Glyphs in source: 2,548 Glyphs kept: 189 Source: 22.0 KB Subset: 48.0 KB <- TTF, uncompressed Reality: not a regression. Compress it: ttf-to-woff2 -> Inter.latin.woff2 ~16 KB (< original)
Variable font flattened
ExampleA variable font goes in; a single static instance comes out. The weight axis is gone because the rebuild copies only outlines and core metrics.
Input: InterVariable.ttf (wght 100-900 axis)
Output: InterVariable.latin.ttf (default instance only,
~400 weight)
The fvar/gvar tables are not carried over.
To keep a specific weight:
1. variable-font-freezer -> Inter-700.ttf
2. Latin Filter -> Inter-700.latin.ttfSilent missing glyph within the range
ExampleIcelandic ð/þ are inside the kept range but absent from many fonts. The filter keeps what exists; the rest just isn't there — no error, but a box at render time.
Font lacks U+00F0 (ð) and U+00FE (þ). Metrics: Glyphs kept: 187 (< 192 — two Latin-1 chars absent) Render of Icelandic 'Þingvellir' / 'það': □ingvellir / þa□ <- boxes for the missing glyphs This is a font-coverage gap, not a tool bug. Check with character-coverage-map before relying on it.
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.
Font has zero Latin coverage
Error — empty subsetWhen no glyph maps to a codepoint in U+0020–U+00FF, the tool refuses to write a degenerate font and throws: 'Subset would be empty — no glyphs in this font match the requested charset (Latin). Run the Character Coverage Map tool first to confirm coverage.' Typical with CJK-only, icon, or symbol fonts. Run character-coverage-map to see what the font does cover.
CFF / PostScript outline font fails the writer
Error — writer limitationopentype.js's font writer can throw on CFF (PostScript) outlines or non-standard tables: 'Subsetting failed in opentype.js's font writer ...'. The error itself suggests converting to TTF first or using character-whitelist-builder with a smaller charset. Most .otf files are CFF; converting to TrueType outlines in FontForge before subsetting is the reliable fix.
Output is a TTF even though you uploaded WOFF2
By designThe tool always emits font/ttf. WOFF/WOFF2 inputs are decompressed to operate on outlines, then re-serialised as TTF. The uncompressed TTF can be larger than a compressed WOFF2 input — not a regression. Always finish with ttf-to-woff2; the final WOFF2 is smaller than the original because the glyph set shrank.
Kerning and ligatures gone
By designThe rebuild copies glyph outlines plus unitsPerEm/ascender/descender — not GPOS (kerning) or GSUB (ligatures, stylistic sets). So AV/To kerning and fi/fl ligatures vanish. The result note states it explicitly. Invisible for body text; visible on display headlines. Preserve layout with a harfbuzz subsetter (see batch guide); audit with kerning-pair-auditor.
Variable font collapses to one weight
By designThe subset is built as a static font from the default instance — fvar/gvar variation data isn't carried over, so a variable font loses its axes. Freeze the weight you want with variable-font-freezer first, then Latin-filter the static result. Otherwise you get only the default (usually ~400) weight.
A character in range is missing from the font
By designThe filter only keeps glyphs the font actually has. Icelandic ð/þ, the currency ¤, or some symbols sit in Latin-1's range but are absent from many fonts. 'Glyphs kept' comes in under 192 and those characters render via the CSS fallback chain — no error, just a box if no fallback covers them. Check with character-coverage-map before relying on a glyph.
Looking for œ, ł, ğ — they were never in scope
By designFrench œ/Œ, Polish ł, Turkish ğ are Latin Extended-A (U+0100+), outside the filter's fixed U+0020–U+00FF range. They're not 'missing' — they were never requested. Use font-subsetter with latin-ext; the Latin-1 vs Extended guide maps which languages need it.
Hinting dropped along with layout tables
By designThe rebuild also omits fpgm/prep/cvt, so TrueType hinting is gone — the same effect as hinting-stripper. On modern macOS/iOS/Windows (DirectWrite) this is invisible; only legacy Windows 7/8 GDI + ClearType users at small sizes might notice. No separate de-hinting step is needed after Latin-filtering.
File exceeds the tier size limit
Error — tier limitFree caps uploads at 5 MB, Pro at 50 MB, Developer at 1 GB. A large CJK or unfrozen variable font can exceed the free limit before subsetting: 'exceeds the free tier per-job limit (5 MB).' Freeze the variable font first with variable-font-freezer, pre-shrink in a desktop tool, or upgrade tier.
Combining-accent text mispositions after subset
By designPrecomposed letters (é = U+00E9) are kept and render fine. But text using base + combining mark (e + U+0301) relies on GPOS mark positioning, which is dropped — the accent may sit wrong. Normalise content to NFC (precomposed) before relying on the subset, or use a layout-preserving harfbuzz subsetter.
Frequently asked questions
What does 'Subset would be empty' mean?
No glyph in the font maps to any codepoint in U+0020–U+00FF, so the filter has nothing to keep and refuses to write a degenerate font. It happens with CJK-only, icon, or symbol fonts. Run character-coverage-map to see what the font actually covers — if it has no Latin, this tool isn't the right one.
Why did my OTF fail with a 'font writer' error?
OTF files usually have CFF (PostScript) outlines, and opentype.js's writer can throw on them or on non-standard tables. The error suggests the fix: convert the OTF to TrueType-outline TTF (e.g. in FontForge) and re-run, or use character-whitelist-builder with a smaller charset. TrueType-outline fonts (.ttf) don't hit this.
Why is the output TTF bigger than my WOFF2 input?
Because TTF is uncompressed and WOFF2 is brotli-compressed. The tool always writes TTF, so a small WOFF2 input becomes a larger TTF output — that's expected, not a regression. Run ttf-to-woff2 on the result; the final WOFF2 will be smaller than your original because the glyph set is smaller.
Why did kerning stop working?
The opentype.js rebuild copies glyph outlines and core metrics but not the GPOS/GSUB layout tables, so kerning pairs and ligatures are dropped — the result note says so. For body text it's usually invisible; for display sizes it can show. If you need kerning, use a harfbuzz-based subsetter (pyftsubset/hb-subset) per the batch-script guide.
Why does my variable font come back as a single weight?
Subsetting builds a static font from the default instance; the fvar/gvar variation tables aren't carried over, so the weight axis is gone. Freeze the specific weight you want with variable-font-freezer first, then Latin-filter that static font. Repeat per weight you need.
A character in Latin-1 is missing from my subset — why?
The filter only keeps glyphs the source font contains. Some fonts omit Latin-1 characters like ð, þ, or ¤ even though they're in range. 'Glyphs kept' will be under 192 and those characters fall back to the next font in your CSS stack. Verify coverage with character-coverage-map before depending on a specific glyph.
Where are œ, ł, and the Turkish letters?
Outside the filter's range. œ/Œ, ł, ğ, ş, and the rest of Central/Eastern European and full-French letters live in Latin Extended-A (U+0100+), which the fixed U+0020–U+00FF preset doesn't touch. Use font-subsetter with latin-ext — see the Latin-1 vs Extended guide.
Does it remove hinting too?
Yes — the rebuild doesn't carry the fpgm/prep/cvt hinting tables, so the result is unhinted, the same as running hinting-stripper. On modern OSes with outline rendering this is invisible; only legacy Windows GDI + ClearType users at small sizes might see a difference. No extra de-hinting step is needed.
Is there a glyph-count limit I can hit?
The free tier has a 1,000-glyph limit, but the Latin preset keeps at most 192 glyphs (191 + .notdef), so you'll never hit it with this tool. The binding free-tier constraint is the 5 MB file-size limit on the input — large CJK or unfrozen variable fonts can exceed it before subsetting.
Why are my accents stacking wrong on some words?
If the text uses decomposed sequences (base letter + a combining accent, e.g. e + U+0301), the accent positioning relies on GPOS mark anchoring — which the subset drops. Precomposed letters (é as the single codepoint U+00E9) are kept and render correctly. Normalise your content to NFC, or use a layout-preserving subsetter for decomposed text.
Is my font sent anywhere when these errors occur?
No. All processing — success or error — happens in your browser via opentype.js/wawoff2. Even when the tool throws 'Subset would be empty' or a writer error, the font never left your machine. The only server interaction is an anonymous run counter for signed-in dashboard stats, which you can opt out of.
How do I tell a real failure from expected behaviour?
Real failures throw a visible error (empty subset, writer failure, tier limit) and produce no download. Expected behaviours produce a download with explanatory metrics: TTF output, 'Kerning + OT layout features dropped' note, a Glyphs kept count under 192, or a flattened variable font. If you got a file and the metrics look sane, the tool worked as designed.
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.