Skip to content

Fix emoji rendering in SVG rasterization#66

Merged
llimllib merged 5 commits intomainfrom
fix-emoji-rendering
Mar 16, 2026
Merged

Fix emoji rendering in SVG rasterization#66
llimllib merged 5 commits intomainfrom
fix-emoji-rendering

Conversation

@llimllib
Copy link
Copy Markdown
Owner

@llimllib llimllib commented Mar 16, 2026

Problem

Mermaid diagrams (and any SVG) containing emoji characters rendered all text in the affected <tspan> as tofu (replacement boxes), not just the emoji — even plain ASCII characters like "Yes it does!" became unreadable.

Root Cause (two issues)

1. Font fallback picks .LastResort before emoji fonts

usvg's default font fallback selector iterates all fonts in fontdb in insertion order looking for one that contains a missing glyph. On macOS:

  • .LastResort (a system font that has glyph IDs for every Unicode codepoint but renders them all as tofu boxes) appears at position ~91 in the font database
  • Apple Color Emoji (which can actually render emoji via sbix tables) appears much later at position ~816

The fallback selector finds .LastResort first since it matches everything. Critically, when usvg reshapes the text with the fallback font and all glyphs match, it replaces the entire text span — including the ASCII characters that were rendering fine with the original font.

A single emoji in a text node causes every character in that node to become tofu.

Fix: Pre-load platform emoji fonts into fontdb before calling load_system_fonts(), so they appear earlier in the iteration order and get selected before .LastResort.

2. Missing raster-images feature on resvg

Even after fixing the fallback order, emoji were still invisible (blank space instead of tofu). Apple Color Emoji uses sbix tables containing embedded PNG bitmaps for each glyph. resvg gates PNG decoding of bitmap font glyphs behind its raster-images feature flag. mdriver had default-features = false and only enabled text and system-fonts, so emoji bitmaps were silently skipped.

Fix: Add raster-images to resvg's feature list.

screenshot

Screenshot 2026-03-16 at 8 49 14 AM

Investigation Notes

  • resvg has supported color font rendering (COLRv0, COLRv1, sbix, CBDT, SVG) since v0.42 via PR #735
  • The SVG text content from mermaid-rs is valid UTF-8 with emoji directly embedded — no encoding issues
  • Adding emoji fonts to the SVG font-family attribute does not help because the font-family list only affects primary font selection, not the fallback selector's iteration order
  • A custom FontResolver.select_fallback that skips .LastResort also works, but pre-loading is simpler and doesn't require reimplementing the fallback logic

Session transcript

transcripts/66-fix-emoji-rendering.html

Pre-load platform emoji fonts (Apple Color Emoji, Noto Color Emoji,
Segoe UI Emoji) into the fontdb before loading system fonts. This
ensures they appear before .LastResort in the fallback iteration order.

Without this fix, when resvg/usvg encounters an emoji character in SVG
text, the default font fallback selector iterates all loaded fonts and
finds .LastResort first. Since .LastResort has glyph IDs for every
codepoint, it matches and usvg reshapes the entire text span with it,
replacing all glyphs (including ASCII) with tofu boxes.

By loading emoji fonts first, they appear earlier in the iteration
order and get selected for emoji fallback instead, rendering correctly
via their sbix/COLR/CBDT tables.
Apple Color Emoji (macOS) uses sbix tables containing embedded PNG
bitmaps for each glyph. resvg gates PNG decoding of these bitmap font
glyphs behind its 'raster-images' feature. Without it, emoji glyphs
are silently skipped even when the correct font is selected.
Warn once on stderr if no platform emoji font could be loaded, so users
have a hint about why emoji render as tofu. The warning only fires in
image mode (--images kitty) since render_svg is only called from the
image pipeline. Uses an AtomicBool to avoid repeating the warning on
every SVG render.

Also expanded the code comments to explain the .LastResort problem
and document the known emoji font paths per platform.
Instead of hardcoding platform-specific file paths for emoji fonts,
load system fonts into a temporary fontdb, search for known emoji font
family names (Apple Color Emoji, Noto Color Emoji, Segoe UI Emoji),
extract the file path, then build the real database with that font
loaded first.

This is more robust across distros and non-standard font installations.
Warns once on stderr if no emoji font is found.
@llimllib llimllib merged commit 841c30b into main Mar 16, 2026
3 checks passed
@llimllib llimllib deleted the fix-emoji-rendering branch March 16, 2026 13:11
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.

1 participant