Skip to content

Draw colour emoji by building a Type3 font from the colour font's bitmap tables #76

Description

@jakejackson1

Background

packages/Emoji gives us monochrome emoji: Noto Emoji registered as the emoji family and pulled in through getBackupSubsFonts(), so useSubstitution swaps in an outline glyph wherever the document font has none. Colour is not reachable by that route, and not by any route that embeds a font as we embed fonts now.

Colour emoji fonts keep their colour in tables PDF has no notion of: CBDT/CBLC (Noto Color Emoji — PNG bitmap strikes), sbix (Apple Color Emoji), COLR/CPAL (Segoe UI Emoji, Noto COLRv1), OT-SVG. A glyph drawn from a CIDFontType2 is a path filled with the current fill colour, so even a faithfully embedded NotoColorEmoji.ttf renders flat or blank. Two further blocks sit on top:

  • Every font we embed is rebuilt from a fixed table whitelist — TTFontFile::makeSubset() emits cmap, glyf, hmtx, loca, head, hhea, maxp, OS/2, name, post, and repackageTTF() (src/TTFontFile.php:4860) keeps a similar list. Colour tables are dropped whether or not subsetting is on.
  • Writer\FontWriter writes Type1, TrueType and Type0/CIDFontType2 only (src/Writer/FontWriter.php:125-144). There is no font type here whose glyphs are content streams.

Proposal

Add Type3 font support and build a Type3 font from a colour font's bitmap or layer tables. Type3 is the one place in PDF where colour lives inside a font: each glyph is a /CharProcs content stream, free to set colours, fill paths, or Do an image XObject. Text extraction survives via /ToUnicode, advances still come from hmtx, and the glyph remains a glyph — so shaping, line breaking, justification and Otl keep working on it rather than being handed an inline image to route around.

Sketch of the work:

  • TTFontFile — parse CBDT/CBLC (start with bitmap format 17/18 PNG data, which is what Noto ships) and expose, per glyph, the PNG bytes plus the strike's placement metrics. COLR v0 + CPAL is a plausible second source: flat layer lists over existing glyf outlines, one fill colour per layer, no paint graph.
  • Writer\FontWriter — a writeType3() alongside the existing three: /Subtype /Type3, /FontMatrix scaled from unitsPerEm, /FontBBox, /CharProcs, /Encoding with /Differences, /Widths, /ToUnicode, and /Resources. Each CharProc is a handful of operators: d0 with the advance, cm to place, Do the image XObject.
  • Image XObjects — one per used glyph, ideally through ImageWriter so PNG alpha already becomes an /SMask. Resources are simple: there is a single shared resource dictionary (object 2, ResourceWriter::writeResources()), so the font's /Resources can point at it or carry its own narrow /XObject dict.
  • Font registration — a colour emoji font needs to flow through SetFont(), available_unifonts and the backupSubsFont lookup like any other family, with cw populated so _charDefined() answers correctly for the substitution scan.

One constraint worth stating up front, because it turns out to be free: Type3 is a simple font, single-byte encoding, 255 glyphs to a font object. We already do exactly that chunking for SMP/SIP fonts — subsets/subsetfontids split a family across several PDF font objects and the writer switches /F<id> mid-string (src/Mpdf.php:5706-5729, 10968-10979). A Type3 emoji font can reuse that machinery instead of inventing its own.

Scope

In:

  • CBDT/CBLC bitmap emoji, embedded as Type3 with image XObjects.
  • Emoji sequence handling good enough to be honest: ZWJ sequences, U+FE0F, skin-tone modifiers, regional indicator pairs, keycaps. The font's cmap plus its GSUB ligatures already map these to single glyphs, so this is mostly a matter of letting Otl do its job before the substitution scan decides a character is undefined.
  • Monochrome Noto Emoji stays the fallback when no colour font is registered.

Out, at least for now:

  • COLR v1 — gradients, composites and transforms would mean translating a paint graph into PDF shadings.
  • sbix and OT-SVG.

Open questions

  • Resolution and size. Noto's CBDT strike is 136px; a document with many distinct emoji embeds a PNG each and grows accordingly. Do we cap, downsample, or leave it to the caller?
  • PDF/A and PDF/X. Type3 is restricted under PDF/A-1 and we would need to decide whether to refuse it and fall back to monochrome, which is probably the right answer.
  • Viewer support. Type3 with image XObjects is old and widely handled, but worth confirming against Acrobat, Preview, pdf.js, Poppler and Ghostscript — the last two matter for our own snapshot suite.
  • Whether the colour font ships as another packages/* package or stays caller-supplied. Noto Color Emoji is ~10MB.
  • Upstreamability: the Type3 writer and CBDT parsing belong upstream on their merits; the registration side leans on FontRegistry, which is ours.

Alternative considered

Substituting an <img> per emoji — splice image tokens in a sibling of SubstituteCharsMB() (src/Mpdf.php:25836) and let the existing inline-image path place them. Cheaper to build and PDF/A-safe, but it hands us ~3.7k assets to ship, makes us own sequence segmentation by hand, and silently drops the emoji from text extraction and copy-paste, since we emit no /Span <</ActualText …>> BDC anywhere. Worth keeping as a second, separately configurable mode rather than as the answer.

Testing

A snapshot fixture per source table, plus unit coverage of the CBDT parse against a small purpose-built font rather than a 10MB download. Note that snapshot comparison rasterises through Ghostscript, so a Type3 fixture also tells us whether Ghostscript agrees with us.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions