Shaping and fallback
Last updated 2026-11-02
- 整形
- shaping
- 回退
- fallback
- 豆腐块
- 复杂文字
On this page
There is a middle layer people skip: the text holds code points, the screen shows glyphs, and the mapping is not one-to-one. Turning a code point sequence into a glyph sequence is called shaping.
What shaping does
At minimum four things:
- Map: look up
cmap, turning code points into glyphs; - Substitute: apply
GSUBrules (ligatures, contextual forms, localised forms); - Position: apply
GPOSrules (kerning, mark attachment, cursive joining); - Reorder: bidirectional scripts (Arabic, Hebrew) must be laid out in the right order.
For Latin text the first three often do nothing, which is why "shaping" sounds mysterious. Scripts that cannot live without it are Arabic, the Indic family, and anything with heavy joining.
Why complex scripts need it
Arabic letters have initial, medial, final and isolated forms; which one applies depends on position within the word. That cannot be expressed as "one character, one glyph" — it needs context.
Which is why the same code points can set completely differently depending on language environment — the reason the locl feature and contextual substitution exist in GSUB / GPOS.
Fallback: when the font lacks the character
Before shaping, something must decide which font to use. The browser roughly does:
- the first family in
font-familythat is installed / loaded; - if that font lacks a character (missing from
cmap, or mapped to.notdef), that one character goes to the next family in the list that has it; - list exhausted: use the system default font;
- system has none either: draw tofu.
Crucially, fallback happens per character. One line may come from three fonts, with different line heights, baselines and weights — that is why mixed Chinese and Latin looks jumpy.
"Which font drew this"
The most common misunderstanding: CSS cannot say "this character must be drawn by that font". It only supplies a candidate list; the browser decides by the rules above.
Therefore:
- "it displays" does not mean "the selected font is what drew it";
- a font may map a code point but have an empty outline; the browser still falls back, so what you see is not from that font;
- the character drawer on this site distinguishes "in the font" from "what it looks like rendered with this font".
Tofu and empty outlines
- Tofu: the box drawn when no font has the character (
U+FFFDis the replacement character, which is something else again); - Empty outline: the font declares a mapping but the glyph contains nothing. Looks like a missing character, causes differ.
The test is cmap (which is how coverage is computed here). But coverage only says "there is a mapping", never "it can be drawn" — a known limitation.
Observing it here
- Local fonts: once bytes are registered with
FontFace, what you see is that font drawing; - Library fonts: before loading, the page font draws it (looks similar, is not the font); after loading, it is real;
- The drawer's side-by-side "default font / this font" pair exists to show exactly this.
Further reading
- OpenType spec: shaping and
GSUB/GPOS - HarfBuzz documentation: one widely used shaping engine
- This site: GSUB / GPOS, Character properties