1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 /* $Id$ */
19 
20 package org.apache.fop.fonts.truetype;
21 
22 import java.io.ByteArrayInputStream;
23 import java.io.DataInputStream;
24 import java.io.File;
25 import java.io.IOException;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.Collections;
29 import java.util.HashMap;
30 import java.util.LinkedHashMap;
31 import java.util.List;
32 import java.util.Map;
33 
34 import org.junit.Assert;
35 import org.junit.Before;
36 import org.junit.Test;
37 
38 import static org.junit.Assert.assertEquals;
39 import static org.junit.Assert.assertTrue;
40 import static org.mockito.Matchers.any;
41 import static org.mockito.Mockito.mock;
42 import static org.mockito.Mockito.when;
43 
44 import org.apache.fontbox.cff.CFFFont;
45 import org.apache.fontbox.cff.CFFParser;
46 import org.apache.fontbox.cff.CFFType1Font;
47 import org.apache.fontbox.cff.CharStringCommand;
48 import org.apache.fontbox.cff.Type2CharString;
49 
50 import org.apache.fop.fonts.MultiByteFont;
51 import org.apache.fop.fonts.cff.CFFDataReader;
52 import org.apache.fop.fonts.cff.CFFDataReader.CFFIndexData;
53 import org.apache.fop.fonts.cff.CFFDataReader.DICTEntry;
54 import org.apache.fop.fonts.truetype.OTFSubSetFile.BytesNumber;
55 
56 public class OTFSubSetFileTestCase extends OTFFileTestCase {
57     private Map<Integer, Integer> glyphs = new HashMap<Integer, Integer>();
58 
59     /**
60      * Initialises the test by creating the font subset. A CFFDataReader is
61      * also created based on the subset data for use in the tests.
62      * @throws IOException
63      */
64     @Before
setUp()65     public void setUp() throws Exception {
66         super.setUp();
67         for (int i = 0; i < 256; i++) {
68             glyphs.put(i, i);
69         }
70     }
71 
getCFFReaderSourceSans()72     private CFFDataReader getCFFReaderSourceSans() throws IOException {
73         byte[] sourceSansData = getSourceSansSubset().getFontSubset();
74         return new CFFDataReader(sourceSansData);
75     }
76 
getSourceSansSubset()77     private OTFSubSetFile getSourceSansSubset() throws IOException {
78         OTFSubSetFile sourceSansSubset = new OTFSubSetFile();
79         sourceSansSubset.readFont(sourceSansReader, "SourceSansProBold", null, glyphs);
80         return sourceSansSubset;
81     }
82 
83     /**
84      * Validates the CharString data against the original font
85      * @throws IOException
86      */
87     @Test
testCharStringIndex()88     public void testCharStringIndex() throws IOException {
89         CFFDataReader cffReaderSourceSans = getCFFReaderSourceSans();
90         assertEquals(256, cffReaderSourceSans.getCharStringIndex().getNumObjects());
91         assertTrue(checkCorrectOffsets(cffReaderSourceSans.getCharStringIndex()));
92         validateCharStrings(cffReaderSourceSans, getSourceSansSubset().getCFFReader());
93     }
94 
95     /**
96      * Checks the index data to ensure that the offsets are valid
97      * @param indexData The index data to check
98      * @return Returns true if it is found to be valid
99      */
checkCorrectOffsets(CFFIndexData indexData)100     private boolean checkCorrectOffsets(CFFIndexData indexData) {
101         int last = 0;
102         for (int i = 0; i < indexData.getOffsets().length; i++) {
103             if (indexData.getOffsets()[i] < last) {
104                 return false;
105             }
106             last = indexData.getOffsets()[i];
107         }
108         return true;
109     }
110 
111     /**
112      * Validates the subset font CharString data by comparing it with the original.
113      * @param subsetCFF The subset CFFDataReader containing the CharString data
114      * @param origCFF The original CFFDataReader containing the CharString data
115      * @throws IOException
116      */
validateCharStrings(CFFDataReader subsetCFF, CFFDataReader origCFF)117     private void validateCharStrings(CFFDataReader subsetCFF, CFFDataReader origCFF)
118             throws IOException {
119         CFFFont sourceSansOriginal = sourceSansProBold.fileFont;
120         CFFIndexData charStrings = subsetCFF.getCharStringIndex();
121         List<byte[]> origCharStringData = sourceSansOriginal.getCharStringBytes();
122         for (int i = 0; i < charStrings.getNumObjects(); i++) {
123             byte[] origCharData = origCharStringData.get(i);
124             byte[] charData = charStrings.getValue(i);
125             List<BytesNumber> origOperands = getFullCharString(new Context(), origCharData, origCFF);
126             List<BytesNumber> subsetOperands = getFullCharString(new Context(), charData, subsetCFF);
127             for (int j = 0; j < origOperands.size(); j++) {
128                 assertTrue(origOperands.get(j).equals(subsetOperands.get(j)));
129             }
130         }
131     }
132 
133     static class Context {
134         private ArrayList<BytesNumber> operands = new ArrayList<BytesNumber>();
135         private ArrayList<BytesNumber> stack = new ArrayList<BytesNumber>();
136         private int hstemCount;
137         private int vstemCount;
138         private int lastOp = -1;
139         private int maskLength = -1;
140 
pushOperand(BytesNumber v)141         public void pushOperand(BytesNumber v) {
142             operands.add(v);
143             if (v instanceof Operator) {
144                 if (v.getNumber() != 11 && v.getNumber() != 12) {
145                     lastOp = v.getNumber();
146                 }
147             } else {
148                 stack.add(v);
149             }
150         }
151 
popOperand()152         public BytesNumber popOperand() {
153             operands.remove(operands.size() - 1);
154             return stack.remove(stack.size() - 1);
155         }
156 
lastOperand()157         public BytesNumber lastOperand() {
158             return operands.get(operands.size() - 1);
159         }
160 
clearStack()161         public void clearStack() {
162             stack.clear();
163         }
164 
getMaskLength()165         public int getMaskLength() {
166             // The number of data bytes for mask is exactly the number needed, one
167             // bit per hint, to reference the number of stem hints declared
168             // at the beginning of the charstring program.
169             if (maskLength > 0) {
170                 return maskLength;
171             }
172             return 1 + (hstemCount + vstemCount  - 1) / 8;
173         }
174 
getFullOperandsList()175         public List<BytesNumber> getFullOperandsList() {
176             return operands;
177         }
178 
countHstem()179         public void countHstem() {
180             // hstem(hm) operator
181             hstemCount += stack.size() / 2;
182             clearStack();
183         }
184 
countVstem()185         public void countVstem() {
186             // vstem(hm) operator
187             vstemCount += stack.size() / 2;
188             clearStack();
189         }
190 
calcMaskLength()191         public int calcMaskLength() {
192             if (lastOp == 1 || lastOp == 18) {
193                 //If hstem and vstem hints are both declared at the beginning of
194                 //a charstring, and this sequence is followed directly by the
195                 //hintmask or cntrmask operators, the vstem hint operator need
196                 //not be included.
197                 vstemCount += stack.size() / 2;
198             }
199             clearStack();
200             return getMaskLength();
201         }
202     }
203     /**
204      * Recursively reads and constructs the full CharString for comparison
205      * @param data The original byte data of the CharString
206      * @param cffData The CFFDataReader containing the subroutine indexes
207      * @return Returns a list of parsed operands and operators
208      * @throws IOException
209      */
getFullCharString(Context context, byte[] data, CFFDataReader cffData)210     private List<BytesNumber> getFullCharString(Context context, byte[] data, CFFDataReader cffData)
211         throws IOException {
212         CFFIndexData localIndexSubr = cffData.getLocalIndexSubr();
213         CFFIndexData globalIndexSubr = cffData.getGlobalIndexSubr();
214         boolean hasLocalSubroutines = localIndexSubr != null && localIndexSubr.getNumObjects() > 0;
215         boolean hasGlobalSubroutines = globalIndexSubr != null && globalIndexSubr.getNumObjects() > 0;
216         for (int dataPos = 0; dataPos < data.length; dataPos++) {
217             int b0 = data[dataPos] & 0xff;
218             if (b0 == 10 && hasLocalSubroutines) {
219                 int subrNumber = getSubrNumber(localIndexSubr.getNumObjects(),
220                         context.popOperand().getNumber());
221                 byte[] subr = localIndexSubr.getValue(subrNumber);
222                 getFullCharString(context, subr, cffData);
223             } else if (b0 == 29 && hasGlobalSubroutines) {
224                 int subrNumber = getSubrNumber(globalIndexSubr.getNumObjects(),
225                         context.popOperand().getNumber());
226                 byte[] subr = globalIndexSubr.getValue(subrNumber);
227                 getFullCharString(context, subr, cffData);
228             } else if ((b0 >= 0 && b0 <= 27) || (b0 >= 29 && b0 <= 31)) {
229                 int size = 1;
230                 int b1 = -1;
231                 if (b0 == 12) {
232                     b1 = data[dataPos++] & 0xff;
233                     size = 2;
234                 } else if (b0 == 1 || b0 == 18) {
235                     context.countHstem();
236                 } else if (b0 == 3 || b0 == 23) {
237                     context.countVstem();
238                 } else if (b0 == 19 || b0 == 20) {
239                     int length = context.calcMaskLength();
240                     dataPos += length;
241                     size = length + 1;
242                 }
243                 context.pushOperand(new Operator(b0, size, getOperatorName(b0, b1)));
244             } else if (b0 == 28 || (b0 >= 32 && b0 <= 255)) {
245                 context.pushOperand(readNumber(b0, data, dataPos));
246                 dataPos += context.lastOperand().getNumBytes() - 1;
247             }
248         }
249         return context.getFullOperandsList();
250     }
251 
252     /**
253      * Parses a number from one or more bytes
254      * @param b0 The first byte to identify how to interpret the number
255      * @param input The original byte data containing the number
256      * @param curPos The current position of the number
257      * @return Returns the number
258      * @throws IOException
259      */
readNumber(int b0, byte[] input, int curPos)260     private BytesNumber readNumber(int b0, byte[] input, int curPos) throws IOException {
261         if (b0 == 28) {
262             int b1 = input[curPos + 1] & 0xff;
263             int b2 = input[curPos + 2] & 0xff;
264             return new BytesNumber((int) (short) (b1 << 8 | b2), 3);
265         } else if (b0 >= 32 && b0 <= 246) {
266             return new BytesNumber(b0 - 139, 1);
267         } else if (b0 >= 247 && b0 <= 250) {
268             int b1 = input[curPos + 1] & 0xff;
269             return new BytesNumber((b0 - 247) * 256 + b1 + 108, 2);
270         } else if (b0 >= 251 && b0 <= 254) {
271             int b1 = input[curPos + 1] & 0xff;
272             return new BytesNumber(-(b0 - 251) * 256 - b1 - 108, 2);
273         } else if (b0 == 255) {
274             int b1 = input[curPos + 1] & 0xff;
275             int b2 = input[curPos + 2] & 0xff;
276             int b3 = input[curPos + 3] & 0xff;
277             int b4 = input[curPos + 4] & 0xff;
278             return new BytesNumber((b1 << 24 | b2 << 16 | b3 << 8 | b4), 5);
279         } else {
280             throw new IllegalArgumentException();
281         }
282     }
283 
284     /**
285      * Gets the subroutine number according to the number of subroutines
286      * and the provided operand.
287      * @param numSubroutines The number of subroutines used to calculate the
288      * subroutine reference.
289      * @param operand The operand for the subroutine
290      * @return Returns the calculated subroutine number
291      */
getSubrNumber(int numSubroutines, int operand)292     private int getSubrNumber(int numSubroutines, int operand) {
293         int bias = getBias(numSubroutines);
294         return bias + operand;
295     }
296 
297     /**
298      * Gets the bias give the number of subroutines. This is used in the
299      * calculation to determine a subroutine's number
300      * @param subrCount The number of subroutines for a given index
301      * @return Returns the bias value
302      */
getBias(int subrCount)303     private int getBias(int subrCount) {
304         if (subrCount < 1240) {
305             return 107;
306         } else if (subrCount < 33900) {
307             return 1131;
308         } else {
309             return 32768;
310         }
311     }
312 
313     /**
314      * A class representing an operator from the CharString data
315      */
316     private class Operator extends BytesNumber {
317         private String opName = "";
318 
Operator(int number, int numBytes, String opName)319         Operator(int number, int numBytes, String opName) {
320             super(number, numBytes);
321             this.opName = opName;
322         }
323 
toString()324         public String toString() {
325             return String.format("[%s]", opName);
326         }
327     }
328 
329     /**
330      * Gets the identifying name for the given operator. This is primarily
331      * used for debugging purposes. See the Type 2 CharString Format specification
332      * document (Technical Note #5177) Appendix A (Command Codes).
333      * @param operator The operator code
334      * @param operatorB The second byte of the operator
335      * @return Returns the operator name.
336      */
getOperatorName(int operator, int operatorB)337     private String getOperatorName(int operator, int operatorB) {
338         switch (operator) {
339         case 0: return "Reserved";
340         case 1: return "hstem";
341         case 2: return "Reserved";
342         case 3: return "vstem";
343         case 4: return "vmoveto";
344         case 5: return "rlineto";
345         case 6: return "hlineto";
346         case 7: return "vlineto";
347         case 8: return "rrcurveto";
348         case 9: return "Reserved";
349         case 10: return "callsubr";
350         case 11: return "return";
351         case 12: return getDoubleOpName(operatorB);
352         case 13: return "Reserved";
353         case 14: return "enchar";
354         case 15:
355         case 16:
356         case 17: return "Reserved";
357         case 18: return "hstemhm";
358         case 19: return "hintmask";
359         case 20: return "cntrmask";
360         case 21: return "rmoveto";
361         case 22: return "hmoveto";
362         case 23: return "vstemhm";
363         case 24: return "rcurveline";
364         case 25: return "rlinecurve";
365         case 26: return "vvcurveto";
366         case 27: return "hhcurveto";
367         case 28: return "shortint";
368         case 29: return "callgsubr";
369         case 30: return "vhcurveto";
370         case 31: return "hvcurveto";
371         default: return "Unknown";
372         }
373     }
374 
375     /**
376      * Gets the name of a double byte operator code
377      * @param operator The second byte of the operator
378      * @return Returns the name
379      */
getDoubleOpName(int operator)380     private String getDoubleOpName(int operator) {
381         switch (operator) {
382         case 0:
383         case 1:
384         case 2: return "Reserved";
385         case 3: return "and";
386         case 4: return "or";
387         case 5: return "not";
388         case 6:
389         case 7:
390         case 8: return "Reserved";
391         case 9: return "abs";
392         case 10: return "add";
393         case 11: return "sub";
394         case 12: return "div";
395         case 13: return "Reserved";
396         case 14: return "neg";
397         case 15: return "eq";
398         case 16:
399         case 17: return "Reserved";
400         case 18: return "drop";
401         case 19: return "Reserved";
402         case 20: return "put";
403         case 21: return "get";
404         case 22: return "ifelse";
405         case 23: return "random";
406         case 24: return "mul";
407         case 25: return "Reserved";
408         case 26: return "sqrt";
409         case 27: return "dup";
410         case 28: return "exch";
411         case 29: return "index";
412         case 30: return "roll";
413         case 31:
414         case 32:
415         case 33: return "Reserved";
416         case 34: return "hflex";
417         case 35: return "flex";
418         case 36: return "hflex1";
419         case 37: return "flex1";
420         case 38: return "Reserved";
421         default: return "Unknown";
422         }
423     }
424 
425     /**
426      * Validates the String index data and size
427      * @throws IOException
428      */
429     @Test
testStringIndex()430     public void testStringIndex() throws IOException {
431         CFFDataReader cffReaderSourceSans = getCFFReaderSourceSans();
432         assertEquals(164, cffReaderSourceSans.getStringIndex().getNumObjects());
433         assertTrue(checkCorrectOffsets(cffReaderSourceSans.getStringIndex()));
434         assertEquals("Amacron", new String(cffReaderSourceSans.getStringIndex().getValue(5)));
435         assertEquals("Edotaccent", new String(cffReaderSourceSans.getStringIndex().getValue(32)));
436         assertEquals("uni0122", new String(cffReaderSourceSans.getStringIndex().getValue(45)));
437     }
438 
439     /**
440      * Validates the Top Dict data
441      * @throws IOException
442      */
443     @Test
testTopDictData()444     public void testTopDictData() throws IOException {
445         CFFDataReader cffReaderSourceSans = getCFFReaderSourceSans();
446         Map<String, DICTEntry> topDictEntries = cffReaderSourceSans.parseDictData(
447                 cffReaderSourceSans.getTopDictIndex().getData());
448         assertEquals(10, topDictEntries.size());
449     }
450 
451     @Test
testFDSelect()452     public void testFDSelect() throws IOException {
453         Assert.assertEquals(getSubset(1).length, 46);
454         Assert.assertEquals(getSubset(2).length, 45);
455     }
456 
getSubset(final int opLen)457     private byte[] getSubset(final int opLen) throws IOException {
458         FontFileReader reader = sourceSansReader;
459         OTFSubSetFile otfSubSetFile = new MyOTFSubSetFile(opLen);
460         otfSubSetFile.readFont(reader, "StandardOpenType", null, new HashMap<Integer, Integer>());
461         return otfSubSetFile.getFontSubset();
462     }
463 
464     @Test
testOffsets()465     public void testOffsets() throws IOException {
466         StringBuilder sb = new StringBuilder();
467         for (int i = 0; i < 2048; i++) {
468             sb.append("SourceSansProBold");
469         }
470         OTFSubSetFile otfSubSetFile = new OTFSubSetFile();
471         otfSubSetFile.readFont(sourceSansReader, sb.toString(), null, glyphs);
472         new CFFParser().parse(otfSubSetFile.getFontSubset());
473     }
474 
475     @Test
testCharset()476     public void testCharset() throws IOException {
477         FontFileReader reader = sourceSansReader;
478         MyOTFSubSetFile otfSubSetFile = new MyOTFSubSetFile(1);
479         otfSubSetFile.readFont(reader, "StandardOpenType", null, new HashMap<Integer, Integer>());
480         ByteArrayInputStream is = new ByteArrayInputStream(otfSubSetFile.getFontSubset());
481         is.skip(otfSubSetFile.charsetOffset);
482         Assert.assertEquals(is.read(), 2);
483     }
484 
485     class MyOTFSubSetFile extends OTFSubSetFile {
486         int charsetOffset;
487         int opLen;
MyOTFSubSetFile(int opLen)488         MyOTFSubSetFile(int opLen) throws IOException {
489             super();
490             this.opLen = opLen;
491         }
492 
createCFF()493         protected void createCFF() throws IOException {
494             cffReader = mock(CFFDataReader.class);
495             when(cffReader.getHeader()).thenReturn(new byte[0]);
496             when(cffReader.getTopDictIndex()).thenReturn(new CFFDataReader().new CFFIndexData() {
497                 public byte[] getByteData() throws IOException {
498                     return new byte[] {0, 0, 1};
499                 }
500             });
501 
502             LinkedHashMap<String, DICTEntry> map = new LinkedHashMap<String, DICTEntry>();
503             DICTEntry dict = new DICTEntry();
504             dict.setOperands(Collections.<Number>singletonList(1));
505             map.put("charset", dict);
506             map.put("CharStrings", dict);
507             when((cffReader.getTopDictEntries())).thenReturn(map);
508             CFFDataReader.Format3FDSelect fdSelect = new CFFDataReader().new Format3FDSelect();
509             fdSelect.setRanges(new HashMap<Integer, Integer>());
510             when(cffReader.getFDSelect()).thenReturn(fdSelect);
511             cffReader.getTopDictEntries().get("CharStrings").setOperandLength(opLen);
512             super.createCFF();
513         }
514 
updateFixedOffsets(Map<String, DICTEntry> topDICT, Offsets offsets)515         protected void updateFixedOffsets(Map<String, DICTEntry> topDICT, Offsets offsets) throws IOException {
516             this.charsetOffset = offsets.charset;
517             super.updateFixedOffsets(topDICT, offsets);
518         }
519     }
520 
521     @Test
testResizeOfOperand()522     public void testResizeOfOperand() throws IOException {
523         OTFSubSetFile otfSubSetFile = new OTFSubSetFile() {
524             protected void writeFDSelect() {
525                 super.writeFDSelect();
526                 writeBytes(new byte[1024 * 100]);
527             }
528         };
529         otfSubSetFile.readFont(sourceSansReader, "StandardOpenType", null, glyphs);
530         byte[] fontSubset = otfSubSetFile.getFontSubset();
531         CFFDataReader cffReader = new CFFDataReader(fontSubset);
532         assertEquals(cffReader.getTopDictEntries().get("CharStrings").getOperandLength(), 5);
533         assertEquals(cffReader.getTopDictEntries().get("CharStrings").getByteData().length, 6);
534     }
535 
536     @Test
testFDArraySize()537     public void testFDArraySize() throws IOException {
538         OTFSubSetFileFDArraySize otfSubSetFileFDArraySize = new OTFSubSetFileFDArraySize();
539         otfSubSetFileFDArraySize.readFont(sourceSansReader, "StandardOpenType", null, glyphs);
540         byte[] fontSubset = otfSubSetFileFDArraySize.getFontSubset();
541         DataInputStream dis = new DataInputStream(new ByteArrayInputStream(fontSubset));
542         dis.skipBytes(otfSubSetFileFDArraySize.offset);
543         Assert.assertEquals(dis.readUnsignedShort(), otfSubSetFileFDArraySize.fdFontCount);
544         Assert.assertEquals(dis.readByte(), 2);
545     }
546 
547     static class OTFSubSetFileFDArraySize extends OTFSubSetFile {
548         int offset;
549         int fdFontCount = 128;
550 
OTFSubSetFileFDArraySize()551         OTFSubSetFileFDArraySize() throws IOException {
552             super();
553         }
554 
createCFF()555         protected void createCFF() throws IOException {
556             super.createCFF();
557             writeFDArray(new ArrayList<Integer>(), new ArrayList<Integer>(), new ArrayList<Integer>());
558         }
559 
writeFDArray(List<Integer> uniqueNewRefs, List<Integer> privateDictOffsets, List<Integer> fontNameSIDs)560         protected int writeFDArray(List<Integer> uniqueNewRefs, List<Integer> privateDictOffsets,
561                                    List<Integer> fontNameSIDs) throws IOException {
562             List<CFFDataReader.FontDict> fdFonts = cffReader.getFDFonts();
563             CFFDataReader.FontDict fdFont = cffReader.new FontDict() {
564                 public byte[] getByteData() throws IOException {
565                     return new byte[128];
566                 }
567             };
568             cffReader = makeCFFDataReader();
569             when(cffReader.getFDFonts()).thenReturn(fdFonts);
570 
571             fdFonts.clear();
572             uniqueNewRefs.clear();
573             privateDictOffsets.clear();
574             fontNameSIDs.clear();
575             for (int i = 0; i < fdFontCount; i++) {
576                 fdFonts.add(fdFont);
577                 uniqueNewRefs.add(i);
578                 privateDictOffsets.add(i);
579                 fontNameSIDs.add(i);
580             }
581             offset = super.writeFDArray(uniqueNewRefs, privateDictOffsets, fontNameSIDs);
582             return offset;
583         }
584     }
585 
586     @Test
testOrderOfEntries()587     public void testOrderOfEntries() throws IOException {
588         OTFSubSetFileEntryOrder otfSubSetFile = getFont(3, 2);
589         assertTrue(otfSubSetFile.offsets.fdArray < otfSubSetFile.offsets.charString);
590         assertEquals(otfSubSetFile.cffReader.getTopDictEntries().get("CharStrings").getOperandLength(), 5);
591         otfSubSetFile = getFont(2, 3);
592         assertTrue(otfSubSetFile.offsets.fdArray < otfSubSetFile.offsets.charString);
593         assertEquals(otfSubSetFile.cffReader.getTopDictEntries().get("CharStrings").getOperandLength(), 5);
594     }
595 
596     private OTFSubSetFileEntryOrder getFont(int csLen, int fdLen) throws IOException {
597         glyphs.clear();
598         OTFSubSetFileEntryOrder otfSubSetFile = new OTFSubSetFileEntryOrder(csLen, fdLen);
599         otfSubSetFile.readFont(sourceSansReader, "StandardOpenType", null, glyphs);
600         return otfSubSetFile;
601     }
602 
603     static class OTFSubSetFileEntryOrder extends OTFSubSetFile {
604         Offsets offsets;
605         int csLen;
606         int fdLen;
607 
608         OTFSubSetFileEntryOrder(int csLen, int fdLen) throws IOException {
609             super();
610             this.csLen = csLen;
611             this.fdLen = fdLen;
612         }
613 
614         protected void createCFF() throws IOException {
615             cffReader = makeCFFDataReader();
616             LinkedHashMap<String, DICTEntry> topDict = new LinkedHashMap<String, DICTEntry>();
617             DICTEntry entry = new DICTEntry();
618             entry.setOperands(Collections.<Number>singletonList(0));
619             topDict.put("charset", entry);
620             entry.setOperandLength(csLen);
621             topDict.put("CharStrings", entry);
622             entry = new DICTEntry();
623             entry.setOperandLength(fdLen);
624             topDict.put("FDArray", entry);
625             when(cffReader.getTopDictEntries()).thenReturn(topDict);
626             super.createCFF();
627         }
628 
629         protected void updateCIDOffsets(Offsets offsets) throws IOException {
630             super.updateCIDOffsets(offsets);
631             this.offsets = offsets;
632         }
633     }
634 
635     private static CFFDataReader makeCFFDataReader() throws IOException {
636         CFFDataReader cffReader = mock(CFFDataReader.class);
637         when(cffReader.getHeader()).thenReturn(new byte[0]);
638         when(cffReader.getTopDictIndex()).thenReturn(cffReader.new CFFIndexData() {
639             public byte[] getByteData() throws IOException {
640                 return new byte[]{0, 0, 1};
641             }
642         });
643         CFFDataReader.Format3FDSelect fdSelect = cffReader.new Format3FDSelect();
644         fdSelect.setRanges(new HashMap<Integer, Integer>());
645         when(cffReader.getFDSelect()).thenReturn(fdSelect);
646         CFFDataReader.FontDict fd = mock(CFFDataReader.FontDict.class);
647         when(fd.getPrivateDictData()).thenReturn(new byte[0]);
648         when(cffReader.getFDFonts()).thenReturn(Collections.singletonList(fd));
649 
650         LinkedHashMap<String, DICTEntry> map = new LinkedHashMap<String, DICTEntry>();
651         DICTEntry e = new DICTEntry();
652         e.setOffset(1);
653         e.setOperandLengths(Arrays.asList(0, 0));
654         e.setOperandLength(2);
655         map.put("FontName", e);
656         map.put("Private", e);
657         map.put("Subrs", e);
658         when(cffReader.parseDictData(any(byte[].class))).thenReturn(map);
659         return cffReader;
660     }
661 
662     @Test
663     public void testWriteCIDDictsAndSubrs() throws IOException {
664         OTFSubSetFile subSetFile = new OTFSubSetFile() {
665             public void readFont(FontFileReader in, String embeddedName, MultiByteFont mbFont) throws IOException {
666                 cffReader = makeCFFDataReader();
667                 fdSubrs = new ArrayList<List<byte[]>>();
668                 fdSubrs.add(new ArrayList<byte[]>());
669                 writeCIDDictsAndSubrs(Collections.singletonList(0));
670             }
671         };
672         subSetFile.readFont(null, null, (MultiByteFont) null);
673 
674         ByteArrayInputStream is = new ByteArrayInputStream(subSetFile.getFontSubset());
675         is.skip(1);
676         Assert.assertEquals(is.read(), 247);
677         Assert.assertEquals(is.read(), 0);
678         final int sizeOfPrivateDictByteData = 108;
679         is.skip(sizeOfPrivateDictByteData - 3);
680         is.skip(2); //start index
681         Assert.assertEquals(is.read(), 1);
682     }
683 
684     @Test
685     public void testResizeOfOperand2() throws IOException {
686         OTFSubSetFile otfSubSetFile = new OTFSubSetFile() {
687             void readFont(FontFileReader in, String embeddedName, MultiByteFont mbFont,
688                           Map<Integer, Integer> usedGlyphs) throws IOException {
689                 cffReader = makeCFFDataReader();
690                 LinkedHashMap<String, DICTEntry> topDict = new LinkedHashMap<String, DICTEntry>();
691                 DICTEntry entry = new DICTEntry();
692                 entry.setOperandLength(1);
693                 entry.setOperator(new int[0]);
694                 entry.setOperands(Collections.<Number>singletonList(0));
695                 topDict.put("version", entry);
696                 when(cffReader.getTopDictEntries()).thenReturn(topDict);
697                 writeTopDICT();
698             }
699         };
700         otfSubSetFile.readFont(sourceSansReader, "StandardOpenType", null, glyphs);
701         ByteArrayInputStream fontSubset = new ByteArrayInputStream(otfSubSetFile.getFontSubset());
702         fontSubset.skip(5);
703         Assert.assertEquals(fontSubset.read(), 248);
704         Assert.assertEquals(fontSubset.read(), (byte)(390 - 108));
705     }
706 
707     @Test
708     public void testCompositeGlyphMapping() throws IOException {
709         glyphs.clear();
710         glyphs.put(0, 0);
711         OTFSubSetFile sourceSansSubset = new OTFSubSetFile() {
712             protected void initializeFont(FontFileReader in) {
713                 fileFont = new CFFType1Font() {
714                     List<Object> sequence = Arrays.asList(0, 0, 0, (int)'a', (int)'b', new CharStringCommand(12, 6));
715                     public Type2CharString getType2CharString(int gid) {
716                         return new Type2CharString(null, null, null, 0, sequence, 0, 0);
717                     }
718                 };
719             }
720         };
721         MultiByteFont multiByteFont = new MultiByteFont(null, null) {
722             public void setEmbedResourceName(String name) {
723                 super.setEmbedResourceName(name);
724                 addPrivateUseMapping('a', 'a');
725                 addPrivateUseMapping('b', 'b');
726             }
727         };
728         multiByteFont.setEmbedURI(new File(".").toURI());
729         multiByteFont.setEmbedResourceName("");
730         sourceSansSubset.readFont(sourceSansReader, "SourceSansProBold", multiByteFont, glyphs);
731         Assert.assertEquals(multiByteFont.getUsedGlyphs().toString(), "{0=0, 97=1, 98=2}");
732     }
733 }
734