Skip to content

PDFBOX-6251 usecmap cidrange precedence - #515

Open
pcorless wants to merge 2 commits into
apache:trunkfrom
pcorless:PDFBOX-6251-usecmap-cidrange-precedence
Open

PDFBOX-6251 usecmap cidrange precedence#515
pcorless wants to merge 2 commits into
apache:trunkfrom
pcorless:PDFBOX-6251-usecmap-cidrange-precedence

Conversation

@pcorless

@pcorless pcorless commented Sep 1, 2026

Copy link
Copy Markdown

PDFBOX-6251: a CMap's own cid mappings lose to the ones it inherits via usecmap

A CMap that starts with usecmap may redefine any code it inherits. It currently can't:
useCmap merges the parent's cidchar/cidrange mappings into the same collections as the
importing CMap's own, and since usecmap is read from the header, the parent's land in
codeToCid/codeToCidRanges first. toCID scans the range list first-match, so an inherited
range that covers a code beats the mapping the CMap declared for it.

new CMapParser().parsePredefined("ETenms-B5-H").toCID(0x41, 1);  // 13681, should be 34

ETenms-B5-H exists only to do this override: it uses ETen-B5-H, then remaps 0x20-0x7E to the
proportional latin CIDs 1-95 where the parent has the fullwidth forms at 13648+. Its parsed ranges
hold both, inherited first:

[0]   32..126 -> 13648   (from ETen-B5-H, always matches first)
[226] 32..126 -> 1       (its own, never reached)

Scope: 31 of the 92 bundled CMaps declare mappings of their own on top of a usecmap, and all
31 are affected. Two are horizontal (ETenms-B5-H, UniJIS-UCS2-HW-H); the other 29 are -V
variants whose own mappings select the vertical forms, so vertical CJK gets horizontal glyphs.

Symptom: the wrong glyph, and usually the wrong width with it — /W is indexed by CID, so a
wrong CID falls outside the array and the advance drops to /DW = 1000. Latin text renders as
J a v a S e r v e r.

Fix

Inherited mappings move to inheritedCodeToCid/inheritedCodeToCidRanges, consulted only after
the CMap's own: own map -> own ranges -> inherited map -> inherited ranges.

A usecmap chain stays nearest-wins, which needs opposite insertion orders: the maps are keyed, so
putAll is last-wins (deepest first, nearer on top); the range list is first-match, so the nearer
CMap's ranges go in ahead.

Three things fell out of it:

  • hasCIDMappings has to count inherited mappings. Identity-V declares none of its own, so
    once the inherited ones move it would report false and toCID would return 0 for every code.
  • The maps are copied, not shared. This kills a latent aliasing bug: the old
    codeToCid.putIfAbsent(length, mappings) stored the parent's inner map by reference when the
    importing CMap had no mapping of that length yet, so a later addCIDMapping wrote into the
    cached predefined parent and corrupted it for everyone after.
  • CID 0 is a mapping, not a miss. The private range scan returned 0 for both, which with two
    range lists would send a CMap's own cidrange to CID 0 through to the inherited mappings. It
    now returns -1 for "no range covers this"; public toCID still returns 0.

Tests

Seven added to TestCMapParser, mvn -pl fontbox -am test green (214):

Test Covers
testUseCmapOwnMappingsWin the case above, both toCID overloads
testUseCmapChainKeepsNearestMapping a two-level chain stays nearest-wins
testUseCmapOwnMappingsBeatInheritedRanges own cidchar and cidrange beat an inherited range
testUseCmapOnlyInheritedMappings Identity-V, everything inherited
testUseCmapDoesNotShareMappingsWithTheUsedCMap the used CMap isn't mutated
testUseCmapOwnRangeBeatsInheritedChar own cidrange beats an inherited cidchar
testUseCmapOwnMappingToCidZeroIsNotAFallthrough CID 0 is a mapping, not a miss

pcorless and others added 2 commits August 31, 2026 21:39
A CMap that begins with usecmap is meant to override the base CMap, but
both the cidchar/cidrange mappings were merged into the same collections
as the importing CMap's own. The range list is consulted first-match, so
an inherited cidrange that covers a code beat the cidchar the importing
CMap declared for it, and the override was silently lost.

ETenms-B5-H and UniJIS-UCS2-HW-H are affected among the bundled
predefined CMaps -- ETenms-B5-H maps 0x20-0x7e to the proportional
Latin cids 1-95, but ETen-B5-H's inherited range won, so 0x41 resolved
to cid 13681 (the fullwidth 'A') instead of 34. Together with their
vertical variants, 32 of the 100 bundled CMaps carried lost overrides.

Inherited mappings now live in their own map and range list, consulted
only after the CMap's own map and ranges. Chained usecmap keeps nearest-
wins ordering: the map merge is last-wins so the nearer CMap is applied
after the deeper one, and the range list is first-wins so the nearer
CMap's ranges are added ahead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first commit's two tests left the lookup order only partly guarded.

Identity-V is the one predefined CMap that declares no cid mappings of its
own, so every lookup it answers now goes through the inherited collections,
and it is the only reason hasCIDMappings has to consider them at all. It had
no test: testIdentity only covers Identity-H.

ETenms-B5-V does declare mappings of its own, six cidchars and twelve
cidranges for the punctuation whose vertical form differs, so the chain test's
javadoc saying it declares none was wrong. Those mappings also cover the case
of a CMap's own mappings beating the ranges it inherits from two levels up.

The remaining two cases have no predefined CMap that exercises them, so they
are built by hand: a cidrange of the CMap's own beating an inherited cidchar,
and the fact that what a CMap inherits is copied rather than shared, which
matters because predefined CMaps are cached and handed out repeatedly.

Also assert the override through the byte[] overload of toCID, which repeats
the lookup order of the int one and was only checked for a pass through.

Five of the six usecmap tests now fail without the CMap change; the Identity-V
one passes either way and is a regression guard for the new path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pcorless pcorless changed the title Pdfbox 6251 usecmap cidrange precedence PDFBOX-6251 usecmap cidrange precedence Sep 1, 2026
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.

1 participant