Skip to content

Fall back on glyph coverage, not just family name - #21

Merged
mantissaman merged 1 commit into
mainfrom
fix/cjk-glyph-coverage
Jul 29, 2026
Merged

Fall back on glyph coverage, not just family name#21
mantissaman merged 1 commit into
mainfrom
fix/cjk-glyph-coverage

Conversation

@mantissaman

Copy link
Copy Markdown
Contributor

Closes #14.

The bug

Font selection walked a chain of family names, the requested family then mapped alternatives then generics, and never asked whether the font it settled on could actually draw the text. A run asking for a Chinese family on a machine without it fell through to a Latin font, and every character came out as a missing-glyph box.

Name matching cannot detect this on its own. The font it picked exists and is perfectly valid, it simply has no glyphs for this script.

That is also why the reporter's font directory did not help. The font loaded fine, and selection still never consulted coverage, so it changed nothing.

The fix

The resolved font is checked against the run's own text. When characters are missing, a font that can draw them is looked for: families with broad non-Latin coverage first as a fast path, then the rest of the font database.

Selection prefers a font covering every missing character, and settles for the one covering the most when none covers all. This part matters more than it sounds. My first attempt picked on the first missing character alone, which looked like a fix and was not: a Japanese face carries the characters shared with Chinese but not the simplified-only ones, so a line of Chinese still rendered with a box in the middle of it. Caught it by looking at the output rather than trusting the test.

Two caches keep it affordable. Faces that have already rescued a run are tried before any search, which for a document in one script is almost always the answer again. Characters no available font can draw are remembered, so the scan is not repeated for every occurrence.

The list marker gets the same treatment, since bullet glyphs are missing from plenty of fonts too.

Scope, stated plainly

Per run, not per character. A run gets one font. Text mixing scripts inside a single run is still imperfect. Doing better means splitting runs into per-font segments, which is a bigger change to the shaping path.

No CJK font is bundled. They are large and every family needs its own licence file. This falls back to a system font, so CJK output needs a suitable one installed. Where none is, the requested font is kept so the text still occupies the right space rather than failing.

That second point has a consequence worth knowing before judging the result: this works on a machine with a CJK font and will still box out on a bare container. It is a real fix to the selection logic, not a guarantee of CJK output everywhere.

Verified

I have no file from the #1 reporter, so I built a document with Chinese, Japanese, Korean and Latin text and rendered it. Before: boxes for all CJK. After: all three scripts render, and Latin is untouched.

That is a weaker check than reproducing their exact case, and I would still like their file to confirm.

Four tests, all written to pass whether or not the machine has a CJK font:

  • Latin text resolves identically to the name-based path, so the common case cannot be disturbed
  • text nothing can draw keeps the requested font instead of failing
  • whitespace and control characters never trigger a search
  • where a covering font does exist, the replacement covers more of the text than the original

Checks

cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings and the full suite all pass. 356 tests, up from 354 on this branch's base plus the two from #20.

python3 scripts/hash_harness.py --check passes with 28 entries matching. The fixtures are all Latin, so the baselines are untouched.

Unrelated thing I noticed

The sample renders show doubled letters in places, "SSuite 400" and "eenthusiasm" in letter.png, and "LLatin" in my own test document. It reproduces on main without any of my changes, so it is pre-existing and not from this work. Looks like a shaping or cluster-mapping bug. Worth its own issue if it is not already known.

Font selection walked a chain of family names, the requested family then
mapped alternatives then generics, and never asked whether the font it
settled on could actually draw the text. A run asking for a Chinese
family on a machine without it fell through to a Latin font, and every
character came out as a missing-glyph box.

Name matching cannot detect this on its own. The font it picked exists
and is perfectly valid, it simply has no glyphs for this script. That is
also why pointing rdocx at a font directory did not help anyone: the
font loaded, and selection still never consulted coverage.

The resolved font is now checked against the run's own text, and when
characters are missing another font that can draw them is looked for.
Families with broad non-Latin coverage are tried first as a fast path,
then the rest of the font database.

Selection prefers a font covering every missing character, and settles
for the one covering the most when none covers all. Choosing on the
first missing character alone looked like a fix and was not: a Japanese
face carries the characters shared with Chinese but not the
simplified-only ones, so a line of Chinese still had gaps in it.

Two caches keep this affordable. Faces that have already rescued a run
are tried before any search, which for a document in one script is
almost always the answer again, and characters no available font can
draw are remembered so the scan is not repeated for every occurrence.

Deliberately not bundling a CJK font. They are large and every family
needs its own licence file. This falls back to a system font, so CJK
output needs a suitable one installed, and where none is the requested
font is kept so the text still occupies the right space.

Closes #14.
@mantissaman
mantissaman force-pushed the fix/cjk-glyph-coverage branch from 3abd36d to 1e0a571 Compare July 29, 2026 22:21
@mantissaman
mantissaman merged commit e673c0c into main Jul 29, 2026
7 checks passed
@mantissaman
mantissaman deleted the fix/cjk-glyph-coverage branch July 29, 2026 22:49
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.

CJK text renders as boxes, font fallback ignores glyph coverage

1 participant