]> source.dussan.org Git - xmlgraphics-fop.git/commitdiff
FOP-2252: OpenType CFF - Remove need for patch to fontbox
authorRobert Meyer <rmeyer@apache.org>
Mon, 10 Feb 2014 16:21:03 +0000 (16:21 +0000)
committerRobert Meyer <rmeyer@apache.org>
Mon, 10 Feb 2014 16:21:03 +0000 (16:21 +0000)
git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@1566674 13f79535-47bb-0310-9956-ffa450edef68

lib/fontbox-1.8.4-patched.jar
src/java/org/apache/fop/fonts/truetype/OTFFile.java
src/java/org/apache/fop/fonts/truetype/OTFSubSetFile.java
test/java/org/apache/fop/fonts/truetype/OTFSubSetFileTestCase.java

index 489acb8ac7938f11de177525064878dec3adbd17..f608b44094fa91ce62d302e1a1e13ca7a6409a87 100644 (file)
Binary files a/lib/fontbox-1.8.4-patched.jar and b/lib/fontbox-1.8.4-patched.jar differ
index 3976b5994fea50b3173363eab0ec8eb5b5ae8f39..ab9654beba760f28165338226d94aa4c73534144 100644 (file)
 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<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;
index 4d0cce67abc0330dccec14a1756974712448efbd..9cb3458c8b8eac39bf7bcd6f07f022f1be3a618c 100644 (file)
@@ -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;
+    }
 }
index c555ebc20251ebc758358321c360deebfb3cc0e9..8c92b5f63f8ab141f127474cb3c44d992215f311 100644 (file)
@@ -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<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