/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* $Id$ */

package org.apache.fop.fonts.truetype;

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import org.apache.fontbox.cff.CFFFont;
import org.apache.fontbox.cff.CFFParser;
import org.apache.fontbox.cff.CFFType1Font;
import org.apache.fontbox.cff.CharStringCommand;
import org.apache.fontbox.cff.Type2CharString;

import org.apache.fop.fonts.MultiByteFont;
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;

public class OTFSubSetFileTestCase extends OTFFileTestCase {
    private Map<Integer, Integer> glyphs = new HashMap<Integer, Integer>();

    /**
     * Initialises the test by creating the font subset. A CFFDataReader is
     * also created based on the subset data for use in the tests.
     * @throws IOException
     */
    @Before
    public void setUp() throws Exception {
        super.setUp();
        for (int i = 0; i < 256; i++) {
            glyphs.put(i, i);
        }
    }

    private CFFDataReader getCFFReaderSourceSans() throws IOException {
        byte[] sourceSansData = getSourceSansSubset().getFontSubset();
        return new CFFDataReader(sourceSansData);
    }

    private OTFSubSetFile getSourceSansSubset() throws IOException {
        OTFSubSetFile sourceSansSubset = new OTFSubSetFile();
        sourceSansSubset.readFont(sourceSansReader, "SourceSansProBold", null, glyphs);
        return sourceSansSubset;
    }

    /**
     * Validates the CharString data against the original font
     * @throws IOException
     */
    @Test
    public void testCharStringIndex() throws IOException {
        CFFDataReader cffReaderSourceSans = getCFFReaderSourceSans();
        assertEquals(256, cffReaderSourceSans.getCharStringIndex().getNumObjects());
        assertTrue(checkCorrectOffsets(cffReaderSourceSans.getCharStringIndex()));
        validateCharStrings(cffReaderSourceSans, getSourceSansSubset().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++) {
            if (indexData.getOffsets()[i] < last) {
                return false;
            }
            last = indexData.getOffsets()[i];
        }
        return true;
    }

