Fix emoji rendering in SVG rasterization#66
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
.LastResortbefore emoji fontsusvg's default font fallback selector iterates all fonts in
fontdbin 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 databaseApple Color Emoji(which can actually render emoji viasbixtables) appears much later at position ~816The fallback selector finds
.LastResortfirst 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
fontdbbefore callingload_system_fonts(), so they appear earlier in the iteration order and get selected before.LastResort.2. Missing
raster-imagesfeature on resvgEven after fixing the fallback order, emoji were still invisible (blank space instead of tofu). Apple Color Emoji uses
sbixtables containing embedded PNG bitmaps for each glyph. resvg gates PNG decoding of bitmap font glyphs behind itsraster-imagesfeature flag. mdriver haddefault-features = falseand only enabledtextandsystem-fonts, so emoji bitmaps were silently skipped.Fix: Add
raster-imagesto resvg's feature list.screenshot
Investigation Notes
font-familyattribute does not help because the font-family list only affects primary font selection, not the fallback selector's iteration orderFontResolver.select_fallbackthat skips.LastResortalso works, but pre-loading is simpler and doesn't require reimplementing the fallback logicSession transcript
transcripts/66-fix-emoji-rendering.html