Tilman Hausherr created PDFBOX-6210:
---------------------------------------

             Summary: Incorrect CJK Character Extraction for Shared Glyphs
                 Key: PDFBOX-6210
                 URL: https://issues.apache.org/jira/browse/PDFBOX-6210
             Project: PDFBox
          Issue Type: Bug
    Affects Versions: 3.0.7 PDFBox, 2.0.36
            Reporter: Tilman Hausherr
             Fix For: 2.0.37, 3.0.8 PDFBox, 4.0.0


As described by Chanhyuk Lee in attached PR

===
h2. Problem

When multiple Unicode code points map to the same glyph, text extracted from a 
generated PDF may not match the character originally entered by the author.

This commonly occurs in CJK fonts, where a CJK Unified Ideograph and its 
radical counterpart share a glyph. For example, in Noto Sans CJK, both 食 
(U+98DF) and ⻝ (U+2EDD, CJK RADICAL EAT ONE) map to the same glyph. As a 
result, rendering 食 in a PDF and extracting it with PDFTextStripper may 
incorrectly produce ⻝.
h2. Root Cause

PDCIDFontType2Embedder.buildToUnicodeCMap() constructs the /ToUnicode map by 
reverse-looking up a glyph's code point using:
cmapLookup.getCharCodes(gid).get(0)
 
However, getCharCodes() returns code points sorted in ascending order. 
Consequently, the first entry is always the lowest code point associated with 
the glyph. In the example above, U+2EDD is selected instead of U+98DF because 
it has the smaller value.

As a result, the generated /ToUnicode mapping may point to a compatibility or 
radical character rather than the character that was actually used in the 
document. The existing comment ("use the first entry even for ambiguous 
mappings") already acknowledges this limitation.
h5. This issue is not unique to PDFBox and has also been observed in 
[wkhtmltopdf|https://github.com/wkhtmltopdf/wkhtmltopdf/issues/4414]/[Qt|https://github.com/qt/qtbase/blob/389988c42f901f2d8f75a023039d641cf5fba9de/src/gui/text/qfontsubset.cpp#L200-L209],
 [Mozilla Firefox|https://bugzilla.mozilla.org/show_bug.cgi?id=1881196], and 
[Typst|https://github.com/typst/typst/issues/4582].
h2. Fix

The code points actually used in the document are already tracked by 
TrueTypeEmbedder.addToSubset(int).

This change builds a glyph → code point mapping from those recorded inputs and 
uses it when generating the /ToUnicode CMap. For glyphs associated with 
multiple code points, the first code point encountered in the document is 
preferred.

The existing reverse cmap lookup is retained only as a fallback for glyphs that 
have no recorded input code point (for example, glyphs drawn directly by GID).

To ensure deterministic behavior, subsetCodePoints is changed from a HashSet to 
a LinkedHashSet, preserving insertion order and making the "first occurrence 
wins" rule stable.

This follows the same approach adopted by Typst to resolve the identical issue 
([typst/typst#4582|https://github.com/typst/typst/issues/4582], fixed in 
[typst/typst#4585|https://github.com/typst/typst/pull/4585]): prefer the code 
point that was actually used rather than reverse-mapping from the font cmap.
h2. Tests

Two tests were added to TestFontEmbedding using Noto Sans CJK KR:
h4. testToUnicodePrefersUsedCodePoint

Scans the font for any glyph shared by multiple printable code points 
(preferably a CJK ideograph/radical pair) and verifies that each code point 
round-trips through PDF generation and extraction as itself.
h4. testToUnicodeCjkAndRadicalLookAlike

Uses the explicit pair 食 (U+98DF) and ⻝ (U+2EDD) and verifies that:

both characters share the same glyph,
the radical is the first entry returned by the cmap reverse lookup,
an ideograph entered as 食 is extracted as 食, and
an intentionally entered radical ⻝ is preserved as ⻝.

Before this change, the test fails because 食 is extracted as ⻝.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to