Registered icon-font TTFs render blank glyphs until table checksums/table directory are recomputed — silent failure in the DWrite font path #16308
Description
Activity
- addedNeeds: Triage 🔍New issue that needs to be reviewed by the issue management team (label applied by bot)New issue that needs to be reviewed by the issue management team (label applied by bot)
on Jul 11, 2026 FaithfulAudio commented
on Jul 26, 2026 ContributorAuthorMore actionsReporter here, with a correction and a fix.
My diagnosis in this issue is wrong. Checksums are not the problem, and nothing in RNW or DirectWrite
rejects these files. I built a DirectWrite probe and measured it. Here is what is actually happening.Measured: DirectWrite accepts every stock TTF as-is
Standalone probe, Windows 11 10.0.26200, VS 2022 x64. For each byte-unmodified font from
react-native-vector-icons@10.3.0it reportsIDWriteFontFile::Analyze, the HRESULT from
IDWriteFontSetBuilder1::AddFontFile, the real family name from the name table, and then replays RNW's
call sequence (CreateTextFormatwith the same emptylocaleNamethe real code passes →
CreateTextLayout→Draw) with a recordingIDWriteTextRendererthat reports the resolved face and
glyph index. The drawn codepoint is taken from each face's ownIDWriteFontFace1::GetUnicodeRanges, so
it is guaranteed to be one that font claims to map.file (unmodified) AnalyzesupportedAddFontFilereal family name codepoint collection containing it nullptr(today)MaterialIcons.ttf 1 hr=0acceptedMaterial IconsU+E000 glyph 40 Segoe UI, glyph 0 MaterialCommunityIcons.ttf 1 hr=0acceptedMaterial Design IconsU+F0001 glyph 1 Segoe UI, glyph 0 Ionicons.ttf 1 hr=0acceptedIoniconsU+EA01 glyph 1 Segoe UI, glyph 0 FontAwesome.ttf 1 hr=0acceptedFontAwesomeU+F000 glyph 13 Segoe UI, glyph 0 Feather.ttf 1 hr=0acceptedFeatherU+F100 glyph 4 Segoe UI, glyph 0 Octicons.ttf 1 hr=0acceptedOcticonsU+F10B glyph 4 Segoe UI, glyph 0 EvilIcons.ttf 1 hr=0acceptedEvilIconsU+F100 glyph 4 Segoe UI, glyph 0 AntDesign.ttf 1 hr=0acceptedanticonU+E600 glyph 2 Segoe UI, glyph 0 Every one of these is a font I listed in this issue as "affected". Stale checksums and all, DirectWrite
loads them,AddFontFileaccepts them withhr=0, and they draw real glyph indices. There is no
validation rejection to loosen — in RNW or below it.There is also no RNW-side validation layer to look for. At
c69cf55f67f9b03f467502dac1007ac2d9ebe209,CreateFontFileReference,IDWriteFontCollection,
IDWriteFontSetBuilder,IDWriteFontFace(outside a comment),AddFontResourceandsharedFontshave
zero hits in the repo, and the git tree (3956 blobs) contains no source file with "Font" in its
name. RNW never opens a font file, never builds a collection, and never inspects a face.The actual root cause, and why "blank" fooled me
WindowsTextLayoutManager::GetTextLayoutpassesnullptras the font collection
(WindowsTextLayoutManager.cppL119):nullptr, // Font collection (nullptr sets it to use the system font collection).
System-collection-only, so no app-bundled font family resolves at all, and every icon codepoint
falls back to Segoe UI glyph 0 (.notdef) — the right-hand column above.I made a point in this issue of "blank, not tofu boxes — blank" and treated that as evidence of a second,
distinct bug. It isn't. Both symptoms are the sameSegoe UIglyph 0. Whether.notdefpaints a hollow
box or nothing at all is a property of the fallback face, not a signal about where the failure is. That
false distinction is the reason I filed this separately from #16306, and it cost me time. There is one
bug, not two.My control experiment was confounded the same way as #16306's: the re-emit pass recomputed checksums,
rebuilt the table directory (which also reorders, re-pads and re-aligns tables), and produced a new
file that had to be re-registered. I attributed the fix to checksums. Nothing supports that.Fix
Fix in #16339: add
DWriteAppFontCollection()toDWriteHelpers— system font set merged with every
*.ttf/*.otfunder the app'sAssets\andAssets\Fonts\, built once, failing closed tonullptr
so behaviour is unchanged for apps that bundle no fonts — and pass it at thatCreateTextFormatcall
instead ofnullptr. The same PR (#16339) fixes #16306.What I could not verify, stated plainly
- The probe machine has none of these fonts installed, so its
nullptrrows fail because the font is
genuinely absent. I did not reproduce thewindows.sharedFonts-registered state from my original
report. What is established is that DirectWrite accepts these exact bytes and renders real glyphs
from them, so "RNW/DWrite validates stale checksums strictly" is false regardless. - The PR is not compiled and not run. Lint, clang-format, typecheck and build gates are unrun. The
probe is separate standalone code. - I have not established what did change when the re-emitted files were registered. If anyone wants to
chase it, I still have both binaries; but with the collection fix in place I do not think it matters.
Two incidental traps worth recording
AntDesign.ttf's real family name isanticon, notAntDesign. RequestingAntDesigngives
.notdefeven with the font in the collection. Anyone debugging icon fonts on Windows should read the
name table rather than trusting the filename or the JS-side family constant.- DirectWrite does not normalise leading, trailing or duplicated whitespace in family names, and RNW does
not trim the string.fontFamily: 'Ionicons 'silently renders.notdef.
The one thing I originally asked for that still stands
"Or, at minimum, surface a diagnostic when the font path refuses a face." The refusal turned out not to
exist, but the silence did cost days: unresolved family → Segoe UI.notdef→ blank pixels, with no
warning anywhere. A dev-build warning when a requestedfontFamilyis not present in the collection
would have made both of these issues five minutes of work instead of a week. I have not authored that —
it needs aFindFamilyNamecall plus whichever logging facility is right for this layer, and I could not
verify either compiles without a build. Happy to write it if a maintainer points me at the logging helper.Suggested retitle
"App-bundled icon fonts render .notdef: WindowsTextLayoutManager passes nullptr as the DirectWrite font
collection" — or just close as a duplicate of #16306 once the PR lands, since they are the same bug. The
current title will send the next person on a font-binary goose chase.- The probe machine has none of these fonts installed, so its
Problem Description
Several stock icon-font TTFs render blank glyphs through RNW's new-arch text stack, even though they register into DirectWrite successfully and enumerate with correct family/
cmap/namerecords. Re-emitting the same fonts with byte-identical glyph data through a minimal OpenType writer that only recomputes table checksums (incl.head.checkSumAdjustment) and rebuilds the table directory makes the very same fonts render. No outlines, metrics, or name records changed in the control case — only checksums/table-directory layout.Fonts affected in our app (all stock files shipped in
react-native-vector-icons10.3.0): Ionicons, FontAwesome, Feather, Octicons, EvilIcons, AntDesign. These same files render fine in other Windows apps and on iOS/Android — icon-font-generator output frequently carries technically-stale checksums that everything else tolerates.Root cause — diagnosed from behavior, NOT traced to a specific line (confidence stated honestly). Some layer of the RNW/DWrite font path applies stricter TTF validation than the rest of Windows: the fonts register and enumerate, but glyph rendering silently produces blanks until the checksums/table directory are made strictly valid. We have not identified whether the rejection happens in an RNW-side loader/validation step or in how DirectWrite validates faces the way RNW loads/uses them. The failure is completely silent — no warning, no error, just blank glyphs — which made this extremely expensive to diagnose.
Suggested fix direction: either align validation tolerance with what DirectWrite/Windows accepts elsewhere (these fonts work everywhere else), or keep the strictness but surface a diagnostic when a face fails validation, and document the requirement.
Related but distinct from #16306 (spaced family names fail to resolve — a lookup problem). This issue is about faces that resolve/enumerate but whose glyphs never render — a loader/validation problem. We hit both in the same integration; fixing one does not fix the other.
Steps To Reproduce
Ionicons.ttffromreact-native-vector-icons10.3.0 (unmodified).windows.sharedFontspackage-manifest extension) and confirm via DirectWrite/WPF enumeration that the family is present with correct records.<Text style={{ fontFamily: 'Ionicons' }}>{glyph}</Text>with a mapped codepoint.Expected Results
Fonts that install and render everywhere else on Windows (and on iOS/Android) render in RNW — or, at minimum, a visible diagnostic is emitted when the font path refuses a face.
CLI version
18.0.0
Environment
Community Modules
react-native-vector-icons10.3.0 /@expo/vector-icons15.1.1 supply the affected TTFs, but the rendering path is core<Text>+ DirectWrite; any TTF with stale table checksums should reproduce.Target React Native Architecture
New Architecture (WinAppSDK) Only
Target Platform Version
10.0.22621
Visual Studio Version
Visual Studio 2026
Build Configuration
Debug
Snack, code example, screenshot, or link to a repository
The decisive experiment is steps 3–6 above: identical glyph data, only checksums/table directory recomputed, blank → rendering.
Our production workaround: a dependency-free build-time Node script re-emits the Windows copies of all bundled icon fonts with recomputed checksums and a rebuilt table directory (the same pass that renames spaced families for #16306), registered via
windows.sharedFonts.Related: #16306 (spaced family-name resolution), #15750 (IProvideFontInfo font-loading unification), #15316 (custom .ttf support on new arch).
Context: found during a Windows hardening pass of a production RNW app (Facilitron FIT — RNW 0.83.2 new-arch, Windows 11 ARM64, WinAppSDK 1.8, 250% display scale). Sibling PRs from the same investigation: #16302, #16303, #16304. Happy to provide before/after font binaries or a minimal repro repo, and to test candidate fixes in our app.