From: Robert Meyer Date: Mon, 10 Feb 2014 16:21:03 +0000 (+0000) Subject: FOP-2252: OpenType CFF - Remove need for patch to fontbox X-Git-Tag: fop-2_0~134 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=785ace252f252ff211330e93635631931d68b890;p=xmlgraphics-fop.git FOP-2252: OpenType CFF - Remove need for patch to fontbox git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@1566674 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/lib/fontbox-1.8.4-patched.jar b/lib/fontbox-1.8.4-patched.jar index 489acb8ac..f608b4409 100644 Binary files a/lib/fontbox-1.8.4-patched.jar and b/lib/fontbox-1.8.4-patched.jar differ diff --git a/src/java/org/apache/fop/fonts/truetype/OTFFile.java b/src/java/org/apache/fop/fonts/truetype/OTFFile.java index 3976b5994..ab9654beb 100644 --- a/src/java/org/apache/fop/fonts/truetype/OTFFile.java +++ b/src/java/org/apache/fop/fonts/truetype/OTFFile.java @@ -20,11 +20,17 @@ package org.apache.fop.fonts.truetype; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; import org.apache.fontbox.cff.CFFDataInput; import org.apache.fontbox.cff.CFFFont; -import org.apache.fontbox.cff.CFFFont.Mapping; import org.apache.fontbox.cff.CFFParser; +import org.apache.fontbox.cff.charset.CFFCharset; public class OTFFile extends OpenFont { @@ -45,17 +51,77 @@ public class OTFFile extends OpenFont { @Override protected void updateBBoxAndOffset() throws IOException { + List gidMappings = getGIDMappings(fileFont); + Map sidNames = constructNameMap(gidMappings); UnicodeMapping[] mappings = unicodeMappings.toArray(new UnicodeMapping[0]); for (int i = 0; i < mappings.length; i++) { int glyphIdx = mappings[i].getGlyphIndex(); - Mapping m = fileFont.getGIDMappings().get(glyphIdx); - int[] bbox = fileFont.getBoundingBox(m.getSID()); - String name = fileFont.getNameOfCharFromCode(m.getSID()); - mtxTab[glyphIdx].setBoundingBox(bbox); + Mapping m = gidMappings.get(glyphIdx); + String name = sidNames.get(m.getSID()); mtxTab[glyphIdx].setName(name); } } + private List getGIDMappings(CFFFont font) { + List gidMappings = new ArrayList(); + Mapping notdef = new Mapping(); + gidMappings.add(notdef); + for (CFFCharset.Entry entry : font.getCharset().getEntries()) { + String name = entry.getName(); + byte[] bytes = font.getCharStringsDict().get(name); + if (bytes == null) { + continue; + } + Mapping mapping = new Mapping(); + mapping.setSID(entry.getSID()); + mapping.setName(name); + mapping.setBytes(bytes); + gidMappings.add(mapping); + } + return gidMappings; + } + + private Map constructNameMap(Collection mappings) { + Map sidNames = new HashMap(); + Iterator it = mappings.iterator(); + while (it.hasNext()) { + Mapping mapping = it.next(); + sidNames.put(mapping.getSID(), mapping.getName()); + } + return sidNames; + } + + private static class Mapping { + private int sid; + private String name; + private byte[] bytes; + + public void setSID(int sid) { + this.sid = sid; + } + + public int getSID() { + return sid; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setBytes(byte[] bytes) { + this.bytes = bytes; + } + + public byte[] getBytes() { + return bytes; + } + } + + @Override protected void initializeFont(FontFileReader in) throws IOException { fontFile = in; diff --git a/src/java/org/apache/fop/fonts/truetype/OTFSubSetFile.java b/src/java/org/apache/fop/fonts/truetype/OTFSubSetFile.java index 4d0cce67a..9cb3458c8 100644 --- a/src/java/org/apache/fop/fonts/truetype/OTFSubSetFile.java +++ b/src/java/org/apache/fop/fonts/truetype/OTFSubSetFile.java @@ -861,7 +861,6 @@ public class OTFSubSetFile extends OTFFile { return hdrTotal + total; } - private BytesNumber readNumber(int b0, byte[] input, int curPos) throws IOException { if (b0 == 28) { int b1 = input[curPos + 1] & 0xff; @@ -887,7 +886,7 @@ public class OTFSubSetFile extends OTFFile { /** * A class used to store the last number operand and also it's size in bytes */ - private static final class BytesNumber { + static class BytesNumber { private int number; private int numBytes; @@ -908,6 +907,26 @@ public class OTFSubSetFile extends OTFFile { this.number = -1; this.numBytes = -1; } + + public String toString() { + return Integer.toString(number); + } + + @Override + public boolean equals(Object entry) { + assert entry instanceof BytesNumber; + BytesNumber bnEntry = (BytesNumber)entry; + return this.number == bnEntry.getNumber() + && this.numBytes == bnEntry.getNumBytes(); + } + + @Override + public int hashCode() { + int hash = 1; + hash = hash * 17 + number; + hash = hash * 31 + numBytes; + return hash; + } } private void writeCharsetTable(boolean cidFont) throws IOException { @@ -1094,4 +1113,12 @@ public class OTFSubSetFile extends OTFFile { System.arraycopy(output, 0, ret, 0, realSize); return ret; } + + /** + * Returns the parsed CFF data for the original font. + * @return The CFFDataReader contaiing the parsed data + */ + public CFFDataReader getCFFReader() { + return cffReader; + } } diff --git a/test/java/org/apache/fop/fonts/truetype/OTFSubSetFileTestCase.java b/test/java/org/apache/fop/fonts/truetype/OTFSubSetFileTestCase.java index c555ebc20..8c92b5f63 100644 --- a/test/java/org/apache/fop/fonts/truetype/OTFSubSetFileTestCase.java +++ b/test/java/org/apache/fop/fonts/truetype/OTFSubSetFileTestCase.java @@ -20,6 +20,8 @@ package org.apache.fop.fonts.truetype; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -27,18 +29,15 @@ import java.util.Map; import org.junit.Before; import org.junit.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import org.apache.fontbox.cff.CFFDataInput; import org.apache.fontbox.cff.CFFFont; -import org.apache.fontbox.cff.CFFParser; -import org.apache.fontbox.cff.IndexData; -import org.apache.fontbox.cff.Type2CharStringParser; import org.apache.fop.fonts.cff.CFFDataReader; import org.apache.fop.fonts.cff.CFFDataReader.CFFIndexData; import org.apache.fop.fonts.cff.CFFDataReader.DICTEntry; +import org.apache.fop.fonts.truetype.OTFSubSetFile.BytesNumber; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; public class OTFSubSetFileTestCase extends OTFFileTestCase { @@ -47,10 +46,6 @@ public class OTFSubSetFileTestCase extends OTFFileTestCase { private byte[] sourceSansData; CFFDataReader cffReaderHeitiStd; - public OTFSubSetFileTestCase() throws IOException { - super(); - } - /** * Initialises the test by creating the font subset. A CFFDataReader is * also created based on the subset data for use in the tests. @@ -80,9 +75,14 @@ public class OTFSubSetFileTestCase extends OTFFileTestCase { public void testCharStringIndex() throws IOException { assertEquals(256, cffReaderSourceSans.getCharStringIndex().getNumObjects()); assertTrue(checkCorrectOffsets(cffReaderSourceSans.getCharStringIndex())); - validateCharStrings(cffReaderSourceSans); + validateCharStrings(cffReaderSourceSans, sourceSansSubset.getCFFReader()); } + /** + * Checks the index data to ensure that the offsets are valid + * @param indexData The index data to check + * @return Returns true if it is found to be valid + */ private boolean checkCorrectOffsets(CFFIndexData indexData) { int last = 0; for (int i = 0; i < indexData.getOffsets().length; i++) { @@ -94,34 +94,267 @@ public class OTFSubSetFileTestCase extends OTFFileTestCase { return true; } - private void validateCharStrings(CFFDataReader cffReader) throws IOException { + /** + * Validates the subset font CharString data by comparing it with the original. + * @param subsetCFF The subset CFFDataReader containing the CharString data + * @param origCFF The original CFFDataReader containing the CharString data + * @throws IOException + */ + private void validateCharStrings(CFFDataReader subsetCFF, CFFDataReader origCFF) + throws IOException { CFFFont sourceSansOriginal = sourceSansProBold.fileFont; + CFFIndexData charStrings = subsetCFF.getCharStringIndex(); Map origCharStringData = sourceSansOriginal.getCharStringsDict(); - IndexData origGlobalIndex = sourceSansOriginal.getGlobalSubrIndex(); - IndexData origLocalIndex = sourceSansOriginal.getLocalSubrIndex(); + for (int i = 0; i < charStrings.getNumObjects(); i++) { + byte[] origCharData = origCharStringData.get(origCharStringData.keySet().toArray( + new String[0])[i]); + byte[] charData = charStrings.getValue(i); + List origOperands = getFullCharString(origCharData, origCFF); + List subsetOperands = getFullCharString(charData, subsetCFF); + for (int j = 0;j < origOperands.size();j++) { + assertTrue(origOperands.get(j).equals(subsetOperands.get(j))); + } + } + } + + /** + * Recursively reads and constructs the full CharString for comparison + * @param data The original byte data of the CharString + * @param cffData The CFFDataReader containing the subroutine indexes + * @return Returns a list of parsed operands and operators + * @throws IOException + */ + private List getFullCharString(byte[] data, CFFDataReader cffData) throws IOException { + CFFIndexData localIndexSubr = cffData.getLocalIndexSubr(); + CFFIndexData globalIndexSubr = cffData.getGlobalIndexSubr(); + boolean hasLocalSubroutines = localIndexSubr != null && localIndexSubr.getNumObjects() > 0; + boolean hasGlobalSubroutines = globalIndexSubr != null && globalIndexSubr.getNumObjects() > 0; + ArrayList operands = new ArrayList(); + for (int dataPos = 0; dataPos < data.length; dataPos++) { + int b0 = data[dataPos] & 0xff; + if (b0 == 10 && hasLocalSubroutines) { + int subrNumber = getSubrNumber(localIndexSubr.getNumObjects(), + operands.get(operands.size() - 1).getNumber()); + byte[] subr = localIndexSubr.getValue(subrNumber); + List subrOperands = getFullCharString(subr, cffData); + operands = mergeOperands(operands, subrOperands); + } else if (b0 == 29 && hasGlobalSubroutines) { + int subrNumber = getSubrNumber(globalIndexSubr.getNumObjects(), + operands.get(operands.size() - 1).getNumber()); + byte[] subr = globalIndexSubr.getValue(subrNumber); + ArrayList subrOperands = (ArrayList)getFullCharString(subr, cffData); + operands = mergeOperands(operands, subrOperands); + } else if ((b0 >= 0 && b0 <= 27) || (b0 >= 29 && b0 <= 31)) { + int size = 1; + int b1 = -1; + if (b0 == 12) { + b1 = data[dataPos++] & 0xff; + size = 2; + } + if (b0 == 19 || b0 == 20) { + dataPos += 1; + size = 2; + } + operands.add(new Operator(b0, size, getOperatorName(b0, b1))); + } else if (b0 == 28 || (b0 >= 32 && b0 <= 255)) { + operands.add(readNumber(b0, data, dataPos)); + dataPos += operands.get(operands.size() - 1).getNumBytes() - 1; + } + } + return operands; + } + + /** + * Merges two lists of operands. This is typically used to merge the CharString + * data with that of a parsed and referenced subroutine. + * @param charString The parsed CharString data so far + * @param subroutine The parsed elements from a subroutine + * @return Returns a merged list of both CharString and subroutine elements. + */ + private ArrayList mergeOperands(List charString, + List subroutine) { + BytesNumber[] charStringOperands = charString.toArray(new BytesNumber[0]); + BytesNumber[] subroutineOperands = subroutine.toArray(new BytesNumber[0]); + BytesNumber[] mergeData = new BytesNumber[charStringOperands.length - 1 + + subroutineOperands.length - 1]; + System.arraycopy(charStringOperands, 0, mergeData, 0, charStringOperands.length - 1); + System.arraycopy(subroutineOperands, 0, mergeData, charStringOperands.length - 1, + subroutineOperands.length - 1); + ArrayList hello = new ArrayList(); + hello.addAll(Arrays.asList(mergeData)); + return hello; + } - CFFDataInput globalSubrs = new CFFDataInput(cffReader.getGlobalIndexSubr().getByteData()); - CFFDataInput localSubrs = new CFFDataInput(cffReader.getLocalIndexSubr().getByteData()); + /** + * Parses a number from one or more bytes + * @param b0 The first byte to identify how to interpret the number + * @param input The original byte data containing the number + * @param curPos The current position of the number + * @return Returns the number + * @throws IOException + */ + private BytesNumber readNumber(int b0, byte[] input, int curPos) throws IOException { + if (b0 == 28) { + int b1 = input[curPos + 1] & 0xff; + int b2 = input[curPos + 2] & 0xff; + return new BytesNumber(Integer.valueOf((short) (b1 << 8 | b2)), 3); + } else if (b0 >= 32 && b0 <= 246) { + return new BytesNumber(Integer.valueOf(b0 - 139), 1); + } else if (b0 >= 247 && b0 <= 250) { + int b1 = input[curPos + 1] & 0xff; + return new BytesNumber(Integer.valueOf((b0 - 247) * 256 + b1 + 108), 2); + } else if (b0 >= 251 && b0 <= 254) { + int b1 = input[curPos + 1] & 0xff; + return new BytesNumber(Integer.valueOf(-(b0 - 251) * 256 - b1 - 108), 2); + } else if (b0 == 255) { + int b1 = input[curPos + 1] & 0xff; + int b2 = input[curPos + 2] & 0xff; + return new BytesNumber(Integer.valueOf((short)(b1 << 8 | b2)), 5); + } else { + throw new IllegalArgumentException(); + } + } - IndexData globalIndexData = CFFParser.readIndexData(globalSubrs); - IndexData localIndexData = CFFParser.readIndexData(localSubrs); + /** + * Gets the subroutine number according to the number of subroutines + * and the provided operand. + * @param numSubroutines The number of subroutines used to calculate the + * subroutine reference. + * @param operand The operand for the subroutine + * @return Returns the calculated subroutine number + */ + private int getSubrNumber(int numSubroutines, int operand) { + int bias = getBias(numSubroutines); + return bias + operand; + } - CFFIndexData charStrings = cffReader.getCharStringIndex(); - for (int i = 0; i < charStrings.getNumObjects(); i++) { - byte[] charData = charStrings.getValue(i); - Type2CharStringParser parser = new Type2CharStringParser(); + /** + * Gets the bias give the number of subroutines. This is used in the + * calculation to determine a subroutine's number + * @param subrCount The number of subroutines for a given index + * @return Returns the bias value + */ + private int getBias(int subrCount) { + if (subrCount < 1240) { + return 107; + } else if (subrCount < 33900) { + return 1131; + } else { + return 32768; + } + } - byte[] origCharData = origCharStringData.get(origCharStringData.keySet().toArray( - new String[0])[i]); - List origSeq = parser.parse(origCharData, origGlobalIndex, origLocalIndex); + /** + * A class representing an operator from the CharString data + */ + private class Operator extends BytesNumber { + private String opName = ""; - List subsetSeq = parser.parse(charData, globalIndexData, localIndexData); + public Operator(int number, int numBytes, String opName) { + super(number, numBytes); + this.opName = opName; + } + public String toString() { + return String.format("[%s]", opName); + } + } - //Validates the subset glyph render routines against the originals - assertEquals(origSeq, subsetSeq); + /** + * Gets the identifying name for the given operator. This is primarily + * used for debugging purposes. See the Type 2 CharString Format specification + * document (Technical Note #5177) Appendix A (Command Codes). + * @param operator The operator code + * @param codeb The second byte of the operator + * @return Returns the operator name. + */ + private String getOperatorName(int operator, int operatorB) { + switch (operator) { + case 0: return "Reserved"; + case 1: return "hstem"; + case 2: return "Reserved"; + case 3: return "vstem"; + case 4: return "vmoveto"; + case 5: return "rlineto"; + case 6: return "hlineto"; + case 7: return "vlineto"; + case 8: return "rrcurveto"; + case 9: return "Reserved"; + case 10: return "callsubr"; + case 11: return "return"; + case 12: return getDoubleOpName(operatorB); + case 13: return "Reserved"; + case 14: return "enchar"; + case 15: + case 16: + case 17: return "Reserved"; + case 18: return "hstemhm"; + case 19: return "hintmask"; + case 20: return "cntrmask"; + case 21: return "rmoveto"; + case 22: return "hmoveto"; + case 23: return "vstemhm"; + case 24: return "rcurveline"; + case 25: return "rlinecurve"; + case 26: return "vvcurveto"; + case 27: return "hhcurveto"; + case 28: return "shortint"; + case 29: return "callgsubr"; + case 30: return "vhcurveto"; + case 31: return "hvcurveto"; + default: return "Unknown"; } } + /** + * Gets the name of a double byte operator code + * @param operator The second byte of the operator + * @return Returns the name + */ + private String getDoubleOpName(int operator) { + switch (operator) { + case 0: + case 1: + case 2: return "Reserved"; + case 3: return "and"; + case 4: return "or"; + case 5: return "not"; + case 6: + case 7: + case 8: return "Reserved"; + case 9: return "abs"; + case 10: return "add"; + case 11: return "sub"; + case 12: return "div"; + case 13: return "Reserved"; + case 14: return "neg"; + case 15: return "eq"; + case 16: + case 17: return "Reserved"; + case 18: return "drop"; + case 19: return "Reserved"; + case 20: return "put"; + case 21: return "get"; + case 22: return "ifelse"; + case 23: return "random"; + case 24: return "mul"; + case 25: return "Reserved"; + case 26: return "sqrt"; + case 27: return "dup"; + case 28: return "exch"; + case 29: return "index"; + case 30: return "roll"; + case 31: + case 32: + case 33: return "Reserved"; + case 34: return "hflex"; + case 35: return "flex"; + case 36: return "hflex1"; + case 37: return "flex1"; + case 38: return "Reserved"; + default: return "Unknown"; + } + } + /** * Validates the String index data and size * @throws IOException