blob: e9a610d9699e83c02df435c6153d3d5c92500155 [file] [log] [blame]
/*
* 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}");
}
}