    /**
     * 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();
        List<byte[]> origCharStringData = sourceSansOriginal.getCharStringBytes();
        for (int i = 0; i < charStrings.getNumObjects(); i++) {
            byte[] origCharData = origCharStringData.get(i);
            byte[] charData = charStrings.getValue(i);
            List<BytesNumber> origOperands = getFullCharString(new Context(), origCharData, origCFF);
            List<BytesNumber> subsetOperands = getFullCharString(new Context(), charData, subsetCFF);
            for (int j = 0; j < origOperands.size(); j++) {
                assertTrue(origOperands.get(j).equals(subsetOperands.get(j)));
            }
        }
    }

    static class Context {
        private ArrayList<BytesNumber> operands = new ArrayList<BytesNumber>();
        private ArrayList<BytesNumber> stack = new ArrayList<BytesNumber>();
        private int hstemCount;
        private int vstemCount;
        private int lastOp = -1;
        private int maskLength = -1;

        public void pushOperand(BytesNumber v) {
            operands.add(v);
            if (v instanceof Operator) {
                if (v.getNumber() != 11 && v.getNumber() != 12) {
                    lastOp = v.getNumber();
                }
            } else {
                stack.add(v);
            }
        }

        public BytesNumber popOperand() {
            operands.remove(operands.size() - 1);
            return stack.remove(stack.size() - 1);
        }

        public BytesNumber lastOperand() {
            return operands.get(operands.size() - 1);
        }

        public void clearStack() {
            stack.clear();
        }

        public int getMaskLength() {
            // The number of data bytes for mask is exactly the number needed, one
            // bit per hint, to reference the number of stem hints declared
            // at the beginning of the charstring program.
            if (maskLength > 0) {
                return maskLength;
            }
            return 1 + (hstemCount + vstemCount  - 1) / 8;
        }

        public List<BytesNumber> getFullOperandsList() {
            return operands;
        }

        public void countHstem() {
            // hstem(hm) operator
            hstemCount += stack.size() / 2;
            clearStack();
        }

        public void countVstem() {
            // vstem(hm) operator
            vstemCount += stack.size() / 2;
            clearStack();
        }

        public int calcMaskLength() {
            if (lastOp == 1 || lastOp == 18) {
                //If hstem and vstem hints are both declared at the beginning of
                //a charstring, and this sequence is followed directly by the
                //hintmask or cntrmask operators, the vstem hint operator need
                //not be included.
                vstemCount += stack.size() / 2;
            }
            clearStack();
            return getMaskLength();
        }
    }
    /**
     * 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(Context context, 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;
        for (int dataPos = 0; dataPos < data.length; dataPos++) {
            int b0 = data[dataPos] & 0xff;
            if (b0 == 10 && hasLocalSubroutines) {
                int subrNumber = getSubrNumber(localIndexSubr.getNumObjects(),
                        context.popOperand().getNumber());
                byte[] subr = localIndexSubr.getValue(subrNumber);
                getFullCharString(context, subr, cffData);
            } else if (b0 == 29 && hasGlobalSubroutines) {
                int subrNumber = getSubrNumber(globalIndexSubr.getNumObjects(),
                        context.popOperand().getNumber());
                byte[] subr = globalIndexSubr.getValue(subrNumber);
                getFullCharString(context, subr, cffData);
            } 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;
                } else if (b0 == 1 || b0 == 18) {
                    context.countHstem();
                } else if (b0 == 3 || b0 == 23) {
                    context.countVstem();
                } else if (b0 == 19 || b0 == 20) {
                    int length = context.calcMaskLength();
                    dataPos += length;
                    size = length + 1;
                }
                context.pushOperand(new Operator(b0, size, getOperatorName(b0, b1)));
            } else if (b0 == 28 || (b0 >= 32 && b0 <= 255)) {
                context.pushOperand(readNumber(b0, data, dataPos));
                dataPos += context.lastOperand().getNumBytes() - 1;
            }
        }
        return context.getFullOperandsList();
    }

    /**
     * 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((int) (short) (b1 << 8 | b2), 3);
        } else if (b0 >= 32 && b0 <= 246) {
            return new BytesNumber(b0 - 139, 1);
        } else if (b0 >= 247 && b0 <= 250) {
            int b1 = input[curPos + 1] & 0xff;
            return new BytesNumber((b0 - 247) * 256 + b1 + 108, 2);
        } else if (b0 >= 251 && b0 <= 254) {
            int b1 = input[curPos + 1] & 0xff;
            return new BytesNumber(-(b0 - 251) * 256 - b1 - 108, 2);
        } else if (b0 == 255) {
            int b1 = input[curPos + 1] & 0xff;
            int b2 = input[curPos + 2] & 0xff;
            int b3 = input[curPos + 3] & 0xff;
            int b4 = input[curPos + 4] & 0xff;
            return new BytesNumber((b1 << 24 | b2 << 16 | b3 << 8 | b4), 5);
        } else {
            throw new IllegalArgumentException();
        }
    }

    /**
     * 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;
    }

    /**
     * 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;
        }
    }

    /**
     * A class representing an operator from the CharString data
     */
    private class Operator extends BytesNumber {
        private String opName = "";

        Operator(int number, int numBytes, String opName) {
            super(number, numBytes);
            this.opName = opName;
        }

        public String toString() {
            return String.format("[%s]", opName);
        }
    }

    /**
     * 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 operatorB 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
     */
    @Test
    public void testStringIndex() throws IOException {
        CFFDataReader cffReaderSourceSans = getCFFReaderSourceSans();
        assertEquals(164, cffReaderSourceSans.getStringIndex().getNumObjects());
        assertTrue(checkCorrectOffsets(cffReaderSourceSans.getStringIndex()));
        assertEquals("Amacron", new String(cffReaderSourceSans.getStringIndex().getValue(5)));
        assertEquals("Edotaccent", new String(cffReaderSourceSans.getStringIndex().getValue(32)));
        assertEquals("uni0122", new String(cffReaderSourceSans.getStringIndex().getValue(45)));
    }

    /**
     * Validates the Top Dict data
     * @throws IOException
     */
    @Test
    public void testTopDictData() throws IOException {
        CFFDataReader cffReaderSourceSans = getCFFReaderSourceSans();
        Map<String, DICTEntry> topDictEntries = cffReaderSourceSans.parseDictData(
                cffReaderSourceSans.getTopDictIndex().getData());
        assertEquals(10, topDictEntries.size());
    }

