Skip to content

Registered icon-font TTFs render blank glyphs until table checksums/table directory are recomputed — silent failure in the DWrite font path #16308

Description

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/name records. 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-icons 10.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

  1. Take stock Ionicons.ttf from react-native-vector-icons 10.3.0 (unmodified).
  2. Register it so DirectWrite sees it (we use the windows.sharedFonts package-manifest extension) and confirm via DirectWrite/WPF enumeration that the family is present with correct records.
  3. Render <Text style={{ fontFamily: 'Ionicons' }}>{glyph}</Text> with a mapped codepoint.
  4. Observe: blank output (not tofu boxes — blank), no diagnostics.
  5. Control: re-emit the same TTF recomputing all table checksums and rebuilding the table directory (glyph data untouched); replace the registered file.
  6. Glyphs now render.

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

System:
  OS: Windows 11 10.0.26200 (ARM64 device; app builds and runs ARM64)
  CPU: (6) x64 Apple Silicon (Windows-on-ARM)
  Memory: 6.49 GB / 15.99 GB
Binaries:
  Node: 22.15.0
  Yarn: 4.5.1
  npm: 10.9.2
SDKs:
  Windows SDK versions: 10.0.19041.0, 10.0.22621.0, 10.0.26100.0
IDEs:
  Visual Studio: 18.6.11822.322 (Community 2026), 17.14.37314.3 (Community 2022)
npmPackages (yarn 4 workspaces — `cli info` reports Not Found in-workspace):
  react-native: 0.83.2
  react-native-windows: 0.83.2 (New Architecture / Fabric composition)
  Microsoft.WindowsAppSDK: 1.8

Community Modules

react-native-vector-icons 10.3.0 / @expo/vector-icons 15.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.

Activity

  1. FaithfulAudio commented on Jul 26, 2026

    @FaithfulAudio
    ContributorAuthor

    Reporter 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.0 it reports IDWriteFontFile::Analyze, the HRESULT from
    IDWriteFontSetBuilder1::AddFontFile, the real family name from the name table, and then replays RNW's
    call sequence (CreateTextFormat with the same empty localeName the real code passes →
    CreateTextLayout → Draw) with a recording IDWriteTextRenderer that reports the resolved face and
    glyph index. The drawn codepoint is taken from each face's own IDWriteFontFace1::GetUnicodeRanges, so
    it is guaranteed to be one that font claims to map.

    file (unmodified) Analyze supported AddFontFile real family name codepoint collection containing it nullptr (today)
    MaterialIcons.ttf 1 hr=0 accepted Material Icons U+E000 glyph 40 Segoe UI, glyph 0
    MaterialCommunityIcons.ttf 1 hr=0 accepted Material Design Icons U+F0001 glyph 1 Segoe UI, glyph 0
    Ionicons.ttf 1 hr=0 accepted Ionicons U+EA01 glyph 1 Segoe UI, glyph 0
    FontAwesome.ttf 1 hr=0 accepted FontAwesome U+F000 glyph 13 Segoe UI, glyph 0
    Feather.ttf 1 hr=0 accepted Feather U+F100 glyph 4 Segoe UI, glyph 0
    Octicons.ttf 1 hr=0 accepted Octicons U+F10B glyph 4 Segoe UI, glyph 0
    EvilIcons.ttf 1 hr=0 accepted EvilIcons U+F100 glyph 4 Segoe UI, glyph 0
    AntDesign.ttf 1 hr=0 accepted anticon U+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, AddFontFile accepts them with hr=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), AddFontResource and sharedFonts have
    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::GetTextLayout passes nullptr as the font collection
    (WindowsTextLayoutManager.cpp L119):

    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 same Segoe UI glyph 0. Whether .notdef paints 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() to DWriteHelpers — system font set merged with every
    *.ttf/*.otf under the app's Assets\ and Assets\Fonts\, built once, failing closed to nullptr
    so behaviour is unchanged for apps that bundle no fonts — and pass it at that CreateTextFormat call
    instead of nullptr. The same PR (#16339) fixes #16306.

    What I could not verify, stated plainly

    • The probe machine has none of these fonts installed, so its nullptr rows fail because the font is
      genuinely absent. I did not reproduce the windows.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 is anticon, not AntDesign. Requesting AntDesign gives
      .notdef even 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 requested fontFamily is 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 a FindFamilyName call 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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs: Triage 🔍New issue that needs to be reviewed by the issue management team (label applied by bot)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions