Skip to content

Latest commit

 

History

History
159 lines (117 loc) · 7.38 KB

File metadata and controls

159 lines (117 loc) · 7.38 KB

morx Table — AppleColorEmoji Modifications

This document explains how the morx (Extended Glyph Metamorphosis) table works in AppleColorEmoji, and details the modifications made by inject_neutral_couple_silhouette.py.


Background: what is morx?

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.

Chains and subtables

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.

Subtable types used in AppleColorEmoji

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)

SubFeatureFlags in AppleColorEmoji

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 original morx chain structure

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 0x010x04 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+u1F9D1u1F9D1_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 0x080x10 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

How FM/FF/MM holding-hands silhouette works (Apple's approach)

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.


Our modification: NN couple silhouette injection

The problem

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.

The solution

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:

  1. Subtable [7] (LigatureMorph, 0x01) runs in ALL rendering contexts and ligates the full sequence into u1F9D1_u1F91D_u1F9D1.XY — no intermediate ZWJ, skin, or handshake glyphs remain in the stream.
  2. Subtable [23] (NoncontextualMorph, 0x20) then performs a single one-to-one substitution: composite → per-XY left-silhouette glyph.
  3. 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.

New glyphs (52 total)

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.

NoncontextualMorph patches

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.

Image generation

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 from silhouette.ML) plus a clasped-hand correction from the right half of silhouette.ML.
  • .R.XY: full couple composited with a right-half gray overlay (sampled from silhouette.ML) plus a clasped-hand correction from the left half of silhouette.MR.

Relevant scripts

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