Author: tilman
Date: Wed Sep  9 15:02:17 2026
New Revision: 1938042

Log:
PDFBOX-6251: don't override CID mappings by inherited maps, by Patrick Corless; 
closes #515

Modified:
   pdfbox/trunk/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java
   
pdfbox/trunk/fontbox/src/test/java/org/apache/fontbox/cmap/TestCMapParser.java

Modified: pdfbox/trunk/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java
==============================================================================
--- pdfbox/trunk/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java        
Wed Sep  9 15:02:11 2026        (r1938041)
+++ pdfbox/trunk/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java        
Wed Sep  9 15:02:17 2026        (r1938042)
@@ -65,6 +65,9 @@ public class CMap
     private final Map<Integer, Map<Integer, Integer>> codeToCid = new 
HashMap<>();
     private final List<CIDRange> codeToCidRanges = new ArrayList<>();
 
+    // the CMaps this one inherits from through the usecmap operator, see 
useCmap
+    private final List<CMap> parentCMaps = new ArrayList<>();
+
     // inverted map
     private final Map <String, byte[]> unicodeToByteCodes = new HashMap<>();
 
@@ -85,7 +88,8 @@ public class CMap
      */
     public boolean hasCIDMappings()
     {
-        return !codeToCid.isEmpty() || !codeToCidRanges.isEmpty();
+        return !codeToCid.isEmpty() || !codeToCidRanges.isEmpty()
+                || parentCMaps.stream().anyMatch(CMap::hasCIDMappings);
     }
 
     /**
@@ -237,17 +241,7 @@ public class CMap
      */
     public int toCID(byte[] code)
     {
-        if (!hasCIDMappings() || code.length < minCidLength || code.length > 
maxCidLength)
-        {
-            return 0;
-        }
-        Integer cid = null;
-        Map<Integer, Integer> codeToCidMap = codeToCid.get(code.length);
-        if (codeToCidMap != null)
-        {
-            cid = codeToCidMap.get(toInt(code));
-        }
-        return cid != null ? cid : toCIDFromRanges(code);
+        return toCID(toInt(code), code.length);
     }
 
     /**
@@ -264,17 +258,15 @@ public class CMap
      */
     public int toCID(int code)
     {
-        if (!hasCIDMappings())
+        for (int length = minCidLength; length <= maxCidLength; length++)
         {
-            return 0;
-        }
-        int cid = 0;
-        int length = minCidLength;
-        while (cid == 0 && (length <= maxCidLength))
-        {
-            cid = toCID(code, length++);
+            int cid = findCID(code, length);
+            if (cid != -1)
+            {
+                return cid;
+            }
         }
-        return cid;
+        return 0;
     }
 
     /**
@@ -286,57 +278,67 @@ public class CMap
      */
     public int toCID(int code, int length)
     {
-        if (!hasCIDMappings() || length < minCidLength || length > 
maxCidLength)
-        {
-            return 0;
-        }
-        Integer cid = null;
-        Map<Integer, Integer> codeToCidMap = codeToCid.get(length);
-        if (codeToCidMap != null)
-        {
-            cid = codeToCidMap.get(code);
-        }
-        return cid != null ? cid : toCIDFromRanges(code, length);
+        int cid = findCID(code, length);
+        return cid != -1 ? cid : 0;
     }
 
     /**
-     * Returns the CID for the given character code.
+     * 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
+     * @param length the origin byte length of the code
+     * @return CID, or -1 if neither this CMap nor any it inherits from maps 
the code
      */
-
-    private int toCIDFromRanges(int code, int length)
+    private int findCID(int code, int length)
     {
-        for (CIDRange range : codeToCidRanges)
+        if (length < minCidLength || length > maxCidLength)
         {
-            int ch = range.map(code, length);
-            if (ch != -1)
+            return -1;
+        }
+        Map<Integer, Integer> codeToCidMap = codeToCid.get(length);
+        Integer cid = codeToCidMap != null ? codeToCidMap.get(code) : null;
+        if (cid != null)
+        {
+            return cid;
+        }
+        int cidFromRange = toCIDFromRanges(code, length);
+        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, length);
+            if (parentCid != -1)
             {
-                return ch;
+                return parentCid;
             }
         }
-        return 0;
+        return -1;
     }
 
     /**
-     * Returns the CID for the given character code.
+     * Returns the CID, the CID ranges of this CMap map the given character 
code to.
      *
-     * @param code character code
-     * @return CID
+     * @param code   character code
+     * @param length the origin byte length of the code
+     * @return CID, or -1 if no range covers the code
      */
