How to debug missing glyphs and tofu with the glyph inspector
- Step 1Get the missing codepoint — Identify the character users can't see and its Unicode value: `ő` is `U+0151`, `😀` is `U+1F600`, `→` is `U+2192`. Copy the character from the bug report so you have the exact codepoint, not a look-alike.
- Step 2Inspect the production font — Drop the exact font file your site serves — the same WOFF2/WOFF the browser downloads, not a desktop OTF that might differ. The inspector decodes it and emits the glyph records.
- Step 3Search the JSON for the codepoint — Copy or download the output and search for the `U+XXXX` value (e.g. `U+0151`). A matching `unicode` means the font has an encoded glyph for it; no match means it doesn't, or the glyph is unencoded.
- Step 4Classify the failure — Match found + the character still tofus → the bug is downstream (font not loaded, wrong subset, shaping, CSS). No match → the font genuinely lacks the codepoint. Shape present but `unicode: null` → it's an unencoded glyph reachable only via features.
- Step 5Fix at the right layer — Genuinely missing → add a fallback font in the `font-family` chain that covers the codepoint, or switch fonts. Wrong subset → re-subset to include the range. Downstream render bug → check `@font-face`, `unicode-range`, and that the file actually loaded.
- Step 6Re-verify after the fix — If you re-subset or swap fonts, re-run the inspector on the new file and confirm the codepoint now appears (and the others you rely on weren't dropped). Cross-check whole-script coverage with the coverage map.
Tofu triage: what the record tells you
Map the inspector's output for a problem codepoint to the actual root cause and the layer that owns the fix.
| What you see in the JSON | Likely cause | Where to fix |
|---|---|---|
No record has unicode = the codepoint | Font genuinely lacks the glyph for that character | Add a fallback in font-family, or switch to a font that covers it |
| A record matches but the character still tofus | Font has it; bug is downstream | Check the font loaded, the right subset shipped, and unicode-range includes it |
Shape exists but unicode: null | Unencoded glyph (ligature/alt) reached only via features | Nothing to fix in coverage; the glyph isn't meant to be typed directly |
Record matches but advance is 0 and bbox is empty | Glyph present but blank/zero-width | Likely an intentional blank or a broken glyph — inspect the source font |
sampled < total_glyphs and no match found | List was truncated at 5,000 — the glyph may be beyond the cap | Use the coverage map, or a Node walk, to check the full glyph set |
Shape mapped to a PUA codepoint (U+E0xx) | Icon/legacy encoding — wrong codepoint for a 'real' character | Re-encode the glyph or update the consuming code to the PUA value |
Common tofu codepoints to check
Characters that frequently tofu when a Latin-only subset ships. Search the inspector output for these U+ values when debugging multilingual content.
| Character | Codepoint | Why it tofus |
|---|---|---|
| ő (Hungarian o-double-acute) | U+0151 | In Latin Extended-A — often dropped by a Latin-Basic-only subset |
| č / š / ž (Czech/Slavic) | U+010D / U+0161 / U+017E | Latin Extended-A; missing from minimal Western subsets |
| → (rightwards arrow) | U+2192 | Arrows block; rarely in text fonts, common in UI copy |
| — (em dash) | U+2014 | General Punctuation; sometimes dropped, breaking typographic dashes |
| ’ (curly apostrophe) | U+2019 | General Punctuation; often shares a glyph with a straight quote, sometimes absent |
| 😀 (grinning face) | U+1F600 | Emoji block; almost never in a text font — needs an emoji fallback, not this font |
Cookbook
Concrete debugging passes. The 'search' step means searching the copied/downloaded JSON for a U+ value.
User reports a tofu where ő should be
ExampleHungarian user sees a box for U+0151. You inspect the shipped WOFF2 and search the JSON. No record has unicode U+0151, but U+00F3 (ó) is present — confirming a Latin-Extended-A gap.
Search output for U+0151 → no match.
Search for U+00F3 (plain ó) → found:
{ "index": 245, "name": "oacute",
"unicode": "U+00F3", "advance": 500, ... }
Diagnosis: font covers Latin-1 but not Latin Extended-A.
Fix: re-subset to include U+0150-0151, U+0170-0171
(or add a fallback font in the family chain).Glyph exists but the page still shows tofu
ExampleAn em dash (U+2014) tofus on the live site. The inspector finds it in the font — so the font isn't the problem. The real cause was a subset shipped without General Punctuation in its unicode-range.
Search output for U+2014 → found:
{ "index": 1182, "name": "emdash",
"unicode": "U+2014", "advance": 1000, ... }
Font HAS the glyph. So why tofu?
→ @font-face unicode-range excluded U+2014, so the
browser never requested the subset containing it.
Fix is in the CSS, not the font.Right shape, wrong codepoint (PUA encoding)
ExampleA checkmark renders only when you use U+E001, not the real U+2713. The inspector shows the glyph is mapped into the Private Use Area, not the standard codepoint.
Search for U+2713 (real check mark) → no match.
Scan PUA range → found:
{ "index": 88, "name": "check",
"unicode": "U+E001", "advance": 700, ... }
The glyph is PUA-encoded. Consumers must use \\e001,
not ✓. Either re-encode to U+2713 or document the PUA.Emoji can't come from a text font
ExampleA grinning face tofus. The inspector confirms the text font has no emoji glyphs at all — this is expected. Emoji need a dedicated emoji font in the fallback chain, not a fix to this font.
Search for U+1F600 → no match (and none in the
1F300-1FAFF range anywhere in the records).
Diagnosis: this is a Latin text font; it was never
going to have emoji. Fix in CSS:
font-family: "Brand", "Apple Color Emoji",
"Segoe UI Emoji", sans-serif;Large font, codepoint beyond the 5,000-glyph cap
ExampleA CJK font tofus a rare ideograph. The inspector's header shows the list was truncated, so 'no match' is inconclusive — you escalate to the coverage map.
Output header:
{ "total_glyphs": 23104, "sampled": 5000, ... }
Search for the rare ideograph → no match in the
first 5,000 records. But the font has 23,104 glyphs,
so this is INCONCLUSIVE.
→ Use character-coverage-map to test the full set.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.
'No match' on a font over 5,000 glyphs
InconclusiveThe inspector serialises at most 5,000 glyph records. On a large CJK or pan-Unicode font (total_glyphs well above 5,000), a codepoint that lives in a glyph beyond the cap simply won't appear in the output — which is not the same as the font lacking it. Always check sampled vs total_glyphs; if truncated, confirm coverage with the character-coverage-map instead of concluding the glyph is missing.
Glyph present but reported with only its primary codepoint
Primary onlyIf a glyph is mapped from multiple codepoints (a shared quote glyph, a unified hyphen), the record shows only the primary one. So a search for a secondary codepoint can miss a glyph that does, in fact, cover the character. When a single-codepoint search comes up empty but the visual shape clearly exists, the coverage map (which reads the full cmap) is the authoritative check.
Match found but the character still doesn't render
Downstream bugA matching unicode proves the font binary has the glyph. If the page still tofus, the fault is downstream: the font didn't load (404, CORS, wrong path), the wrong subset shipped, unicode-range excluded the codepoint, or shaping (GSUB/GPOS) failed. The inspector has done its job — move the investigation to the network tab and the @font-face CSS.
Desktop font differs from the served web font
Wrong artifactInspecting the OTF on your machine can mislead if production serves a subset WOFF2 generated by a build step. The subset may have dropped exactly the codepoint in question. Always inspect the actual file the browser downloads (grab it from the network tab) so you're debugging what users get, not what the designer shipped.
PUA-encoded glyph mistaken for missing
Mis-encodingIcon fonts and some legacy fonts park glyphs in the Private Use Area (U+E000–U+F8FF). A search for the 'real' codepoint (e.g. U+2713 for a check) finds nothing, but the shape is present under U+E0xx. That's an encoding choice, not a missing glyph — consumers must reference the PUA value, or the glyph must be re-encoded.
Unencoded ligature looks like a missing character
By designIf you expect 'fi' (the precomposed ligature) and search for its codepoint, you may find nothing even though the font draws a beautiful fi — because the ligature glyph is unencoded (unicode: null) and produced by the GSUB liga feature from the separate f and i glyphs. That's correct behaviour; the ligature isn't meant to be a single typed character. To see which features can substitute glyphs, use the opentype-features-inspector.
Zero-width or blank glyph that 'exists'
Blank glyphA record can match the codepoint yet have advance: 0 and an empty bbox/svgPath — a deliberately blank glyph (zero-width joiner, format control) or a broken one. The character is technically present but invisible, which users may still report as 'missing'. The bbox and advance tell you it's blank, not absent.
Production WOFF2 too large for the free tier
413-style blockIf the binary you're debugging is over the tier size limit (5 MB free / 50 MB pro / 1 GB developer), the inspector rejects it before parsing. Large unsubset CJK or icon fonts can exceed the free cap. Subset it first, or inspect on a higher tier — but make sure you're still inspecting the production artifact, not a different file.
Frequently asked questions
How do I check whether a font has a specific character?
Find the character's Unicode codepoint (e.g. ő = U+0151), drop the font into the inspector, and search the JSON output for that U+ value. A matching unicode field means the font has an encoded glyph for it. No match means the font lacks it — unless the font has more than 5,000 glyphs, in which case the list may be truncated and you should confirm with the coverage map.
The glyph is in the font but the page shows tofu — why?
Because the bug is downstream of the binary. Common causes: the font file didn't actually load (check the network tab for a 404 or CORS error); the wrong subset shipped; the @font-face unicode-range excluded the codepoint so the browser never fetched the relevant subset; or shaping failed. A confirmed match in the inspector means you should stop blaming the font and look at the CSS and network layers.
What's the difference between a missing glyph and an unencoded one?
A missing glyph means no glyph in the font draws that character — you'll find no matching unicode and no relevant shape. An unencoded glyph means the shape exists in the font but no codepoint maps to it (unicode: null); it's reached only through OpenType features like ligatures or small caps. Missing → add a fallback or switch fonts. Unencoded → nothing to fix; it isn't meant to be typed directly.
Why does the same character render differently across browsers?
When a glyph is genuinely missing, the browser walks its own fallback chain, and the chains differ. Safari may fall back to Apple Color Emoji while Chrome uses a system emoji or Noto font — visibly different even though both are 'fallback'. The fix is to make your font-family chain explicit so you control the fallback rather than leaving it to each browser's defaults.
Can the inspector tell me everything a font covers at once?
Not as a coverage report — it lists glyphs, not blocks, and stops at 5,000 records. For a whole-font coverage picture use the character-coverage-map, which scores the font against 346 real Unicode blocks and flags complete/partial/empty coverage. The inspector is the right tool for drilling into one specific glyph; the coverage map is right for the overview.
My search found nothing but I can see the character rendered — what gives?
Two likely reasons. First, the glyph may be mapped from multiple codepoints and the inspector reports only the primary one, so a secondary codepoint won't match. Second, on a font over 5,000 glyphs the record list is truncated. In both cases, the coverage map (which reads the full cmap) is the authoritative check.
How do I know if I'm debugging the right font file?
Grab the exact file from the browser's network tab — the WOFF2 your site actually serves — rather than the source OTF on your machine. Build pipelines often subset fonts, and the subset may have dropped exactly the codepoint that's tofuing. Inspecting the wrong artifact is the single most common reason a 'should exist' glyph appears missing.
What does a tofu box actually mean?
The browser asked the active font for a glyph at a given codepoint, the font's cmap had no entry, and the font drew its .notdef glyph (index 0) — the box. So tofu always means 'this font has no glyph for this codepoint at render time'. The inspector confirms whether that's because the font lacks it or because the wrong/incomplete font was loaded.
Can I debug a confidential or unreleased font safely?
Yes — parsing runs entirely in your browser via opentype.js, with a 0 bytes uploaded badge on the result. Unreleased brand fonts and licensed type never reach a server, which matters when you're debugging a font you're not allowed to redistribute.
A glyph matches but renders as nothing — is it broken?
Look at its advance and bbox. If advance is 0 and the bbox is empty with svgPath: "", it's a blank glyph — either intentional (a control or zero-width character) or a broken one. The character is technically present but invisible, which users sometimes report as missing. The metrics distinguish 'blank' from 'absent'.
How do I fix a genuinely missing glyph?
Two options. Add a fallback font to your font-family chain that covers the codepoint — the browser walks the chain automatically when the primary font lacks a glyph. Or re-subset/replace the primary font to include the range you need. If you re-subset, re-run the inspector afterwards to confirm the codepoint is present and that you didn't drop others.
Does subsetting risk removing characters I need?
Yes, which is exactly why you verify. After running the font-subsetter, drop the result into the inspector and confirm the codepoints your content uses still have records. Pair with the coverage map for the script-wide view. Catching a dropped codepoint here is far cheaper than catching it in a user bug report.
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.