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..f4f90d89d64 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<>(); @@ -80,12 +83,13 @@ 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() { - return !codeToCid.isEmpty() || !codeToCidRanges.isEmpty(); + return !codeToCid.isEmpty() || !codeToCidRanges.isEmpty() + || parentCMaps.stream().anyMatch(CMap::hasCIDMappings); } /** @@ -237,22 +241,12 @@ 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); } /** * 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:
@@ -264,17 +258,15 @@ public int toCID(byte[] code) */ 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 int toCID(int code) */ public int toCID(int code, int length) { - if (!hasCIDMappings() || length < minCidLength || length > maxCidLength) - { - return 0; - } - Integer cid = null; - Map 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 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 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 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; } /** @@ -442,11 +444,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) @@ -459,13 +461,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 @@ -476,15 +478,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); @@ -495,147 +491,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() 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..418998f229a 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,284 @@ 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"); + } + + /** + * 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"); + } }