-
-    private int toCIDFromRanges(byte[] code)
+    private int toCIDFromRanges(int code, int length)
     {
         for (CIDRange range : codeToCidRanges)
         {
-            int ch = range.map(code);
+            int ch = range.map(code, length);
             if (ch != -1)
             {
                 return ch;
             }
         }
-        return 0;
+        return -1;
     }
 
     /**
@@ -476,15 +478,9 @@ public class CMap
                 }
                 unicodeToByteCodes.put(v, bar);
             });
-        cmap.codeToCid.forEach((key, value) ->
-        {
-            Map<Integer, Integer> existingMapping = codeToCid.putIfAbsent(key, 
value);
-            if (existingMapping!=null)
-            {
-                existingMapping.putAll(value);
-            }
-        });
-        codeToCidRanges.addAll(cmap.codeToCidRanges);
+        // 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);
         maxCidLength = Math.max(maxCidLength, cmap.maxCidLength);

Modified: 
pdfbox/trunk/fontbox/src/test/java/org/apache/fontbox/cmap/TestCMapParser.java
==============================================================================
--- 
pdfbox/trunk/fontbox/src/test/java/org/apache/fontbox/cmap/TestCMapParser.java  
    Wed Sep  9 15:02:11 2026        (r1938041)
+++ 
pdfbox/trunk/fontbox/src/test/java/org/apache/fontbox/cmap/TestCMapParser.java  
    Wed Sep  9 15:02:17 2026        (r1938042)
@@ -1,264 +1,544 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.fontbox.cmap;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import org.apache.pdfbox.io.RandomAccessReadBuffer;
-
-import org.apache.pdfbox.io.RandomAccessReadBufferedFile;
-
-import org.junit.jupiter.api.Test;
-
-/**
- * This will test the CMapParser implementation.
- *
- */
-class TestCMapParser
-{
-
-    /**
-     * Check whether the parser and the resulting mapping is working correct.
-     *
-     * @throws IOException If something went wrong
-     */
-    @Test
-    void testLookup() throws IOException
-    {
-        final String resourceDir = "src/test/resources/cmap";
-        File inDir = new File(resourceDir);
-
-        CMap cMap = new CMapParser()
-                .parse(new RandomAccessReadBufferedFile(new File(inDir, 
"CMapTest")));
-
-        // char mappings
-        byte[] bytes1 = {0, 1};
-        assertEquals("A", cMap.toUnicode(bytes1), "bytes 00 01 from bfrange 
<0001> <0005> <0041>");
-
-        byte[] bytes2 = {1, 00};
-        String str2 = "0";
-        assertEquals(str2,
-                cMap.toUnicode(bytes2), "bytes 01 00 from bfrange <0100> 
<0109> <0030>");
-
-        byte[] bytes3 = { 1, 32 };
-        assertEquals("P", cMap.toUnicode(bytes3), "bytes 01 00 from bfrange 
<0100> <0109> <0030>");
-
-        byte[] bytes4 = { 1, 33 };
-        assertEquals("R", cMap.toUnicode(bytes4), "bytes 01 00 from bfrange 
<0100> <0109> <0030>");
-
-        byte[] bytes5 = { 0, 10 };
-        String str5 = "*";
-        assertEquals(str5, cMap.toUnicode(bytes5), "bytes 00 0A from bfchar 
<000A> <002A>");
-
-        byte[] bytes6 = { 1, 10 };
-        String str6 = "+";
-        assertEquals(str6, cMap.toUnicode(bytes6), "bytes 01 0A from bfchar 
<010A> <002B>");
-
-        // CID mappings
-        byte[] cid1 = { 0, 65 };
-        assertEquals(65, cMap.toCID(cid1), "CID 65 from cidrange <0000> <00ff> 
0 ");
-
-        byte[] cid2 = { 1, 24 };
-        int strCID2 = 0x0118;
-        assertEquals(strCID2, cMap.toCID(cid2), "CID 280 from cidrange <0100> 
<01ff> 256");
-
-        byte[] cid3 = { 2, 8 };
-        int strCID3 = 0x0208;
-        assertEquals(strCID3, cMap.toCID(cid3), "CID 520 from cidchar <0208> 
520");
-
-        byte[] cid4 = { 1, 0x2c };
-        int strCID4 = 0x12C;
-        assertEquals(strCID4, cMap.toCID(cid4), "CID 300 from cidrange <0300> 
<0300> 300");
-    }
-
-    @Test
-    void testIdentity() throws IOException
-    {
-        CMap cMap = new CMapParser().parsePredefined("Identity-H");
-
-        assertEquals(65, cMap.toCID(new byte[] { 0, 65 }), "Indentity-H CID 
65");
-        assertEquals(12345, cMap.toCID(new byte[] { 0x30, 0x39 }), 
"Indentity-H CID 12345");
-        assertEquals(0xFFFF, cMap.toCID(new byte[] { (byte) 0xFF, (byte) 0xFF 
}),
-                "Indentity-H CID 0xFFFF");
-    }
-
-    @Test
-    void testUniJIS_UTF16_H() throws IOException
-    {
-        CMap cMap = new CMapParser().parsePredefined("UniJIS-UTF16-H");
-
-        // the next 3 cases demonstrate the issue of possible false result 
values of CMap.toCID(int code)
-        assertEquals(694, cMap.toCID(0xb1), "UniJIS-UTF16-H CID 0xb1 -> 694");
-        assertNotEquals(694, cMap.toCID(0xb1, 1), "UniJIS-UTF16-H CID 0xb1 -> 
694");
-        assertEquals(694, cMap.toCID(0xb1, 2), "UniJIS-UTF16-H CID 0x00b1 -> 
694");
-
-        // 1:1 cid char mapping
-        assertEquals(694, cMap.toCID(new byte[] { 0x00, (byte) 0xb1 }),
-                "UniJIS-UTF16-H CID 0x00b1 -> 694");
-        assertEquals(20168, cMap.toCID(new byte[] { (byte) 0xd8, 0x50, (byte) 
0xdc, 0x4b }),
-                "UniJIS-UTF16-H CID 0xd850dc4b -> 20168");
-
-        // cid range mapping
-        assertEquals(19223, cMap.toCID(new byte[] { 0x54, 0x34 }),
-                "UniJIS-UTF16-H CID 0x5434 -> 19223");
-        assertEquals(10006, cMap.toCID(new byte[] { (byte) 0xd8, 0x3c, (byte) 
0xdd, 0x12 }),
-                "UniJIS-UTF16-H CID 0xd83cdd12 -> 10006");
-
-    }
-
-    @Test
-    void testUniJIS_UCS2_H() throws IOException
-    {
-        CMap cMap = new CMapParser().parsePredefined("UniJIS-UCS2-H");
-
-        assertEquals(34, cMap.toCID(new byte[] { 0, 65 }), "UniJIS-UCS2-H CID 
65 -> 34");
-    }
-
-    @Test
-    void testAdobe_GB1_UCS2() throws IOException
-    {
-        CMap cMap = new CMapParser().parsePredefined("Adobe-GB1-UCS2");
-
-        assertEquals("0", cMap.toUnicode(new byte[] { 0, 0x11 }),
-                "Adobe-GB1-UCS2 CID 0x11 -> \"0\"");
-    }
-
-    /**
-     * Test the parser against a valid, but poorly formatted CMap file.
-     * @throws IOException If something went wrong
-     */
-    @Test
-    void testParserWithPoorWhitespace() throws IOException
-    {
-        CMap cMap = new CMapParser().parse(new RandomAccessReadBufferedFile(
-                new File("src/test/resources/cmap", "CMapNoWhitespace")));
-
-        assertNotNull(cMap, "Failed to parse nasty CMap file");
-    }
-
-    @Test
-    void testParserWithMalformedbfrange1() throws IOException
-    {
-        CMap cMap = new CMapParser()
-                .parse(new RandomAccessReadBufferedFile(
-                        new File("src/test/resources/cmap", 
"CMapMalformedbfrange1")));
-
-        assertNotNull(cMap, "Failed to parse malformed CMap file");
-
-        byte[] bytes1 = { 0, 1 };
-        assertEquals("A", cMap.toUnicode(bytes1), "bytes 00 01 from bfrange 
<0001> <0009> <0041>");
-
-        byte[] bytes2 = { 1, 00 };
-        assertNull(cMap.toUnicode(bytes2));
-
-    }
-
-    @Test
-    void testParserWithMalformedbfrange2() throws IOException
-    {
-        CMap cMap = new CMapParser()
-                .parse(new RandomAccessReadBufferedFile(
-                        new File("src/test/resources/cmap", 
"CMapMalformedbfrange2")));
-
-        assertNotNull(cMap, "Failed to parse malformed CMap file");
-
-        assertEquals("0", cMap.toUnicode(new byte[] { 0, 1 }),
-                "bytes 00 01 from bfrange <0001> <0009> <0030>");
-
-        assertEquals("A", cMap.toUnicode(new byte[] { 2, 0x32 }),
-                "bytes 02 32 from bfrange <0232> <0432> <0041>");
-
-        // check border values for non strict mode
-        assertNotNull(cMap.toUnicode(new byte[] { 2, (byte) 0xF0 }));
-        assertNotNull(cMap.toUnicode(new byte[] { 2, (byte) 0xF1 }));
-
-        // use strict mode
-        cMap = new CMapParser(true)
-                .parse(new RandomAccessReadBufferedFile(
-                        new File("src/test/resources/cmap", 
"CMapMalformedbfrange2")));
-        // check border values for strict mode
-        assertNotNull(cMap.toUnicode(new byte[] { 2, (byte) 0xF0 }));
-        assertNull(cMap.toUnicode(new byte[] { 2, (byte) 0xF1 }));
-
-    }
-
-    @Test
-    void testPredefinedMap() throws IOException
-    {
-        CMap cMap = new CMapParser().parsePredefined("Adobe-Korea1-UCS2");
-        assertNotNull(cMap, "Failed to parse predefined CMap 
Adobe-Korea1-UCS2");
-
-        assertEquals("Adobe-Korea1-UCS2", cMap.getName(), "wrong CMap name");
-        assertEquals(0, cMap.getWMode(), "wrong WMode");
-        assertFalse(cMap.hasCIDMappings());
-        assertTrue(cMap.hasUnicodeMappings());
-
-        cMap = new CMapParser().parsePredefined("Identity-V");
-        assertNotNull(cMap, "Failed to parse predefined CMap Identity-V");
-    }
-
-    @Test
-    void testIdentitybfrange() throws IOException
-    {
-        // use strict mode
-        CMap cMap = new CMapParser(true)
-                .parse(new RandomAccessReadBufferedFile(
-                        new File("src/test/resources/cmap", 
"Identitybfrange")));
-        assertEquals("Adobe-Identity-UCS", cMap.getName(), "wrong CMap name");
-
-        byte[] bytes = { 0, 65 };
-        assertEquals(new String(bytes, StandardCharsets.UTF_16BE), 
cMap.toUnicode(bytes),
-                "Indentity 0x0048");
-        bytes = new byte[] { 0x30, 0x39 };
-        assertEquals(new String(bytes, StandardCharsets.UTF_16BE), 
cMap.toUnicode(bytes),
-                "Indentity 0x3039");
-        // check border values for strict mode
-        bytes = new byte[] { 0x30, (byte) 0xFF };
-        assertEquals(new String(bytes, StandardCharsets.UTF_16BE), 
cMap.toUnicode(bytes),
-                "Indentity 0x30FF");
-        // check border values for strict mode
-        bytes = new byte[] { 0x31, 0x00 };
-        assertEquals(new String(bytes, StandardCharsets.UTF_16BE), 
cMap.toUnicode(bytes),
-                "Indentity 0x3100");
-        bytes = new byte[] { (byte) 0xFF, (byte) 0xFF };
-        assertEquals(new String(bytes, StandardCharsets.UTF_16BE), 
cMap.toUnicode(bytes),
-                "Indentity 0xFFFF");
-
-    }
-
-    /**
-     * Test that parsing a CMap with empty byte arrays in bfrange does not 
throw
-     * ArrayIndexOutOfBoundsException. Empty hex strings produce zero-length 
byte
-     * arrays, causing increment() to be called with position -1.
-     */
-    @Test
-    void testBadIncrement() throws IOException
-    {
-        byte[] cmapData = "1 beginbfrange\n<> <> 
<2223>\nendbfrange".getBytes("US-ASCII");
-        CMapParser parser = new CMapParser();
-        CMap cmap = parser.parse(new RandomAccessReadBuffer(cmapData));
-        assertNotNull(cmap);
-    }
-}
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.fontbox.cmap;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import org.apache.pdfbox.io.RandomAccessReadBuffer;
+
+import org.apache.pdfbox.io.RandomAccessReadBufferedFile;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * This will test the CMapParser implementation.
+ *
+ */
+class TestCMapParser
+{
+
+    /**
+     * Check whether the parser and the resulting mapping is working correct.
+     *
+     * @throws IOException If something went wrong
+     */
+    @Test
+    void testLookup() throws IOException
+    {
+        final String resourceDir = "src/test/resources/cmap";
+        File inDir = new File(resourceDir);
+
+        CMap cMap = new CMapParser()
+                .parse(new RandomAccessReadBufferedFile(new File(inDir, 
"CMapTest")));
+
+        // char mappings
+        byte[] bytes1 = {0, 1};
+        assertEquals("A", cMap.toUnicode(bytes1), "bytes 00 01 from bfrange 
<0001> <0005> <0041>");
+
+        byte[] bytes2 = {1, 00};
+        String str2 = "0";
+        assertEquals(str2,
+                cMap.toUnicode(bytes2), "bytes 01 00 from bfrange <0100> 
<0109> <0030>");
+
+        byte[] bytes3 = { 1, 32 };
+        assertEquals("P", cMap.toUnicode(bytes3), "bytes 01 00 from bfrange 
<0100> <0109> <0030>");
+
+        byte[] bytes4 = { 1, 33 };
+        assertEquals("R", cMap.toUnicode(bytes4), "bytes 01 00 from bfrange 
<0100> <0109> <0030>");
+
+        byte[] bytes5 = { 0, 10 };
+        String str5 = "*";
+        assertEquals(str5, cMap.toUnicode(bytes5), "bytes 00 0A from bfchar 
<000A> <002A>");
+
+        byte[] bytes6 = { 1, 10 };
+        String str6 = "+";
+        assertEquals(str6, cMap.toUnicode(bytes6), "bytes 01 0A from bfchar 
<010A> <002B>");
+
+        // CID mappings
+        byte[] cid1 = { 0, 65 };
+        assertEquals(65, cMap.toCID(cid1), "CID 65 from cidrange <0000> <00ff> 
0 ");
+
+        byte[] cid2 = { 1, 24 };
+        int strCID2 = 0x0118;
+        assertEquals(strCID2, cMap.toCID(cid2), "CID 280 from cidrange <0100> 
<01ff> 256");
+
+        byte[] cid3 = { 2, 8 };
+        int strCID3 = 0x0208;
+        assertEquals(strCID3, cMap.toCID(cid3), "CID 520 from cidchar <0208> 
520");
+
+        byte[] cid4 = { 1, 0x2c };
+        int strCID4 = 0x12C;
+        assertEquals(strCID4, cMap.toCID(cid4), "CID 300 from cidrange <0300> 
<0300> 300");
+    }
+
+    @Test
+    void testIdentity() throws IOException
+    {
+        CMap cMap = new CMapParser().parsePredefined("Identity-H");
+
+        assertEquals(65, cMap.toCID(new byte[] { 0, 65 }), "Indentity-H CID 
65");
+        assertEquals(12345, cMap.toCID(new byte[] { 0x30, 0x39 }), 
"Indentity-H CID 12345");
+        assertEquals(0xFFFF, cMap.toCID(new byte[] { (byte) 0xFF, (byte) 0xFF 
}),
+                "Indentity-H CID 0xFFFF");
+    }
+
+    @Test
+    void testUniJIS_UTF16_H() throws IOException
+    {
+        CMap cMap = new CMapParser().parsePredefined("UniJIS-UTF16-H");
+
+        // the next 3 cases demonstrate the issue of possible false result 
values of CMap.toCID(int code)
+        assertEquals(694, cMap.toCID(0xb1), "UniJIS-UTF16-H CID 0xb1 -> 694");
+        assertNotEquals(694, cMap.toCID(0xb1, 1), "UniJIS-UTF16-H CID 0xb1 -> 
694");
+        assertEquals(694, cMap.toCID(0xb1, 2), "UniJIS-UTF16-H CID 0x00b1 -> 
694");
+
+        // 1:1 cid char mapping
+        assertEquals(694, cMap.toCID(new byte[] { 0x00, (byte) 0xb1 }),
+                "UniJIS-UTF16-H CID 0x00b1 -> 694");
+        assertEquals(20168, cMap.toCID(new byte[] { (byte) 0xd8, 0x50, (byte) 
0xdc, 0x4b }),
+                "UniJIS-UTF16-H CID 0xd850dc4b -> 20168");
+
+        // cid range mapping
+        assertEquals(19223, cMap.toCID(new byte[] { 0x54, 0x34 }),
+                "UniJIS-UTF16-H CID 0x5434 -> 19223");
+        assertEquals(10006, cMap.toCID(new byte[] { (byte) 0xd8, 0x3c, (byte) 
0xdd, 0x12 }),
+                "UniJIS-UTF16-H CID 0xd83cdd12 -> 10006");
+
+    }
+
+    @Test
+    void testUniJIS_UCS2_H() throws IOException
+    {
+        CMap cMap = new CMapParser().parsePredefined("UniJIS-UCS2-H");
+
+        assertEquals(34, cMap.toCID(new byte[] { 0, 65 }), "UniJIS-UCS2-H CID 
65 -> 34");
+    }
+
+    @Test
+    void testAdobe_GB1_UCS2() throws IOException
+    {
+        CMap cMap = new CMapParser().parsePredefined("Adobe-GB1-UCS2");
+
+        assertEquals("0", cMap.toUnicode(new byte[] { 0, 0x11 }),
+                "Adobe-GB1-UCS2 CID 0x11 -> \"0\"");
+    }
+
+    /**
+     * Test the parser against a valid, but poorly formatted CMap file.
+     * @throws IOException If something went wrong
+     */
+    @Test
+    void testParserWithPoorWhitespace() throws IOException
+    {
+        CMap cMap = new CMapParser().parse(new RandomAccessReadBufferedFile(
+                new File("src/test/resources/cmap", "CMapNoWhitespace")));
+
+        assertNotNull(cMap, "Failed to parse nasty CMap file");
+    }
+
+    @Test
+    void testParserWithMalformedbfrange1() throws IOException
+    {
+        CMap cMap = new CMapParser()
+                .parse(new RandomAccessReadBufferedFile(
+                        new File("src/test/resources/cmap", 
"CMapMalformedbfrange1")));
+
+        assertNotNull(cMap, "Failed to parse malformed CMap file");
+
+        byte[] bytes1 = { 0, 1 };
+        assertEquals("A", cMap.toUnicode(bytes1), "bytes 00 01 from bfrange 
<0001> <0009> <0041>");
+
+        byte[] bytes2 = { 1, 00 };
+        assertNull(cMap.toUnicode(bytes2));
+
+    }
+
+    @Test
+    void testParserWithMalformedbfrange2() throws IOException
+    {
+        CMap cMap = new CMapParser()
+                .parse(new RandomAccessReadBufferedFile(
+                        new File("src/test/resources/cmap", 
"CMapMalformedbfrange2")));
+
+        assertNotNull(cMap, "Failed to parse malformed CMap file");
+
+        assertEquals("0", cMap.toUnicode(new byte[] { 0, 1 }),
+                "bytes 00 01 from bfrange <0001> <0009> <0030>");
+
+        assertEquals("A", cMap.toUnicode(new byte[] { 2, 0x32 }),
+                "bytes 02 32 from bfrange <0232> <0432> <0041>");
+
+        // check border values for non strict mode
+        assertNotNull(cMap.toUnicode(new byte[] { 2, (byte) 0xF0 }));
+        assertNotNull(cMap.toUnicode(new byte[] { 2, (byte) 0xF1 }));
+
+        // use strict mode
+        cMap = new CMapParser(true)
+                .parse(new RandomAccessReadBufferedFile(
+                        new File("src/test/resources/cmap", 
"CMapMalformedbfrange2")));
+        // check border values for strict mode
+        assertNotNull(cMap.toUnicode(new byte[] { 2, (byte) 0xF0 }));
+        assertNull(cMap.toUnicode(new byte[] { 2, (byte) 0xF1 }));
+
+    }
+
+    @Test
+    void testPredefinedMap() throws IOException
+    {
+        CMap cMap = new CMapParser().parsePredefined("Adobe-Korea1-UCS2");
+        assertNotNull(cMap, "Failed to parse predefined CMap 
Adobe-Korea1-UCS2");
+
+        assertEquals("Adobe-Korea1-UCS2", cMap.getName(), "wrong CMap name");
+        assertEquals(0, cMap.getWMode(), "wrong WMode");
+        assertFalse(cMap.hasCIDMappings());
+        assertTrue(cMap.hasUnicodeMappings());
+
+        cMap = new CMapParser().parsePredefined("Identity-V");
+        assertNotNull(cMap, "Failed to parse predefined CMap Identity-V");
+    }
+
+    @Test
+    void testIdentitybfrange() throws IOException
+    {
+        // use strict mode
+        CMap cMap = new CMapParser(true)
+                .parse(new RandomAccessReadBufferedFile(
+                        new File("src/test/resources/cmap", 
"Identitybfrange")));
+        assertEquals("Adobe-Identity-UCS", cMap.getName(), "wrong CMap name");
+
+        byte[] bytes = { 0, 65 };
+        assertEquals(new String(bytes, StandardCharsets.UTF_16BE), 
cMap.toUnicode(bytes),
+                "Indentity 0x0048");
+        bytes = new byte[] { 0x30, 0x39 };
+        assertEquals(new String(bytes, StandardCharsets.UTF_16BE), 
cMap.toUnicode(bytes),
+                "Indentity 0x3039");
+        // check border values for strict mode
+        bytes = new byte[] { 0x30, (byte) 0xFF };
+        assertEquals(new String(bytes, StandardCharsets.UTF_16BE), 
cMap.toUnicode(bytes),
+                "Indentity 0x30FF");
+        // check border values for strict mode
+        bytes = new byte[] { 0x31, 0x00 };
+        assertEquals(new String(bytes, StandardCharsets.UTF_16BE), 
cMap.toUnicode(bytes),
+                "Indentity 0x3100");
+        bytes = new byte[] { (byte) 0xFF, (byte) 0xFF };
+        assertEquals(new String(bytes, StandardCharsets.UTF_16BE), 
cMap.toUnicode(bytes),
+                "Indentity 0xFFFF");
+
+    }
+
+    /**
+     * Test that parsing a CMap with empty byte arrays in bfrange does not 
throw
+     * ArrayIndexOutOfBoundsException. Empty hex strings produce zero-length 
byte
+     * arrays, causing increment() to be called with position -1.
+     */
+    @Test
+    void testBadIncrement() throws IOException
+    {
+        byte[] cmapData = "1 beginbfrange\n<> <> 
<2223>\nendbfrange".getBytes("US-ASCII");
+        CMapParser parser = new CMapParser();
+        CMap cmap = parser.parse(new RandomAccessReadBuffer(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+.
+     */
+    @Test
+    void testUseCmapOwnMappingsWin() throws IOException
+    {
+        CMap parent = new CMapParser().parsePredefined("ETen-B5-H");
+        assertEquals(13681, parent.toCID(0x41, 1), "ETen-B5-H maps 0x41 to the 
full width form");
+
+        CMap cMap = new CMapParser().parsePredefined("ETenms-B5-H");
+        assertEquals(34, cMap.toCID(0x41, 1), "ETenms-B5-H overrides 0x41 to 
the proportional form");
+        assertEquals(1, cMap.toCID(0x20, 1), "ETenms-B5-H overrides 0x20 to 
the proportional form");
+
+        // codes the CMap does not redefine still come from the one it uses
+        assertEquals(parent.toCID(new byte[] { (byte) 0xA1, 0x40 }),
+                cMap.toCID(new byte[] { (byte) 0xA1, 0x40 }),
+                "an inherited code is unaffected");
+
+        // the byte[] overload repeats the lookup order of the int one, so 
check the override there too
+        assertEquals(13681, parent.toCID(new byte[] { 0x41 }));
+        assertEquals(34, cMap.toCID(new byte[] { 0x41 }),
+                "the byte[] overload has to prefer the CMap's own mapping as 
well");
+
+        // 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(34, new 
CMapParser().parsePredefined("UniJIS-UCS2-H").toCID(0x41, 2),
+                "UniJIS-UCS2-H maps 0x0041 to the proportional form");
+        assertEquals(264, halfWidth.toCID(0x41, 2),
+                "UniJIS-UCS2-HW-H overrides 0x0041 to the half width form");
+    }
+
+    /**
+     * 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.
+     */
+    @Test
+    void testUseCmapChainKeepsNearestMapping() throws IOException
+    {
+        CMap cMap = new CMapParser().parsePredefined("ETenms-B5-V");
+
+        assertEquals(1, cMap.getWMode(), "ETenms-B5-V is vertical");
+        assertEquals(34, cMap.toCID(0x41, 1),
+                "ETenms-B5-V inherits the proportional override from 
ETenms-B5-H, not ETen-B5-H");
+    }
+
+    /**
+     * 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.
+     */
+    @Test
+    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, 2));
+        assertEquals(111, horizontal.toCID(0xA14C, 2));
+        assertEquals(121, horizontal.toCID(0xA156, 2));
+
+        // ETenms-B5-V's own cidchars replace them with the vertical forms
+        assertEquals(13646, vertical.toCID(0xA14B, 2), "own cidchar has to 
beat the inherited range");
+        assertEquals(109, vertical.toCID(0xA14C, 2), "own cidchar has to beat 
the inherited range");
+        assertEquals(312, vertical.toCID(0xA156, 2), "own cidchar has to beat 
the inherited range");
+
+        // and its own cidranges likewise, two usecmap levels down
+        assertEquals(128, horizontal.toCID(0xA15D, 2));
+        assertEquals(130, vertical.toCID(0xA15D, 2), "own cidrange has to beat 
the inherited range");
+    }
+
+    /**
+     * 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.
+     */
+    @Test
+    void testUseCmapOnlyInheritedMappings() throws IOException
+    {
+        CMap cMap = new CMapParser().parsePredefined("Identity-V");
+
+        assertEquals(1, cMap.getWMode(), "Identity-V is vertical");
+        assertTrue(cMap.hasCIDMappings(), "Identity-V has cid mappings, all of 
them inherited");
+
+        assertEquals(65, cMap.toCID(new byte[] { 0, 65 }), "Identity-V CID 
65");
+        assertEquals(12345, cMap.toCID(new byte[] { 0x30, 0x39 }), "Identity-V 
CID 12345");
+        assertEquals(0xFFFF, cMap.toCID(new byte[] { (byte) 0xFF, (byte) 0xFF 
}),
+                "Identity-V CID 0xFFFF");
+        assertEquals(12345, cMap.toCID(0x3039, 2), "Identity-V CID 12345");
+    }
+
+    /**
+     * 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.
+     */
+    @Test
+    void testUseCmapDoesNotShareMappingsWithTheUsedCMap() throws IOException
+    {
+        CMap used = new CMap();
+        used.addCIDMapping(new byte[] { 0x41 }, 100);
+        used.addCIDRange(new byte[] { 0x50 }, new byte[] { 0x5F }, 200);
+
+        CMap cMap = new CMap();
+        cMap.useCmap(used);
+        assertEquals(100, cMap.toCID(0x41, 1), "the mapping is inherited");
+        assertEquals(205, cMap.toCID(0x55, 1), "the range is inherited");
+
+        cMap.addCIDMapping(new byte[] { 0x41 }, 300);
+        cMap.addCIDRange(new byte[] { 0x50 }, new byte[] { 0x5F }, 400);
+
+        assertEquals(300, cMap.toCID(0x41, 1), "the CMap's own mapping wins");
+        assertEquals(405, cMap.toCID(0x55, 1), "the CMap's own range wins");
+        assertEquals(100, used.toCID(0x41, 1), "the used CMap must not have 
been modified");
+        assertEquals(205, used.toCID(0x55, 1), "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.
+     */
+    @Test
+    void testUseCmapOwnRangeBeatsInheritedChar() throws IOException
+    {
+        CMap used = new CMap();
+        used.addCIDMapping(new byte[] { 0x41 }, 100);
+
+        CMap cMap = new CMap();
+        cMap.useCmap(used);
+        cMap.addCIDRange(new byte[] { 0x40 }, new byte[] { 0x4F }, 200);
+
+        assertEquals(201, cMap.toCID(0x41, 1), "the CMap's own range has to 
beat the inherited char");
+        assertEquals(200, cMap.toCID(0x40, 1), "a code the used CMap says 
nothing about");
+        assertEquals(100, used.toCID(0x41, 1), "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.
+     */
+    @Test
+    void testUseCmapNearerCMapWins()
+    {
+        CMap far = new CMap();
+        far.addCIDMapping(new byte[] { 0x41 }, 100);
+
+        CMap near = new CMap();
+        near.useCmap(far);
+        near.addCIDRange(new byte[] { 0x40 }, new byte[] { 0x4F }, 200);
+
+        CMap cMap = new CMap();
+        cMap.useCmap(near);
+
+        assertEquals(201, cMap.toCID(0x41, 1), "the nearer CMap's range has to 
beat the farther "
+                + "CMap's cidchar");
+        assertEquals(100, far.toCID(0x41, 1), "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.
+     */
+    @Test
+    void testUseCmapNestedToFiveLevels()
+    {
+        CMap cMap = new CMap();
+        cMap.addCIDMapping(new byte[] { 0x01 }, 10);
+        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(new byte[] { (byte) (level - 1) }, 10 * 
level);
+            nested.addCIDMapping(new byte[] { (byte) level }, 10 * 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, 1));
+        assertEquals(30, cMap.toCID(0x02, 1));
+        assertEquals(40, cMap.toCID(0x03, 1));
+        assertEquals(50, cMap.toCID(0x04, 1));
+        assertEquals(50, cMap.toCID(0x05, 1));
+    }
+
+    /**
+     * 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.
+     */
+    @Test
+    void testUseCmapPassesThroughEmptyLevels()
+    {
+        CMap cMap = new CMap();
+        cMap.addCIDMapping(new byte[] { 0x41 }, 100);
+        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, 1));
+        assertEquals(0, cMap.toCID(0x42, 1), "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.
+     */
+    @Test
+    void testUseCmapSeveralUsedCMaps()
+    {
+        CMap first = new CMap();
+        first.addCIDMapping(new byte[] { 0x41 }, 100);
+        first.addCIDMapping(new byte[] { 0x42 }, 101);
+
+        CMap second = new CMap();
+        second.addCIDMapping(new byte[] { 0x42 }, 200);
+        second.addCIDMapping(new byte[] { 0x43 }, 201);
+
+        CMap cMap = new CMap();
+        cMap.useCmap(first);
+        cMap.useCmap(second);
+        cMap.addCIDMapping(new byte[] { 0x41 }, 300);
+
+        assertEquals(300, cMap.toCID(0x41, 1), "the CMap's own mapping beats 
both");
+        assertEquals(101, cMap.toCID(0x42, 1), "a code both used CMaps map 
comes from the first");
+        assertEquals(201, cMap.toCID(0x43, 1), "a code only the second maps 
still resolves");
+        assertEquals(0, cMap.toCID(0x44, 1), "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.
+     */
+    @Test
+    void testUseCmapOwnMappingToCidZeroIsNotAFallthrough()
+    {
+        CMap used = new CMap();
+        used.addCIDRange(new byte[] { 0x00 }, new byte[] { (byte) 0xFF }, 500);
+
+        CMap cMap = new CMap();
+        cMap.useCmap(used);
+        cMap.addCIDRange(new byte[] { 0x41 }, new byte[] { 0x41 }, 0);
+
+        assertEquals(0, cMap.toCID(0x41, 1), "the CMap's own .notdef has to 
win");
+        assertEquals(0, cMap.toCID(new byte[] { 0x41 }), "the byte[] overload 
as well");
+        assertEquals(566, cMap.toCID(0x42, 1), "a code it doesn't redefine 
still comes from the parent");
+        assertEquals(565, used.toCID(0x41, 1), "the used CMap answers for 
itself unchanged");
+    }
+
+    /**
+     * The length guessing overload probes the code lengths shortest first. A 
code mapped to CID 0 at
+     * the shortest length is mapped, so the probing stops there rather than 
running on to a longer
+     * length that happens to map the same value to something else.
+     */
+    @Test
+    void testToCidZeroAtShortestLengthStopsTheLengthProbing()
+    {
+        CMap cMap = new CMap();
+        cMap.addCIDMapping(new byte[] { 0x41 }, 0);
+        cMap.addCIDMapping(new byte[] { 0x00, 0x41 }, 700);
+
+        assertEquals(0, cMap.toCID(0x41, 1), "the one byte code maps to 
.notdef");
+        assertEquals(700, cMap.toCID(0x41, 2), "the two byte code maps to 
700");
+        assertEquals(0, cMap.toCID(0x41), "the shortest length maps the code, 
so that is the answer");
+    }
+}

Reply via email to