fix(font): resolve Identity-H CJK via Encoding::Identity variant - #910
fix(font): resolve Identity-H CJK via Encoding::Identity variant#910gandli wants to merge 2 commits into
Conversation
char_to_unicode_uncached gated Identity recovery behind `if let Encoding::Standard(..)`, but PDF Identity-H/V names are folded into the Encoding::Identity enum variant, so the branch was skipped and CIDs fell through to a bare char::from_u32(cid) fallback (mis-decoding CJK glyphs, e.g. CID 0x69 -> 'i'). Replace the nested match with a single match over &self.encoding that accepts both Encoding::Identity and a Standard string naming an Identity/UCS2/UTF16 CMap, driving the unchanged recovery logic via an is_identity_enc flag. Adds a synthetic regression test (no external fixtures).
Extends the synthetic regression test beyond the single CJK offset case to cover the full surface the bug affected (the whole Identity recovery path was skipped whenever the parser folded Identity-H/V into the Encoding::Identity variant): - A: Identity-H + displaced ToUnicode CMap (proves recovery, not just CJK) - B: Identity-H without ToUnicode, CID==Unicode (CID-as-Unicode fallback) - C: Latin subset (structural, not CJK-specific) - D: UCS2-style direct char-code==Unicode path All synthetic, no external fixtures.
|
Thanks for the contribution, and welcome! You've correctly spotted that the Before this can merge, though, there are a couple of things to work through — the change is broader than the one glyph it targets. 1. Main concern — it changes a deliberate policy for the whole Identity-H population, not just your case. For a CID uncovered by a present-but-partial ToUnicode CMap, the existing 2. The test doesn't actually exercise the fix. I traced the cascade: scenarios A & C supply a complete ToUnicode CMap, so they resolve via PRIORITY 1 (which isn't encoding-gated) regardless of your change; B & D use ASCII ( 3. DRY. After this, the Priority-2 block and the line-4490 4. Minor. Really do appreciate the fix — Identity-H CJK extraction is a real gap. Let's get the test to reproduce the actual bug and the corpus diff to confirm direction, and I think this lands. |
|
Corpus regression result (419-PDF suite, main vs this PR): 0 differing files across text/markdown/html. Detail, because "0" here is informative rather than a coverage gap: the suite has 126 PDFs using But it also means the suite never hits the case your fix targets — a ToUnicode miss on an Identity-H CID font — so the corpus can neither confirm the fix nor exhibit the mojibake risk. Both therefore rest entirely on a test that actually reproduces the ToUnicode-miss path (embedded cmap + real CJK CID≠Unicode + absent/partial ToUnicode), plus the policy question of whether raw-CID-as-Unicode is the right fallback there vs the existing deliberate U+FFFD. That's the path to merge. |
yfedoseev
left a comment
There was a problem hiding this comment.
Thanks for this, and for the unusually clear write-up of the mechanism.
Your diagnosis is correct. font_dict.rs:2686 folds "Identity-H" | "Identity-V" into Encoding::Identity, so the if let Encoding::Standard(..) gate really did make that whole Type0 block unreachable for exactly the fonts it was written for. That's a genuine dead path and worth repairing.
The problem is that I can't get the tests to demonstrate it. I merged your branch onto current main (clean merge) and ran:
- All four tests pass with your
font_dict.rschange reverted. I restored just that file frommain, kept your test file, and got4 passed; 0 failed. Per CONTRIBUTING a bug-fix test has to fail before the change and pass after — these stay green either way, so they don't pin the behaviour you're fixing. - 419-PDF regression sweep, your branch merged onto main vs main: zero documents change. No regressions, which is good news, but also no observable improvement.
Both results point the same way: the block you've made reachable is a fallback, and the earlier paths in char_to_unicode_uncached (ToUnicode in particular) already resolve these fonts before control ever gets there. So the repair may well be correct in principle while changing no outcome in practice.
To make this reviewable, the fixture needs a font where the earlier paths genuinely can't resolve the code. The most promising shape looks like a Type0/Identity-H font with a non-Identity CIDSystemInfo ordering (e.g. Adobe-GB1) and no /ToUnicode, so the predefined-CMap lookup around line 4177 is the only thing that can produce a character — that one takes encoding_name, which is precisely what your change starts supplying. If such a fixture goes red on main and green on your branch, I'd be happy with the change.
Two smaller things while you're in there:
cargo fmt --checkisn't clean on the branch — a missing trailing comma atfont_dict.rs:4125and the block body underif is_identity_enc {never got re-indented after the nesting level was removed. CI enforces this one.- For
Encoding::Identityyou synthesise the name"Identity-H", which then reacheslookup_predefined_cmap. There's precedent for that at line 4320 so it's consistent, but a font that was actuallyIdentity-Vwill look up a horizontal CMap. Probably harmless for the Unicode value; worth a comment noting the choice is deliberate. - The branch is 34 commits behind
main. It merges cleanly, so no urgency, but a rebase before merge would keep CI honest.
Not a rejection at all — the analysis is right and I'd like to land it. It just needs a fixture that goes red without it.
|
@gandli — sorry for the five-week gap. Coming back to this with a smaller ask than I left you with. When I last wrote I asked you to work through the policy question and rebuild the test. Having spent more time in this function, most of what I was worried about turns out to be our problem rather than yours. The block your change routes Identity fonts into is one of two implementations of CID→GID→cmap recovery in What I would like in this PR:
If you would rather not carry (2), say so and I will split it out; the guard and the test are the parts I would want either way. |
Purpose
Fix a structural character-to-Unicode bug for Type0 / Identity-H / Identity-V
composite fonts whose
Encodingis parsed into theEncoding::Identityenum variant. Previously the entire Identity recovery path for such fonts
was skipped, so every CID fell through to a bare
char::from_u32(cid)fallback. This is not a single-glyph issue: it mis-decodes any glyph whose
code is not its own Unicode scalar — e.g. a subset CIDFontType2 code
0x69emitted as
i(U+0069) instead of the intended character, and silentlywrong characters wherever the raw code happens to be a valid but incorrect
code point.
Overview
font_dict.rs::char_to_unicode_uncachedgated its Identity handling behindif let Encoding::Standard(ref encoding_name) = self.encoding. But PDFIdentity-H / Identity-V names are collapsed into the
Encoding::Identityenum variant (see the
Encodingdoc comment), so theif let Standard(..)arm never matched and the whole block — ToUnicode CMap, embedded TrueType
cmap, UCS2/UTF16 direct mapping, and the CID-as-Unicode last resort — was
bypassed.
The fix replaces the nested
if let+if encoding_name == ...with a singlematchover&self.encodingthat accepts bothEncoding::Identityand aStandardstring naming an Identity/UCS2/UTF16 CMap, producing a booleanis_identity_encthat drives the (otherwise unchanged) recovery logic. Allfour sub-paths below are now reached for the folded variant, exactly as they
already were for the
Standard("Identity-H")string form:Context
The trigger is purely structural: whenever the parser folds an
Identity-H/V encoding into
Encoding::Identity, the font lost all Unicoderecovery and degraded to raw-code emission. Language-agnostic — CJK, Latin,
and symbol subsets are equally affected. Root cause was the
Encodingenumfolding, not the ToUnicode CMap itself. This routes
Encoding::Identityfonts through the same recovery path
Encoding::Standard("Identity-H")fonts already used.
Verification
Regression test
tests/test_identity_h_encoding_variant_cjk.rsis 100%synthetic (no third-party or external fixture). It builds Type0 / Identity-H
fonts and asserts the true text is recovered while mojibake does not surface,
covering the four sub-paths above:
works, not just for one glyph)
fallback sub-path)
cargo test --release --test test_identity_h_encoding_variant_cjk→4 passed; 0 failed.
Real-world confirmation: a CJK document whose text layer contained
i务流水号under the buggy build extracts as业务流水号after the fix(the first character is no longer mis-decoded as
i).Reviewer guidance
matchofchar_to_unicode_uncached;non-Identity code paths are untouched.
lookup_predefined_cmapis now called with&encoding_name(the bindingis now a
Stringrather than a&String).truetype_cmap()returnsNoneand recovery still depends on ToUnicode / predefined CMap — identicalto the prior behaviour for the
Standard("Identity-H")form, i.e. noregression introduced there.