This document explains how the morx (Extended Glyph Metamorphosis) table works in
AppleColorEmoji, and details the modifications made by inject_neutral_couple_silhouette.py.
morx is an Apple-specific OpenType/AAT table that drives glyph substitution and
rearrangement during text layout. It is the primary mechanism Apple uses to map
emoji Unicode sequences (ZWJ sequences, skin-tone modifier sequences, etc.) to the
composite glyph IDs stored in the font.
A morx table contains one or more MorphChains. AppleColorEmoji has a single
chain. Each chain holds an ordered list of MorphSubtables. Subtables are applied
left-to-right; each one can transform the current glyph run before passing it to the
next.
Every subtable carries a SubFeatureFlags bitmask. The rendering engine enables a
set of feature flags based on context (e.g. normal rendering, silhouette rendering for
Messages app contact photos). A subtable only fires when its flags are a subset of the
currently active flags.
| MorphType | Name | What it does |
|---|---|---|
| 1 | ContextualMorph | State-machine-driven per-glyph substitution with mark/current register |
| 2 | LigatureMorph | State-machine-driven multi-glyph → single-glyph ligature formation |
| 4 | NoncontextualMorph | Simple one-to-one glyph substitution table (lookup) |
The silhouette feature is split across three flags:
| Flag | Meaning |
|---|---|
0x00000001 |
Always active (normal rendering) |
0x00000020 |
Left-person silhouette |
0x00000040 |
Right-person silhouette |
Subtables with flags 0x01 fire in all rendering contexts. Subtables with flags
0x20 or 0x40 fire only when the engine is rendering a couple emoji for the
Messages silhouette feature (one person gray, the other in colour).
The single MorphChain in AppleColorEmoji (iOS) originally contains 26 subtables (indices 0–25 after our patch is applied; 0–25 before as well, since our insertion preserves count by shifting). The subtables most relevant to couple handling:
| Original index | Flags | Type | Role |
|---|---|---|---|
| 0 | 0x01 |
NoncontextualMorph | Initial identity / normalisation pass |
| 1–6 | 0x01–0x04 |
LigatureMorph | Various ZWJ ligature formations |
| 7 | 0x01 |
LigatureMorph | NN skin-tone couple — forms u1F9D1+skin+ZWJ+u1F91D+ZWJ+u1F9D1+skin → composite u1F9D1_u1F91D_u1F9D1.XY |
| 9 | 0x01 |
LigatureMorph | NN neutral couple — forms u1F9D1+ZWJ+u1F91D+ZWJ+u1F9D1 → u1F9D1_u1F91D_u1F9D1.66 |
| 14 | 0x01 |
ContextualMorph | Person contextual tagging (pre-pass) |
| 15 | 0x01 |
ContextualMorph | Tags u1F468/9/9D1.X → .L/.R for heart, kiss, and wrestling couples |
| 16 | 0x01 |
LigatureMorph | Forms heart/kiss composite from .L+ZWJ+♥+ZWJ+.R |
| 17–22 | 0x08–0x10 |
Various | Other feature variants |
| 23 | 0x20 |
NoncontextualMorph | Left silhouette — maps left-person half-glyph → silhouette.ML |
| 24 | 0x40 |
NoncontextualMorph | Right silhouette — maps right-person half-glyph → silhouette.MR |
| 25 | 0x01 |
NoncontextualMorph | Final normalisation pass |
For female+male, female+female, and male+male holding-hands couples, subtable [15]
(ContextualMorph, 0x01) tags the left person glyph with .L and the right with .R
during its state-machine traversal. Subtable [23] then maps every .L variant to
silhouette.ML and subtable [24] maps every .R variant to silhouette.MR.
Apple does not provide silhouette support for neutral-person (U+1F9D1) holding-
hands couples — it only added those couples in later OS versions and never patched the
silhouette pipeline for them.
The NN holding-hands ZWJ sequence (U+1F9D1 [skin] U+200D U+1F91D U+200D U+1F9D1 [skin])
is resolved to a composite glyph by subtable [7] (LigatureMorph, always active).
By the time the silhouette subtables [23]/[24] process the glyph stream, the entire
sequence has already been collapsed into a single composite such as
u1F9D1_u1F91D_u1F9D1.11. Apple's subtables [23] and [24] have no entries for any
of these composites — they left them as identity pass-throughs.
We patch the two existing NoncontextualMorph silhouette subtables to map each of the 26 composite glyphs to a new per-combination silhouette glyph. This works because:
- Subtable [7] (LigatureMorph,
0x01) runs in ALL rendering contexts and ligates the full sequence intou1F9D1_u1F91D_u1F9D1.XY— no intermediate ZWJ, skin, or handshake glyphs remain in the stream. - Subtable [23] (NoncontextualMorph,
0x20) then performs a single one-to-one substitution: composite → per-XY left-silhouette glyph. - Subtable [24] (NoncontextualMorph,
0x40) does the same for the right silhouette.
The silhouette images are baked composites — both persons are in one image, with the appropriate half already grayed out. This matches exactly how Apple handles all other couple silhouettes.
For each of the 26 skin-tone combinations (XY where X, Y ∈ {1..5} plus 66 for
neutral+neutral):
| Glyph name | Image content |
|---|---|
silhouette.u1F9D1.u1F91D.L.XY |
Left person gray, right person in combo XY colours |
silhouette.u1F9D1.u1F91D.R.XY |
Right person gray, left person in combo XY colours |
Glyph outlines are cloned from the donor composite u1F9D1_u1F91D_u1F9D1.66 (empty
placeholder, 800-unit advance). Actual rendering comes from the sbix PNG strikes.
Left-silhouette subtable (0x20):
u1F9D1_u1F91D_u1F9D1.XY → silhouette.u1F9D1.u1F91D.L.XY (for all 26 XY)
Right-silhouette subtable (0x40):
u1F9D1_u1F91D_u1F9D1.XY → silhouette.u1F9D1.u1F91D.R.XY (for all 26 XY)
Identity pass-through subtables (all others with silhouette.ML → silhouette.ML):
All 52 new glyphs are added as self-mappings so they survive unmodified through the
chain when silhouette features are not active.
make_neutral_couple_silhouette.py generates the 52 per-ppem PNG files from existing
source images. For each couple image u1F9D1_u1F91D_u1F9D1.XY.png it produces:
.L.XY: full couple composited with a left-half gray overlay (sampled fromsilhouette.ML) plus a clasped-hand correction from the right half ofsilhouette.ML..R.XY: full couple composited with a right-half gray overlay (sampled fromsilhouette.ML) plus a clasped-hand correction from the left half ofsilhouette.MR.
| Script / CLI | What it touches |
|---|---|
make_neutral_couple_silhouette.py |
Generates 52 per-ppem silhouette PNG files from couple + ML/MR reference images |
inject_neutral_couple_silhouette.py (emojifonts-inject-silhouette) |
Adds 52 glyphs, injects PNGs into sbix, patches NoncontextualMorph subtables [23]/[24] |
apple-prepare.sh |
Calls make_neutral_couple_silhouette.py then emojifonts-inject-silhouette during font preparation |
tests/test_cli_tools.py |
Regression tests: verifies glyph presence, sbix PNG injection, and morx mapping correctness |