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 {
@Override
protected void updateBBoxAndOffset() throws IOException {
+ List<Mapping> gidMappings = getGIDMappings(fileFont);
+ Map<Integer, String> 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<Mapping> getGIDMappings(CFFFont font) {
+ List<Mapping> gidMappings = new ArrayList<Mapping>();
+ 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<Integer, String> constructNameMap(Collection<Mapping> mappings) {
+ Map<Integer, String> sidNames = new HashMap<Integer, String>();
+ Iterator<Mapping> 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;
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;
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 {
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.
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++) {
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<String, byte[]> 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<BytesNumber> origOperands = getFullCharString(origCharData, origCFF);
+ List<BytesNumber> 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<BytesNumber> 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<BytesNumber> operands = new ArrayList<BytesNumber>();
+ 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<BytesNumber> 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<BytesNumber> subrOperands = (ArrayList<BytesNumber>)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<BytesNumber> mergeOperands(List<BytesNumber> charString,
+ List<BytesNumber> 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<BytesNumber> hello = new ArrayList<BytesNumber>();
+ 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<Object> origSeq = parser.parse(origCharData, origGlobalIndex, origLocalIndex);
+ /**
+ * A class representing an operator from the CharString data
+ */
+ private class Operator extends BytesNumber {
+ private String opName = "";
- List<Object> 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