Skip to content

Comments

Add color emoji support via SBIX bitmap rendering#1690

Open
jspears wants to merge 3 commits intofoliojs:masterfrom
jspears:support-color-emoji
Open

Add color emoji support via SBIX bitmap rendering#1690
jspears wants to merge 3 commits intofoliojs:masterfrom
jspears:support-color-emoji

Conversation

@jspears
Copy link

@jspears jspears commented Feb 22, 2026

Summary

Adds color emoji rendering to PDFKit. When an emoji font is registered, text containing emoji is automatically segmented into plain text and emoji runs. Emoji glyphs are rendered as inline bitmap images (PNG XObjects) extracted from the font's sbix table, while surrounding text continues to render normally through the existing font pipeline.

There is a follow up PR stacked on this one that adds support for COLR/CPAL and CBDT/CBLC

Motivation

PDFKit currently discards color emoji data during font subsetting. The PDF either shows monochrome outlines or blank spaces where emoji should appear. This is a common pain point for anyone generating PDFs with user-supplied text that may contain emoji.

How It Works

Text Segmentation

A new emoji segmenter (lib/emoji/segmenter.js) splits input text into text vs emoji segments using Unicode range heuristics. It correctly handles ZWJ sequences, regional indicator pairs (flag emoji), skin tone modifiers, variation selectors, and keycap sequences.

Emoji Font Registration

A new registerEmojiFont method (lib/mixins/fonts.js, lib/font_factory.js) loads an emoji font via fontkit. TrueType Collections are handled by iterating the font collection and matching by postscriptName or familyName, avoiding the getVariation call that crashes on emoji fonts lacking fvar/gvar tables.

Can also be set via document constructor options:

const doc = new PDFDocument({
  emojiFont: '/path/to/Apple Color Emoji.ttc',
  emojiFontFamily: 'AppleColorEmoji',
});

Rendering

  • widthOfString is now emoji-aware, summing text widths and emoji widths across segments
  • _line detects emoji segments and routes them to _emojiFragment instead of _fragment
  • _emojiFragment uses fontkit layout/shaping, extracts SBIX bitmaps via glyph.getImageForSize, and places them as image XObjects
  • Bitmap images are cached by glyphId:ppem to avoid redundant embedding

Line Wrapping

No changes needed. LineWrapper calls widthOfString which is now emoji-aware, so word wrapping works correctly with mixed text/emoji content.

Usage

import PDFDocument from 'pdfkit';

const doc = new PDFDocument({
  emojiFont: '/System/Library/Fonts/Apple Color Emoji.ttc',
  emojiFontFamily: 'AppleColorEmoji',
});

doc.font('Helvetica')
   .fontSize(18)
   .text('Hello World PDFKit');

Testing

  • All 287 existing unit tests pass with no regressions
  • 10 new visual snapshot tests covering: simple emoji, multiple emoji, he- 10 new visual snapshot tests covering:la- 10 ni, skin tone modifiers, different font sizes, emoji-only lines, plain text (no regression), and keycap emoji

Limitations

  • Currently supports sbix (bitmap) fonts only. Apple Color Emoji
  • Follow up PR supports COLR/CPAL and CBDT/CBLC via NotoColorEmoji.ttf/Twemoji.Mozilla.ttf respectively

@jspears jspears changed the title feat: add color emoji support via SBIX bitmap rendering Add color emoji support via SBIX bitmap rendering Feb 22, 2026
@jspears
Copy link
Author

jspears commented Feb 22, 2026

Copy link
Member

@blikblum blikblum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some nit

Please add only one visual test

We should prefer unit tests going forward

Comment on lines +43 to +44
// Do NOT pass family to fontkit.create — for TTCs it calls getVariation()
// which fails on emoji fonts that lack fvar/gvar tables.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this bug reported in fontkit?

In the long run is better to fix it down stream so open font code path can be shared

return null;
}

if (!imgData || !imgData.data || imgData.data.length === 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!imgData || !imgData.data || imgData.data.length === 0) {
if (!imgData?.data?.length) {

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants