From 8f2357709f5e09393b055af5db53178738dd51eb Mon Sep 17 00:00:00 2001 From: Patrick Corless Date: Fri, 4 Sep 2026 21:23:30 -0600 Subject: [PATCH 1/3] PDFBOX-6251: Let a CMap's own cid mappings win over the ones it uses A CMap that starts with usecmap may redefine any code it inherits, but useCmap merged the used CMap's cidchar/cidrange mappings into the same collections as the importing CMap's own. usecmap is read from the header, so the used CMap's mappings landed there first, and since toCID scans the range list first-match, an inherited range beat the mapping the CMap declared for the same code. ETenms-B5-H exists only to do such an override: it uses ETen-B5-H and remaps 0x20-0x7E to the proportional latin CIDs 1-95, yet toCID(0x41, 1) answered 13681, the fullwidth form, instead of 34. 31 of the 92 bundled predefined CMaps declare mappings on top of a usecmap and all 31 were affected; 29 of them are the -V variants, whose own mappings select the vertical glyph forms. Keep the used CMap as a reference instead of copying its cid mappings, and ask it only for codes this CMap does not map itself. Precedence then follows the usecmap chain nearest-first at every level, so it also holds for nesting deeper than two levels and for a CMap carrying more than one usecmap. hasCIDMappings has to take the used CMaps into account, Identity-V declaring no cid mappings of its own. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014khhepY6XDZVnwu9by9rMo --- .../java/org/apache/fontbox/cmap/CMap.java | 80 +++--- .../apache/fontbox/cmap/TestCMapParser.java | 243 ++++++++++++++++++ 2 files changed, 272 insertions(+), 51 deletions(-) diff --git a/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java b/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java index df50a55b1ea..a9b7c5cbd98 100644 --- a/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java +++ b/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java @@ -65,6 +65,9 @@ public class CMap private final Map> codeToCid = new HashMap<>(); private final List codeToCidRanges = new ArrayList<>(); + // the CMaps this one inherits from through the usecmap operator, see useCmap + private final List parentCMaps = new ArrayList<>(); + // inverted map private final Map 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 @@ private static int toInt(byte[] data, int dataLen) */ public int toCID(byte[] code) { - if (!hasCIDMappings() || code.length < minCidLength || code.length > maxCidLength) - { - return 0; - } - Integer cid = null; - Map 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,10 +258,6 @@ public int toCID(byte[] code) */ public int toCID(int code) { - if (!hasCIDMappings()) - { - return 0; - } int cid = 0; int length = minCidLength; while (cid == 0 && (length <= maxCidLength)) @@ -286,51 +276,45 @@ public int toCID(int code) */ public int toCID(int code, int length) { - if (!hasCIDMappings() || length < minCidLength || length > maxCidLength) + if (length < minCidLength || length > maxCidLength) { return 0; } - Integer cid = null; Map codeToCidMap = codeToCid.get(length); - if (codeToCidMap != null) + Integer cid = codeToCidMap != null ? codeToCidMap.get(code) : null; + if (cid != null) { - cid = codeToCidMap.get(code); + return cid; } - return cid != null ? cid : toCIDFromRanges(code, length); - } - - /** - * Returns the CID for the given character code. - * - * @param code character code - * @return CID - */ - - private int toCIDFromRanges(int code, int length) - { - for (CIDRange range : codeToCidRanges) + int cidFromRange = toCIDFromRanges(code, length); + if (cidFromRange != 0) { - int ch = range.map(code, length); - if (ch != -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.toCID(code, length); + if (parentCid != 0) { - return ch; + return parentCid; } } return 0; } /** - * 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 0 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; @@ -476,15 +460,9 @@ void useCmap(CMap cmap) } unicodeToByteCodes.put(v, bar); }); - cmap.codeToCid.forEach((key, value) -> - { - Map 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); diff --git a/fontbox/src/test/java/org/apache/fontbox/cmap/TestCMapParser.java b/fontbox/src/test/java/org/apache/fontbox/cmap/TestCMapParser.java index 9d860dcbbdc..7d7ea571f13 100644 --- a/fontbox/src/test/java/org/apache/fontbox/cmap/TestCMapParser.java +++ b/fontbox/src/test/java/org/apache/fontbox/cmap/TestCMapParser.java @@ -261,4 +261,247 @@ void testBadIncrement() throws IOException 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"); + } } From 691ae7641389c7777607a7c5b75d0486e70df187 Mon Sep 17 00:00:00 2001 From: Patrick Corless Date: Fri, 4 Sep 2026 21:38:25 -0600 Subject: [PATCH 2/3] PDFBOX-6251 rework parent cmap lookup --- .../java/org/apache/fontbox/cmap/CMap.java | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java b/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java index a9b7c5cbd98..f0cb2c6a976 100644 --- a/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java +++ b/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java @@ -83,7 +83,7 @@ public class CMap /** * This will tell if this cmap has any CID mappings. - * + * * @return true If there are any CID mappings, false otherwise. */ public boolean hasCIDMappings() @@ -246,7 +246,7 @@ public int toCID(byte[] code) /** * 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.
* Example:
@@ -304,7 +304,7 @@ public int toCID(int code, int length) } /** - * Returns the CID the CID ranges of this CMap map the given character code to. + * Returns the CID, the CID ranges of this CMap map the given character code to. * * @param code character code * @param length the origin byte length of the code @@ -426,11 +426,11 @@ void addCodespaceRange( CodespaceRange range ) maxCodeLength = Math.max(maxCodeLength, range.getCodeLength()); minCodeLength = Math.min(minCodeLength, range.getCodeLength()); } - + /** * Implementation of the usecmap operator. This will * copy all of the mappings from one cmap to another. - * + * * @param cmap The cmap to load mappings from. */ void useCmap(CMap cmap) @@ -443,13 +443,13 @@ void useCmap(CMap cmap) cmap.charToUnicodeTwoBytes.forEach((k, v) -> unicodeToByteCodes.put(v, new byte[]{(byte) ((k >>> 8) & 0xFF), (byte) (k & 0xFF)}) ); - cmap.charToUnicodeMoreBytes.forEach((k, v) -> + cmap.charToUnicodeMoreBytes.forEach((k, v) -> { byte[] bar; if (k <= 0xFFFFFF) { // 3 bytes - bar = new byte[]{(byte) ((k >>> 16) & 0xFF), (byte) ((k >>> 8) & 0xFF), + bar = new byte[]{(byte) ((k >>> 16) & 0xFF), (byte) ((k >>> 8) & 0xFF), (byte) (k & 0xFF)}; } else @@ -473,147 +473,147 @@ void useCmap(CMap cmap) * Returns the WMode of a CMap. * * 0 represents a horizontal and 1 represents a vertical orientation. - * + * * @return the wmode */ - public int getWMode() + public int getWMode() { return wmode; } /** * Sets the WMode of a CMap. - * + * * @param newWMode the new WMode. */ - public void setWMode(int newWMode) + public void setWMode(int newWMode) { wmode = newWMode; } /** * Returns the name of the CMap. - * + * * @return the CMap name. */ - public String getName() + public String getName() { return cmapName; } /** * Sets the name of the CMap. - * + * * @param name the CMap name. */ - public void setName(String name) + public void setName(String name) { cmapName = name; } /** * Returns the version of the CMap. - * + * * @return the CMap version. */ - public String getVersion() + public String getVersion() { return cmapVersion; } /** * Sets the version of the CMap. - * + * * @param version the CMap version. */ - public void setVersion(String version) + public void setVersion(String version) { cmapVersion = version; } /** * Returns the type of the CMap. - * + * * @return the CMap type. */ - public int getType() + public int getType() { return cmapType; } /** * Sets the type of the CMap. - * + * * @param type the CMap type. */ - public void setType(int type) + public void setType(int type) { cmapType = type; } /** * Returns the registry of the CIDSystemInfo. - * + * * @return the registry. */ - public String getRegistry() + public String getRegistry() { return registry; } /** * Sets the registry of the CIDSystemInfo. - * + * * @param newRegistry the registry. */ - public void setRegistry(String newRegistry) + public void setRegistry(String newRegistry) { registry = newRegistry; } /** * Returns the ordering of the CIDSystemInfo. - * + * * @return the ordering. */ - public String getOrdering() + public String getOrdering() { return ordering; } /** * Sets the ordering of the CIDSystemInfo. - * + * * @param newOrdering the ordering. */ - public void setOrdering(String newOrdering) + public void setOrdering(String newOrdering) { ordering = newOrdering; } /** * Returns the supplement of the CIDSystemInfo. - * + * * @return the supplement. */ - public int getSupplement() + public int getSupplement() { return supplement; } /** * Sets the supplement of the CIDSystemInfo. - * + * * @param newSupplement the supplement. */ - public void setSupplement(int newSupplement) + public void setSupplement(int newSupplement) { supplement = newSupplement; } - - /** + + /** * Returns the mapping for the space character. - * + * * @return the mapped code for the space character */ public int getSpaceMapping() From cd53a45cb2e6106134679267afc73658a3e1b09f Mon Sep 17 00:00:00 2001 From: Patrick Corless Date: Fri, 4 Sep 2026 22:19:28 -0600 Subject: [PATCH 3/3] PDFBOX-6251 rework for -1 or 0 base cid lookups. --- .../java/org/apache/fontbox/cmap/CMap.java | 42 +++++++++++++------ .../apache/fontbox/cmap/TestCMapParser.java | 37 ++++++++++++++++ 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java b/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java index f0cb2c6a976..f4f90d89d64 100644 --- a/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java +++ b/fontbox/src/main/java/org/apache/fontbox/cmap/CMap.java @@ -258,13 +258,15 @@ public int toCID(byte[] code) */ public int toCID(int code) { - int cid = 0; - int length = minCidLength; - while (cid == 0 && (length <= maxCidLength)) + for (int length = minCidLength; length <= maxCidLength; length++) { - cid = toCID(code, length++); + int cid = findCID(code, length); + if (cid != -1) + { + return cid; + } } - return cid; + return 0; } /** @@ -275,10 +277,26 @@ public int toCID(int code) * @return CID */ public int toCID(int code, int length) + { + int cid = findCID(code, length); + return cid != -1 ? cid : 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 + * @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 findCID(int code, int length) { if (length < minCidLength || length > maxCidLength) { - return 0; + return -1; } Map codeToCidMap = codeToCid.get(length); Integer cid = codeToCidMap != null ? codeToCidMap.get(code) : null; @@ -287,20 +305,20 @@ public int toCID(int code, int length) return cid; } int cidFromRange = toCIDFromRanges(code, length); - if (cidFromRange != 0) + 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.toCID(code, length); - if (parentCid != 0) + int parentCid = parentCMap.findCID(code, length); + if (parentCid != -1) { return parentCid; } } - return 0; + return -1; } /** @@ -308,7 +326,7 @@ public int toCID(int code, int length) * * @param code character code * @param length the origin byte length of the code - * @return CID, or 0 if no range covers the code + * @return CID, or -1 if no range covers the code */ private int toCIDFromRanges(int code, int length) { @@ -320,7 +338,7 @@ private int toCIDFromRanges(int code, int length) return ch; } } - return 0; + return -1; } /** diff --git a/fontbox/src/test/java/org/apache/fontbox/cmap/TestCMapParser.java b/fontbox/src/test/java/org/apache/fontbox/cmap/TestCMapParser.java index 7d7ea571f13..418998f229a 100644 --- a/fontbox/src/test/java/org/apache/fontbox/cmap/TestCMapParser.java +++ b/fontbox/src/test/java/org/apache/fontbox/cmap/TestCMapParser.java @@ -504,4 +504,41 @@ void testUseCmapSeveralUsedCMaps() 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"); + } }