Skip to content

Choose the font an emoji is drawn in, and lay out a font without GDEF (#76) - #290

Merged
jakejackson1 merged 7 commits into
gravitypdffrom
feature/76-emoji-prerequisites
Sep 21, 2026
Merged

jakejackson1 merged 7 commits into
gravitypdffrom
feature/76-emoji-prerequisites

Conversation

@jakejackson1

@jakejackson1 jakejackson1 commented Sep 21, 2026 •

Copy link
Copy Markdown
Member

Phase 0 of the colour emoji plan for #76. It builds on #294 (Ucdn and Bidi moved into Mpdf\Unicode) and is the base of a stack: Phase 1 (#291, Type3 + CBDT), Phase 2 (#292, sbix) and Phase 3 (#293, COLRv0) build on it.

This PR chooses the font an emoji is drawn in and lets the shaper form it. Drawing the glyphs in colour comes in #291–#293.

Commits

Each commit passes composer cs and the test suite on its own.

  1. Read a font's metrics back as they were just rewritten. FontCache::jsonWrite() now forgets its in-memory copy. Before this, AddFont() read back stale metrics whenever it regenerated a font in the same process.

  2. Share the Unicode table generators' file reading and line wrapping. These move into the GeneratedTable trait, which the emoji generator uses too.

  3. Find each emoji in a run of text by the grammar of UTS Keep a greyscale JPEG greyscale when its Exif orientation is corrected #51. Mpdf\Unicode\Emoji finds ZWJ sequences, flags, keycaps, skin tones and tag sequences, and the presentation each asks for. Its tables come from Unicode 18's emoji-data.txt through the new composer emoji:update.

  4. Lay out a font without GDEF as HarfBuzz does. Most emoji fonts have GSUB and no GDEF, and TTFontFile refused them under useOTL. Such a font is now read with every glyph in class 0, and a mark filtering set in a font with no marks filters nothing. ParserGoldenMasterTest joins the shared GoldenMasterTestCase, since no font is refused any more. The fixtures for angerthas and Blank-WideCmap-Synthetic are regenerated.

  5. Record the colour formats a font carries and the tag characters it maps. The font cache gains:

    • colorFormats: any of COLRv1, COLRv0, SVG, CBDT and sbix.
    • tagChars: the Private Use code each plane-14 tag's glyph was read at.

    CACHE_FORMAT goes to 5. tests/data/ttf/color/ holds one tiny fixture font per format, all built from one set of designs and GSUB by the build.py beside them, which uses fontTools and Pillow. That also exercises all five CBLC index formats.

  6. Hand an emoji to the shaper as HarfBuzz would. Presentation selectors are removed before shaping, as HarfBuzz hides them, so Noto's keycap 1 FE0F 20E3 forms its ligature. Tag characters are given their Private Use code. Noto Emoji now forms its ZWJ, flag, keycap and subdivision-flag ligatures.

  7. Keep an emoji whole on its way to a backup font, and prefer a colour font where it asks for colour. SubstituteCharsMB() moves a whole emoji sequence to a backup font. Before, its joiners were left behind in the document font. The rules for choosing the font live in the new Mpdf\Fonts\FontSubstitution service.

Which font an emoji goes to

  • The current font lacks any part of the emoji: the emoji goes to the backup fonts, like any other missing character.
  • The current font has all of it: the emoji stays, unless it asks for a presentation the font can't give and a backup font can.
    • Asking for colour means Emoji_Presentation, U+FE0F, or any multi-codepoint sequence. A black and white current font then gives way to a colour backup font.
    • U+FE0E asks for text, so a colour current font gives way to a non-colour backup.
    • A colour current font keeps the colour emoji it has, even with other colour fonts among the backups.
  • Order of the backup fonts: a run that starts with a colour-asking emoji tries the colour backups first. A run asking for text tries the others first. Otherwise, and within each group, the configured order holds. A run is cut where the presentation it asks for changes.
  • With no colour font among the backups, nothing changes from before.

Checking whether a backup font is in colour loads its metrics the first time a run asks for a presentation: about 2 ms for five fonts with a warm font cache. A backup font that is only checked and never used isn't embedded.

Deviation from the plan

The PUA-budget item moves to Phase 1. It only matters for colour fonts, and in Phase 1 those fonts skip TrueType subsetting. The budget can be enforced there without keeping the subsetter's three copies of the PUA loop in step.

Known limits

Tests

  • New test classes:
    • EmojiTest: the UTS Keep a greyscale JPEG greyscale when its Exif orientation is corrected #51 cases.
    • EmojiTablesTest: the generator.
    • ColorFormatsTest: format detection and tag aliases, including SVG and a COLRv1 table with no v0 records, from patched copies of the fixtures.
    • OtlWithoutGdefTest: Noto Emoji's ligatures.
    • EmojiSubstitutionTest: whole sequences, presentation order, a colour document font, and runs that meet SIP text or an emoji no font has.
    • FontSubstitutionTest: backup font order, coverage and cached widths.
  • Additions to existing tests: FontCacheTest (the stale read), LookupFlagTest, OtlDumpTest and SharedFirstLookupFeatureTest (Aboriginal Sans, which has no GDEF).

I also planted 38 small bugs across the new code. Every one that changes behaviour makes a test fail.

composer test passes (2742 tests, including snapshots), and so does composer cs. PHPStan has no findings beyond the base's.

#291–#293 haven't been rebased onto this yet. Until they are, GitHub shows #290's commits in #291's diff, and #293's Otl will need use Mpdf\Unicode\Emoji;.

🤖 Generated with Claude Code

jakejackson1 and others added 6 commits September 21, 2026 16:24
FontCache::jsonWrite() now forgets its in-memory copy of the entry. Before, AddFont() read back the
stale metrics it had just replaced whenever it regenerated a font in the same process.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The emoji tables are generated the same way as Ucdn's, so reading a file of the database and
wrapping a table's entries move into the GeneratedTable trait the generators share.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mpdf\Unicode\Emoji finds ZWJ sequences, flags, keycaps, skin tones and tag sequences, and what
presentation each asks for. Its property tables are generated from Unicode 18's emoji-data.txt by the
new composer emoji:update.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Most emoji fonts carry GSUB and no GDEF, and TTFontFile refused them under useOTL. Such a font is now
read with every glyph in class 0, and a mark filtering set in a font with no marks filters nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ps (#76)

The font cache now holds colorFormats (COLRv1, COLRv0, SVG, CBDT, sbix) and tagChars, the Private Use
code each plane-14 tag's glyph was read at, and CACHE_FORMAT goes to 5. tests/data/ttf/color holds a
tiny fixture font per format, built by the build.py beside them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Presentation selectors are removed before shaping, as HarfBuzz hides them, so a font's ligature can
form without one in the way. Tag characters are given the Private Use code their glyph was read at.
Noto Emoji now forms its ZWJ, flag, keycap and subdivision-flag ligatures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jakejackson1
jakejackson1 force-pushed the feature/76-emoji-prerequisites branch 2 times, most recently from 06e530d to f2b6b99 Compare September 21, 2026 06:48
@jakejackson1 jakejackson1 changed the title Keep an emoji whole on its way to an emoji font, and lay out a font without GDEF (#76) Choose the font an emoji is drawn in, and lay out a font without GDEF (#76) Sep 21, 2026
…font where it asks for colour (#76)

SubstituteCharsMB() moves a ZWJ sequence, flag, keycap, skin tone or tag sequence to a backup font
whole, rather than leaving its joiners in the document font. An emoji that asks for colour is
offered to a colour backup font first, and U+FE0E to the others first. The rules for choosing the
font live in the new Mpdf\Fonts\FontSubstitution service.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jakejackson1
jakejackson1 force-pushed the feature/76-emoji-prerequisites branch from f2b6b99 to 7b49a07 Compare September 21, 2026 06:53
@jakejackson1
jakejackson1 added this pull request to stack #296 September 21, 2026 06:57
@jakejackson1
jakejackson1 merged commit 7b6b676 into gravitypdf Sep 21, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

create-upstream-pr enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant