Fall back on glyph coverage, not just family name - #21
Merged
Conversation
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
force-pushed
the
fix/cjk-glyph-coverage
branch
from
July 29, 2026 22:21
3abd36d to
1e0a571
Compare
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.
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:
Checks
cargo fmt --all --check,cargo clippy --workspace --all-targets -- -D warningsand 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 --checkpasses 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.