    @Test
    public void testFDSelect() throws IOException {
        Assert.assertEquals(getSubset(1).length, 46);
        Assert.assertEquals(getSubset(2).length, 45);
    }

    private byte[] getSubset(final int opLen) throws IOException {
        FontFileReader reader = sourceSansReader;
        OTFSubSetFile otfSubSetFile = new MyOTFSubSetFile(opLen);
        otfSubSetFile.readFont(reader, "StandardOpenType", null, new HashMap<Integer, Integer>());
        return otfSubSetFile.getFontSubset();
    }

    @Test
    public void testOffsets() throws IOException {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 2048; i++) {
            sb.append("SourceSansProBold");
        }
        OTFSubSetFile otfSubSetFile = new OTFSubSetFile();
        otfSubSetFile.readFont(sourceSansReader, sb.toString(), null, glyphs);
        new CFFParser().parse(otfSubSetFile.getFontSubset());
    }

    @Test
    public void testCharset() throws IOException {
        FontFileReader reader = sourceSansReader;
        MyOTFSubSetFile otfSubSetFile = new MyOTFSubSetFile(1);
        otfSubSetFile.readFont(reader, "StandardOpenType", null, new HashMap<Integer, Integer>());
        ByteArrayInputStream is = new ByteArrayInputStream(otfSubSetFile.getFontSubset());
        is.skip(otfSubSetFile.charsetOffset);
        Assert.assertEquals(is.read(), 2);
    }

    class MyOTFSubSetFile extends OTFSubSetFile {
        int charsetOffset;
        int opLen;
        MyOTFSubSetFile(int opLen) throws IOException {
            super();
            this.opLen = opLen;
        }

        protected void createCFF() throws IOException {
            cffReader = mock(CFFDataReader.class);
            when(cffReader.getHeader()).thenReturn(new byte[0]);
            when(cffReader.getTopDictIndex()).thenReturn(new CFFDataReader().new CFFIndexData() {
                public byte[] getByteData() throws IOException {
                    return new byte[] {0, 0, 1};
                }
            });

            LinkedHashMap<String, DICTEntry> map = new LinkedHashMap<String, DICTEntry>();
            DICTEntry dict = new DICTEntry();
            dict.setOperands(Collections.<Number>singletonList(1));
            map.put("charset", dict);
            map.put("CharStrings", dict);
            when((cffReader.getTopDictEntries())).thenReturn(map);
            CFFDataReader.Format3FDSelect fdSelect = new CFFDataReader().new Format3FDSelect();
            fdSelect.setRanges(new HashMap<Integer, Integer>());
            when(cffReader.getFDSelect()).thenReturn(fdSelect);
            cffReader.getTopDictEntries().get("CharStrings").setOperandLength(opLen);
            super.createCFF();
        }

        protected void updateFixedOffsets(Map<String, DICTEntry> topDICT, Offsets offsets) throws IOException {
            this.charsetOffset = offsets.charset;
            super.updateFixedOffsets(topDICT, offsets);
        }
    }

    @Test
    public void testResizeOfOperand() throws IOException {
        OTFSubSetFile otfSubSetFile = new OTFSubSetFile() {
            protected void writeFDSelect() {
                super.writeFDSelect();
                writeBytes(new byte[1024 * 100]);
            }
        };
        otfSubSetFile.readFont(sourceSansReader, "StandardOpenType", null, glyphs);
        byte[] fontSubset = otfSubSetFile.getFontSubset();
        CFFDataReader cffReader = new CFFDataReader(fontSubset);
        assertEquals(cffReader.getTopDictEntries().get("CharStrings").getOperandLength(), 5);
        assertEquals(cffReader.getTopDictEntries().get("CharStrings").getByteData().length, 6);
    }

    @Test
    public void testFDArraySize() throws IOException {
        OTFSubSetFileFDArraySize otfSubSetFileFDArraySize = new OTFSubSetFileFDArraySize();
        otfSubSetFileFDArraySize.readFont(sourceSansReader, "StandardOpenType", null, glyphs);
        byte[] fontSubset = otfSubSetFileFDArraySize.getFontSubset();
        DataInputStream dis = new DataInputStream(new ByteArrayInputStream(fontSubset));
        dis.skipBytes(otfSubSetFileFDArraySize.offset);
        Assert.assertEquals(dis.readUnsignedShort(), otfSubSetFileFDArraySize.fdFontCount);
        Assert.assertEquals(dis.readByte(), 2);
    }

    static class OTFSubSetFileFDArraySize extends OTFSubSetFile {
        int offset;
        int fdFontCount = 128;

        OTFSubSetFileFDArraySize() throws IOException {
            super();
        }

        protected void createCFF() throws IOException {
            super.createCFF();
            writeFDArray(new ArrayList<Integer>(), new ArrayList<Integer>(), new ArrayList<Integer>());
        }

        protected int writeFDArray(List<Integer> uniqueNewRefs, List<Integer> privateDictOffsets,
                                   List<Integer> fontNameSIDs) throws IOException {
            List<CFFDataReader.FontDict> fdFonts = cffReader.getFDFonts();
            CFFDataReader.FontDict fdFont = cffReader.new FontDict() {
                public byte[] getByteData() throws IOException {
                    return new byte[128];
                }
            };
            cffReader = makeCFFDataReader();
            when(cffReader.getFDFonts()).thenReturn(fdFonts);

            fdFonts.clear();
            uniqueNewRefs.clear();
            privateDictOffsets.clear();
            fontNameSIDs.clear();
            for (int i = 0; i < fdFontCount; i++) {
                fdFonts.add(fdFont);
                uniqueNewRefs.add(i);
                privateDictOffsets.add(i);
                fontNameSIDs.add(i);
            }
            offset = super.writeFDArray(uniqueNewRefs, privateDictOffsets, fontNameSIDs);
            return offset;
        }
    }

    @Test
    public void testOrderOfEntries() throws IOException {
        OTFSubSetFileEntryOrder otfSubSetFile = getFont(3, 2);
        assertTrue(otfSubSetFile.offsets.fdArray < otfSubSetFile.offsets.charString);
        assertEquals(otfSubSetFile.cffReader.getTopDictEntries().get("CharStrings").getOperandLength(), 5);
        otfSubSetFile = getFont(2, 3);
        assertTrue(otfSubSetFile.offsets.fdArray < otfSubSetFile.offsets.charString);
        assertEquals(otfSubSetFile.cffReader.getTopDictEntries().get("CharStrings").getOperandLength(), 5);
    }

    private OTFSubSetFileEntryOrder getFont(int csLen, int fdLen) throws IOException {
        glyphs.clear();
        OTFSubSetFileEntryOrder otfSubSetFile = new OTFSubSetFileEntryOrder(csLen, fdLen);
        otfSubSetFile.readFont(sourceSansReader, "StandardOpenType", null, glyphs);
        return otfSubSetFile;
    }

    static class OTFSubSetFileEntryOrder extends OTFSubSetFile {
        Offsets offsets;
        int csLen;
        int fdLen;

        OTFSubSetFileEntryOrder(int csLen, int fdLen) throws IOException {
            super();
            this.csLen = csLen;
            this.fdLen = fdLen;
        }

        protected void createCFF() throws IOException {
            cffReader = makeCFFDataReader();
            LinkedHashMap<String, DICTEntry> topDict = new LinkedHashMap<String, DICTEntry>();
            DICTEntry entry = new DICTEntry();
            entry.setOperands(Collections.<Number>singletonList(0));
            topDict.put("charset", entry);
            entry.setOperandLength(csLen);
            topDict.put("CharStrings", entry);
            entry = new DICTEntry();
            entry.setOperandLength(fdLen);
            topDict.put("FDArray", entry);
            when(cffReader.getTopDictEntries()).thenReturn(topDict);
            super.createCFF();
        }

        protected void updateCIDOffsets(Offsets offsets) throws IOException {
            super.updateCIDOffsets(offsets);
            this.offsets = offsets;
        }
    }

    private static CFFDataReader makeCFFDataReader() throws IOException {
        CFFDataReader cffReader = mock(CFFDataReader.class);
        when(cffReader.getHeader()).thenReturn(new byte[0]);
        when(cffReader.getTopDictIndex()).thenReturn(cffReader.new CFFIndexData() {
            public byte[] getByteData() throws IOException {
                return new byte[]{0, 0, 1};
            }
        });
        CFFDataReader.Format3FDSelect fdSelect = cffReader.new Format3FDSelect();
        fdSelect.setRanges(new HashMap<Integer, Integer>());
        when(cffReader.getFDSelect()).thenReturn(fdSelect);
        CFFDataReader.FontDict fd = mock(CFFDataReader.FontDict.class);
        when(fd.getPrivateDictData()).thenReturn(new byte[0]);
        when(cffReader.getFDFonts()).thenReturn(Collections.singletonList(fd));

        LinkedHashMap<String, DICTEntry> map = new LinkedHashMap<String, DICTEntry>();
        DICTEntry e = new DICTEntry();
        e.setOffset(1);
        e.setOperandLengths(Arrays.asList(0, 0));
        e.setOperandLength(2);
        map.put("FontName", e);
        map.put("Private", e);
        map.put("Subrs", e);
        when(cffReader.parseDictData(any(byte[].class))).thenReturn(map);
        return cffReader;
    }

    @Test
    public void testWriteCIDDictsAndSubrs() throws IOException {
        OTFSubSetFile subSetFile = new OTFSubSetFile() {
            public void readFont(FontFileReader in, String embeddedName, MultiByteFont mbFont) throws IOException {
                cffReader = makeCFFDataReader();
                fdSubrs = new ArrayList<List<byte[]>>();
                fdSubrs.add(new ArrayList<byte[]>());
                writeCIDDictsAndSubrs(Collections.singletonList(0));
            }
        };
        subSetFile.readFont(null, null, (MultiByteFont) null);

        ByteArrayInputStream is = new ByteArrayInputStream(subSetFile.getFontSubset());
        is.skip(1);
        Assert.assertEquals(is.read(), 247);
        Assert.assertEquals(is.read(), 0);
        final int sizeOfPrivateDictByteData = 108;
        is.skip(sizeOfPrivateDictByteData - 3);
        is.skip(2); //start index
        Assert.assertEquals(is.read(), 1);
    }

    @Test
    public void testResizeOfOperand2() throws IOException {
        OTFSubSetFile otfSubSetFile = new OTFSubSetFile() {
            void readFont(FontFileReader in, String embeddedName, MultiByteFont mbFont,
                          Map<Integer, Integer> usedGlyphs) throws IOException {
                cffReader = makeCFFDataReader();
                LinkedHashMap<String, DICTEntry> topDict = new LinkedHashMap<String, DICTEntry>();
                DICTEntry entry = new DICTEntry();
                entry.setOperandLength(1);
                entry.setOperator(new int[0]);
                entry.setOperands(Collections.<Number>singletonList(0));
                topDict.put("version", entry);
                when(cffReader.getTopDictEntries()).thenReturn(topDict);
                writeTopDICT();
            }
        };
        otfSubSetFile.readFont(sourceSansReader, "StandardOpenType", null, glyphs);
        ByteArrayInputStream fontSubset = new ByteArrayInputStream(otfSubSetFile.getFontSubset());
        fontSubset.skip(5);
        Assert.assertEquals(fontSubset.read(), 248);
        Assert.assertEquals(fontSubset.read(), (byte)(390 - 108));
    }

    @Test
    public void testCompositeGlyphMapping() throws IOException {
        glyphs.clear();
        glyphs.put(0, 0);
        OTFSubSetFile sourceSansSubset = new OTFSubSetFile() {
            protected void initializeFont(FontFileReader in) {
                fileFont = new CFFType1Font() {
                    List<Object> sequence = Arrays.asList(0, 0, 0, (int)'a', (int)'b', new CharStringCommand(12, 6));
                    public Type2CharString getType2CharString(int gid) {
                        return new Type2CharString(null, null, null, 0, sequence, 0, 0);
                    }
                };
            }
        };
        MultiByteFont multiByteFont = new MultiByteFont(null, null) {
            public void setEmbedResourceName(String name) {
                super.setEmbedResourceName(name);
                addPrivateUseMapping('a', 'a');
                addPrivateUseMapping('b', 'b');
            }
        };
        multiByteFont.setEmbedURI(new File(".").toURI());
        multiByteFont.setEmbedResourceName("");
        sourceSansSubset.readFont(sourceSansReader, "SourceSansProBold", multiByteFont, glyphs);
        Assert.assertEquals(multiByteFont.getUsedGlyphs().toString(), "{0=0, 97=1, 98=2}");
    }
}
