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.
Background
packages/Emojigives us monochrome emoji: Noto Emoji registered as theemojifamily and pulled in throughgetBackupSubsFonts(), souseSubstitutionswaps 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 embeddedNotoColorEmoji.ttfrenders flat or blank. Two further blocks sit on top:TTFontFile::makeSubset()emitscmap,glyf,hmtx,loca,head,hhea,maxp,OS/2,name,post, andrepackageTTF()(src/TTFontFile.php:4860) keeps a similar list. Colour tables are dropped whether or not subsetting is on.Writer\FontWriterwrites 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
/CharProcscontent stream, free to set colours, fill paths, orDoan image XObject. Text extraction survives via/ToUnicode, advances still come fromhmtx, and the glyph remains a glyph — so shaping, line breaking, justification andOtlkeep working on it rather than being handed an inline image to route around.Sketch of the work:
TTFontFile— parseCBDT/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.COLRv0 +CPALis a plausible second source: flat layer lists over existingglyfoutlines, one fill colour per layer, no paint graph.Writer\FontWriter— awriteType3()alongside the existing three:/Subtype /Type3,/FontMatrixscaled fromunitsPerEm,/FontBBox,/CharProcs,/Encodingwith/Differences,/Widths,/ToUnicode, and/Resources. Each CharProc is a handful of operators:d0with the advance,cmto place,Dothe image XObject.ImageWriterso PNG alpha already becomes an/SMask. Resources are simple: there is a single shared resource dictionary (object 2,ResourceWriter::writeResources()), so the font's/Resourcescan point at it or carry its own narrow/XObjectdict.SetFont(),available_unifontsand thebackupSubsFontlookup like any other family, withcwpopulated 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/subsetfontidssplit 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/CBLCbitmap emoji, embedded as Type3 with image XObjects.cmapplus its GSUB ligatures already map these to single glyphs, so this is mostly a matter of lettingOtldo its job before the substitution scan decides a character is undefined.Out, at least for now:
COLRv1 — gradients, composites and transforms would mean translating a paint graph into PDF shadings.sbixand OT-SVG.Open questions
packages/*package or stays caller-supplied. Noto Color Emoji is ~10MB.FontRegistry, which is ours.Alternative considered
Substituting an
<img>per emoji — splice image tokens in a sibling ofSubstituteCharsMB()(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 …>> BDCanywhere. 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.