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