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
 }

Reply via email to