Author: tilman
Date: Wed Sep 9 15:02:07 2026
New Revision: 1938040
Log:
PDFBOX-6251: don't override CID mappings by inherited maps, by Patrick Corless;
closes #515
Modified:
pdfbox/branches/2.0/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java
pdfbox/branches/2.0/fontbox/src/test/java/org/apache/fontbox/cmap/TestCMapParser.java
Modified:
pdfbox/branches/2.0/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java
==============================================================================
--- pdfbox/branches/2.0/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java
Wed Sep 9 14:47:37 2026 (r1938039)
+++ pdfbox/branches/2.0/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java
Wed Sep 9 15:02:07 2026 (r1938040)
@@ -57,9 +57,13 @@ public class CMap
private final Map <String, byte[]> unicodeToByteCodes = new
HashMap<String, byte[]>();
// CID mappings
+ // map with all code to cid mappings organized by the origin byte length
of the input value
private final Map<Integer,Integer> codeToCid = new
HashMap<Integer,Integer>();
private final List<CIDRange> codeToCidRanges = new ArrayList<CIDRange>();
+ // the CMaps this one inherits from through the usecmap operator, see
useCmap
+ private final List<CMap> parentCMaps = new ArrayList<CMap>();
+
private static final String SPACE = " ";
private int spaceMapping = -1;
@@ -77,7 +81,19 @@ public class CMap
*/
public boolean hasCIDMappings()
{
- return !codeToCid.isEmpty() || !codeToCidRanges.isEmpty();
+ return !codeToCid.isEmpty() || !codeToCidRanges.isEmpty() ||
hasCIDMappings(parentCMaps);
+ }
+
+ private boolean hasCIDMappings(List<CMap> parentCMaps)
+ {
+ for (CMap cmap : parentCMaps)
+ {
+ if (cmap.hasCIDMappings())
+ {
+ return true;
+ }
+ }
+ return false;
}
/**
@@ -168,16 +184,66 @@ public class CMap
/**
* Returns the CID for the given character code.
*
+ * This method exists for convenience. It may return false values as the
origin byte length of the input value is
+ * unknown and the mapping for some input values aren't unique. <br>
+ * Example:<br>
+ * The two byte value 0x00, 0x65 maps to 0x20 <br>
+ * An input value of 0x65 always returns 0x20 even if the value has an
origin byte length of 1.
+ *
* @param code character code
* @return CID
*/
public int toCID(int code)
{
+ int cid = findCID(code);
+ if (cid != -1)
+ {
+ return cid;
+ }
+ return 0;
+ }
+
+ /**
+ * Returns the CID this CMap, or one of the CMaps it inherits from, maps
the given character code
+ * to, or -1 if none of them maps it. CID 0 is the .notdef glyph and a
CMap may map a code to it
+ * deliberately, so "mapped to 0" has to be told apart from "not mapped"
while the usecmap chain
+ * is walked. The public toCID methods report both as 0.
+ *
+ * @param code character code
+ * @return CID, or -1 if neither this CMap nor any it inherits from maps
the code
+ */
+ private int findCID(int code)
+ {
Integer cid = codeToCid.get(code);
if (cid != null)
{
return cid;
}
+ int cidFromRange = toCIDFromRanges(code);
+ if (cidFromRange != -1)
+ {
+ return cidFromRange;
+ }
+ // this CMap doesn't map the code itself, so ask the ones it inherits
from
+ for (CMap parentCMap : parentCMaps)
+ {
+ int parentCid = parentCMap.findCID(code);
+ if (parentCid != -1)
+ {
+ return parentCid;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the CID, the CID ranges of this CMap map the given character
code to.
+ *
+ * @param code character code
+ * @return CID, or -1 if no range covers the code
+ */
+ private int toCIDFromRanges(int code)
+ {
for (CIDRange range : codeToCidRanges)
{
int ch = range.map((char)code);
@@ -186,7 +252,7 @@ public class CMap
return ch;
}
}
- return 0;
+ return -1;
}
/**
@@ -231,13 +297,15 @@ public class CMap
/**
* This will add a CID mapping.
+ * <p>
+ * <b>This method had wrong parameter names until 2038</b>
*
- * @param code character code
- * @param cid CID
+ * @param cid character code
+ * @param code CID
*/
- void addCIDMapping(int code, int cid)
+ void addCIDMapping(int cid, int code)
{
- codeToCid.put(cid, code);
+ codeToCid.put(code, cid);
}
/**
@@ -250,14 +318,19 @@ public class CMap
*/
void addCIDRange(char from, char to, int cid)
{
+ addCIDRange(codeToCidRanges, from, to, cid);
+ }
+
+ private void addCIDRange(List<CIDRange> cidRanges, char from, char to, int
cid)
+ {
CIDRange lastRange = null;
- if (!codeToCidRanges.isEmpty())
+ if (!cidRanges.isEmpty())
{
- lastRange = codeToCidRanges.get(codeToCidRanges.size() - 1);
+ lastRange = cidRanges.get(cidRanges.size() - 1);
}
if (lastRange == null || !lastRange.extend(from, to, cid))
{
- codeToCidRanges.add(new CIDRange(from, to, cid));
+ cidRanges.add(new CIDRange(from, to, cid));
}
}
@@ -286,11 +359,14 @@ public class CMap
addCodespaceRange(codespaceRange);
}
charToUnicode.putAll(cmap.charToUnicode);
- codeToCid.putAll(cmap.codeToCid);
- codeToCidRanges.addAll(cmap.codeToCidRanges);
-
// unicodeToByteCodes should be filled too, but this isn't possible in
2.0.*
// because we don't know the code length
+
+ // The parent is kept, not merged: it is asked only for codes this
CMap doesn't map itself,
+ // so this CMap's own mappings win and a usecmap chain resolves
nearest-first. See toCID(int, int).
+ parentCMaps.add(cmap);
+ maxCodeLength = Math.max(maxCodeLength, cmap.maxCodeLength);
+ minCodeLength = Math.min(minCodeLength, cmap.minCodeLength);
}
/**
Modified:
pdfbox/branches/2.0/fontbox/src/test/java/org/apache/fontbox/cmap/TestCMapParser.java
==============================================================================
---
pdfbox/branches/2.0/fontbox/src/test/java/org/apache/fontbox/cmap/TestCMapParser.java
Wed Sep 9 14:47:37 2026 (r1938039)
+++
pdfbox/branches/2.0/fontbox/src/test/java/org/apache/fontbox/cmap/TestCMapParser.java
Wed Sep 9 15:02:07 2026 (r1938040)
@@ -205,4 +205,258 @@ public class TestCMapParser extends Test
CMap cmap = parser.parse(new ByteArrayInputStream(cmapData));
assertNotNull(cmap);
}
+
+ /**
+ * A CMap that redefines a code it inherits through usecmap must win over
the CMap it uses.
+ *
+ * ETenms-B5-H exists only to do that: it uses ETen-B5-H and then remaps
0x20-0x7E to the
+ * proportional latin CIDs 1-95, where the parent maps them to the full
width forms at 13648+.
+ */
+ public void testUseCmapOwnMappingsWin() throws IOException
+ {
+ CMap parent = new CMapParser().parsePredefined("ETen-B5-H");
+ assertEquals("ETen-B5-H maps 0x41 to the full width form", 13681,
parent.toCID(0x41));
+
+ CMap cMap = new CMapParser().parsePredefined("ETenms-B5-H");
+ assertEquals("ETenms-B5-H overrides 0x41 to the proportional form",
34, cMap.toCID(0x41));
+ assertEquals("ETenms-B5-H overrides 0x20 to the proportional form", 1,
cMap.toCID(0x20));
+
+ // codes the CMap does not redefine still come from the one it uses
+ assertEquals("an inherited code is unaffected",
+ parent.toCID(0xA140), cMap.toCID(0xA140));
+
+ // the byte[] overload repeats the lookup order of the int one, so
check the override there too
+ assertEquals(13681, parent.toCID(0x41));
+ assertEquals("the byte[] overload has to prefer the CMap's own mapping
as well",
+ 34, cMap.toCID(0x41));
+
+ // UniJIS-UCS2-HW-H likewise overrides its parent's proportional latin
with the half width forms
+ CMap halfWidth = new CMapParser().parsePredefined("UniJIS-UCS2-HW-H");
+ assertEquals("UniJIS-UCS2-H maps 0x0041 to the proportional form",
+ 34, new
CMapParser().parsePredefined("UniJIS-UCS2-H").toCID(0x41));
+ assertEquals("UniJIS-UCS2-HW-H overrides 0x0041 to the half width
form",
+ 264, halfWidth.toCID(0x41));
+ }
+
+ /**
+ * The override has to survive a chain of usecmap: ETenms-B5-V uses
ETenms-B5-H, which in turn
+ * uses ETen-B5-H. A code that only the middle CMap redefines has to keep
that redefinition.
+ */
+ public void testUseCmapChainKeepsNearestMapping() throws IOException
+ {
+ CMap cMap = new CMapParser().parsePredefined("ETenms-B5-V");
+
+ assertEquals("UniJIS-UCS2-HW-H overrides 0x0041 to the half width
form", 1, cMap.getWMode());
+ assertEquals("ETenms-B5-V inherits the proportional override from
ETenms-B5-H, not ETen-B5-H",
+ 34, cMap.toCID(0x41));
+ }
+
+ /**
+ * Both kinds of mapping a CMap declares have to beat the ranges it
inherits. ETenms-B5-V
+ * declares six cidchars and twelve cidranges for the punctuation whose
vertical form differs,
+ * on top of the horizontal forms it inherits from ETenms-B5-H and
ETen-B5-H.
+ *
+ * The cidchars were already resolved correctly before this was fixed, a
cidchar being consulted
+ * ahead of any range either way, so they are here as a guard rather than
as a second
+ * reproducer.
+ */
+ public void testUseCmapOwnMappingsBeatInheritedRanges() throws IOException
+ {
+ CMap horizontal = new CMapParser().parsePredefined("ETenms-B5-H");
+ CMap vertical = new CMapParser().parsePredefined("ETenms-B5-V");
+
+ // the horizontal forms come from an inherited range in both CMaps
+ assertEquals(110, horizontal.toCID(0xA14B));
+ assertEquals(111, horizontal.toCID(0xA14C));
+ assertEquals(121, horizontal.toCID(0xA156));
+
+ // ETenms-B5-V's own cidchars replace them with the vertical forms
+ assertEquals("own cidchar has to beat the inherited range", 13646,
vertical.toCID(0xA14B));
+ assertEquals("own cidchar has to beat the inherited range", 109,
vertical.toCID(0xA14C));
+ assertEquals("own cidchar has to beat the inherited range", 312,
vertical.toCID(0xA156));
+
+ // and its own cidranges likewise, two usecmap levels down
+ assertEquals(128, horizontal.toCID(0xA15D));
+ assertEquals("own cidchar has to beat the inherited range", 130,
vertical.toCID(0xA15D));
+ }
+
+ /**
+ * Identity-V is the one predefined CMap that declares no cid mappings at
all, it only uses
+ * Identity-H. Every lookup it answers is therefore an inherited one,
which also makes it the
+ * case that proves hasCIDMappings has to account for what a CMap
inherited.
+ */
+ public void testUseCmapOnlyInheritedMappings() throws IOException
+ {
+ CMap cMap = new CMapParser().parsePredefined("Identity-V");
+
+ assertEquals("Identity-V is vertical", 1, cMap.getWMode());
+ assertTrue("Identity-V has cid mappings, all of them inherited",
cMap.hasCIDMappings());
+
+ assertEquals("Identity-V CID 65", 65, cMap.toCID(65));
+ assertEquals("Identity-V CID 12345", 12345, cMap.toCID(0x3039));
+ assertEquals("Identity-V CID 0xFFFF", 0xFFFF, cMap.toCID(0xFFFF));
+ assertEquals("Identity-V CID 12345", 12345, cMap.toCID(0x3039));
+ }
+
+ /**
+ * A CMap holds on to the CMaps it uses rather than copying their
mappings, so it must never
+ * write into one: adding a mapping to the importing CMap must not reach
back into the used one.
+ *
+ * @throws java.io.IOException
+ */
+ public void testUseCmapDoesNotShareMappingsWithTheUsedCMap() throws
IOException
+ {
+ CMap used = new CMap();
+ used.addCIDMapping(100, 0x41);
+ used.addCIDRange((char) 0x50, (char) 0x5F, 200);
+
+ CMap cMap = new CMap();
+ cMap.useCmap(used);
+ assertEquals(100, cMap.toCID(0x41));//, "the mapping is inherited");
+ assertEquals(205, cMap.toCID(0x55));//, "the range is inherited");
+
+ cMap.addCIDMapping(300, 0x41);
+ cMap.addCIDRange((char) 0x50, (char) 0x5F, 400);
+
+ assertEquals(300, cMap.toCID(0x41));//, "the CMap's own mapping wins");
+ assertEquals(405, cMap.toCID(0x55));//, "the CMap's own range wins");
+ assertEquals(100, used.toCID(0x41));//, "the used CMap must not have
been modified");
+ assertEquals(205, used.toCID(0x55));//, "the used CMap must not have
been modified");
+ }
+
+ /**
+ * Everything a CMap declares outranks everything it inherits, so a
cidrange of its own has to
+ * beat an inherited cidchar too, not just an inherited cidrange. No
predefined CMap pairs the
+ * two that way round, hence the hand built pair here.
+ */
+ public void testUseCmapOwnRangeBeatsInheritedChar() throws IOException
+ {
+ CMap used = new CMap();
+ used.addCIDMapping(100, 0x41);
+
+ CMap cMap = new CMap();
+ cMap.useCmap(used);
+ cMap.addCIDRange((char) 0x40, (char) 0x4F, 200);
+
+ assertEquals(201, cMap.toCID(0x41));//, "the CMap's own range has to
beat the inherited char");
+ assertEquals(200, cMap.toCID(0x40));//, "a code the used CMap says
nothing about");
+ assertEquals(100, used.toCID(0x41));//, "the used CMap must not have
been modified");
+ }
+
+ /**
+ * A usecmap chain is resolved nearest first: a CMap is asked for its own
mappings, and only if
+ * it has none for the code does it pass the question on to the CMap it
uses. So a range in the
+ * nearer CMap outranks a cidchar in the one behind it, even though a
cidchar outranks a range
+ * within a single CMap.
+ */
+ public void testUseCmapNearerCMapWins()
+ {
+ CMap far = new CMap();
+ far.addCIDMapping(100, 0x41);
+
+ CMap near = new CMap();
+ near.useCmap(far);
+ near.addCIDRange((char) 0x40, (char) 0x4F, 200);
+
+ CMap cMap = new CMap();
+ cMap.useCmap(near);
+
+ assertEquals(201, cMap.toCID(0x41));//, "the nearer CMap's range has
to beat the farther CMap's cidchar");
+ assertEquals(100, far.toCID(0x41));//, "a used CMap answers for itself
unchanged");
+ }
+
+ /**
+ * "CMap files can be nested to five levels", so a redefinition has to
survive that depth, and
+ * each level has to be able to redefine what the level below it declared.
+ */
+ public void testUseCmapNestedToFiveLevels()
+ {
+ CMap cMap = new CMap();
+ cMap.addCIDMapping(10, 1);
+ for (int level = 2; level <= 5; level++)
+ {
+ CMap nested = new CMap();
+ nested.useCmap(cMap);
+ // redefine the code the level below just defined, and add one of
its own
+ nested.addCIDMapping(10 * level, level - 1);
+ nested.addCIDMapping(10 * level, level);
+ cMap = nested;
+ }
+
+ assertTrue(cMap.hasCIDMappings());
+ // every code but the last was redefined one level up, the last one
wasn't
+ assertEquals(20, cMap.toCID(0x01));
+ assertEquals(30, cMap.toCID(0x02));
+ assertEquals(40, cMap.toCID(0x03));
+ assertEquals(50, cMap.toCID(0x04));
+ assertEquals(50, cMap.toCID(0x05));
+ }
+
+ /**
+ * A CMap with no cid mappings of its own answers with the ones of the
CMap it uses, five levels
+ * down if need be. Identity-V is the predefined case of this,
testUseCmapOnlyInheritedMappings
+ * covers that one.
+ */
+ public void testUseCmapPassesThroughEmptyLevels()
+ {
+ CMap cMap = new CMap();
+ cMap.addCIDMapping(100, 0x41);
+ assertEquals(100, cMap.toCID(0x41));
+ for (int level = 2; level <= 5; level++)
+ {
+ CMap nested = new CMap();
+ nested.useCmap(cMap);
+ cMap = nested;
+ }
+
+ assertTrue(cMap.hasCIDMappings());//, "the mappings are five levels
down but they are there");
+ assertEquals(100, cMap.toCID(0x41));
+ assertEquals(0, cMap.toCID(0x42));//, "a code no level in the chain
maps");
+ }
+
+ /**
+ * The specification gives a CMap one usecmap, but nothing here has to
break if a file carries
+ * more than one. Every used CMap is kept, and they are asked in the order
they were declared.
+ */
+ public void testUseCmapSeveralUsedCMaps()
+ {
+ CMap first = new CMap();
+ first.addCIDMapping(100, 0x41);
+ first.addCIDMapping(101, 0x42);
+
+ CMap second = new CMap();
+ second.addCIDMapping(200, 0x42);
+ second.addCIDMapping(201, 0x43);
+
+ CMap cMap = new CMap();
+ cMap.useCmap(first);
+ cMap.useCmap(second);
+ cMap.addCIDMapping(300, 0x41);
+
+ assertEquals(300, cMap.toCID(0x41));//, "the CMap's own mapping beats
both");
+ assertEquals(101, cMap.toCID(0x42));//, "a code both used CMaps map
comes from the first");
+ assertEquals(201, cMap.toCID(0x43));//, "a code only the second maps
still resolves");
+ assertEquals(0, cMap.toCID(0x44));//, "a code none of them maps");
+ }
+
+ /**
+ * CID 0 is the .notdef glyph, and a CMap may map a code to it
deliberately. That is a mapping,
+ * not the absence of one, so it has to outrank whatever the CMap it uses
says about the code.
+ */
+ public void testUseCmapOwnMappingToCidZeroIsNotAFallthrough()
+ {
+ CMap used = new CMap();
+ used.addCIDRange((char) 0, (char) 0xFF, 500);
+
+ CMap cMap = new CMap();
+ cMap.useCmap(used);
+ cMap.addCIDRange((char) 0x41, (char) 0x41, 0);
+
+ assertEquals(0, cMap.toCID(0x41));//, "the CMap's own .notdef has to
win");
+ assertEquals(0, cMap.toCID(0x41));//, "the byte[] overload as well");
+ assertEquals(566, cMap.toCID(0x42));//, "a code it doesn't redefine
still comes from the parent");
+ assertEquals(565, used.toCID(0x41));//, "the used CMap answers for
itself unchanged");
+ }
+
+ // testToCidZeroAtShortestLengthStopsTheLengthProbing missing in 2.0
because length not properly supported
}