blob: 6f8e948ddcf98c346d6199787b58f36c84b64ce3 [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.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.fontbox.cff.CFFStandardString;
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.cff.CFFDataReader.FDSelect;
import org.apache.fop.fonts.cff.CFFDataReader.FontDict;
import org.apache.fop.fonts.cff.CFFDataReader.Format0FDSelect;
import org.apache.fop.fonts.cff.CFFDataReader.Format3FDSelect;
import org.apache.fop.fonts.type1.AdobeStandardEncoding;
/**
* Reads an OpenType CFF file and generates a subset
* The OpenType specification can be found at the Microsoft
* Typography site: http://www.microsoft.com/typography/otspec/
*/
public class OTFSubSetFile extends OTFSubSetWriter {
/** A map containing each glyph to be included in the subset
* with their existing and new GID's **/
protected Map<Integer, Integer> subsetGlyphs = new LinkedHashMap<Integer, Integer>();
/** A map of the new GID to SID used to construct the charset table **/
protected Map<Integer, Integer> gidToSID;
protected CFFIndexData localIndexSubr;
protected CFFIndexData globalIndexSubr;
/** List of subroutines to write to the local / global indexes in the subset font **/
protected List<byte[]> subsetLocalIndexSubr;
protected List<byte[]> subsetGlobalIndexSubr;
/** For fonts which have an FDSelect or ROS flag in Top Dict, this is used to store the
* local subroutine indexes for each group as opposed to the above subsetLocalIndexSubr */
protected List<List<byte[]>> fdSubrs;
/** The subset FD Select table used to store the mappings between glyphs and their
* associated FDFont object which point to a private dict and local subroutines. */
private Map<Integer, FDIndexReference> subsetFDSelect;
/** A list of unique subroutines from the global / local subroutine indexes */
protected List<Integer> localUniques;
protected List<Integer> globalUniques;
/** A store of the number of subroutines each global / local subroutine will store **/
protected int subsetLocalSubrCount;
protected int subsetGlobalSubrCount;
/** A list of char string data for each glyph to be stored in the subset font **/
protected List<byte[]> subsetCharStringsIndex;
/** The embedded name to change in the name table **/
protected String embeddedName;
/** An array used to hold the string index data for the subset font **/
protected List<byte[]> stringIndexData = new ArrayList<byte[]>();
/** The CFF reader object used to read data and offsets from the original font file */
protected CFFDataReader cffReader;
/** The class used to represent this font **/
private MultiByteFont mbFont;
/** The number of standard strings in CFF **/
public static final int NUM_STANDARD_STRINGS = 391;
/** The operator used to identify a local subroutine reference */
private static final int LOCAL_SUBROUTINE = 10;
/** The operator used to identify a global subroutine reference */
private static final int GLOBAL_SUBROUTINE = 29;
private static final String ACCENT_CMD = "seac";
/** The parser used to parse type2 charstring */
private Type2Parser type2Parser;
public OTFSubSetFile() throws IOException {
super();
}
public void readFont(FontFileReader in, String embeddedName, MultiByteFont mbFont) throws IOException {
readFont(in, embeddedName, mbFont, mbFont.getUsedGlyphs());
}
/**
* Reads and creates a subset of the font.
*
* @param in FontFileReader to read from
* @param embeddedName Name to be checked for in the font file
* @param usedGlyphs Map of glyphs (glyphs has old index as (Integer) key and
* new index as (Integer) value)
* @throws IOException in case of an I/O problem
*/
void readFont(FontFileReader in, String embeddedName, MultiByteFont mbFont,
Map<Integer, Integer> usedGlyphs) throws IOException {
this.mbFont = mbFont;
fontFile = in;
this.embeddedName = embeddedName;
initializeFont(in);
cffReader = new CFFDataReader(fontFile);
mapChars(usedGlyphs);
//Sort by the new GID and store in a LinkedHashMap
subsetGlyphs = sortByValue(usedGlyphs);
//Create the CIDFontType0C data
createCFF();
}
private void mapChars(Map<Integer, Integer> usedGlyphs) throws IOException {
if (fileFont instanceof CFFType1Font) {
CFFType1Font cffType1Font = (CFFType1Font) fileFont;
subsetGlyphs = sortByValue(usedGlyphs);
for (int gid : subsetGlyphs.keySet()) {
Type2CharString type2CharString = cffType1Font.getType2CharString(gid);
List<Number> stack = new ArrayList<Number>();
for (Object obj : type2CharString.getType1Sequence()) {
if (obj instanceof CharStringCommand) {
String name = CharStringCommand.TYPE1_VOCABULARY.get(((CharStringCommand) obj).getKey());
if (ACCENT_CMD.equals(name)) {
int first = stack.get(3).intValue();
int second = stack.get(4).intValue();
mbFont.mapChar(AdobeStandardEncoding.getUnicodeFromCodePoint(first));
mbFont.mapChar(AdobeStandardEncoding.getUnicodeFromCodePoint(second));
}
stack.clear();
} else {
stack.add((Number) obj);
}
}
}
}
}
private Map<Integer, Integer> sortByValue(Map<Integer, Integer> map) {
List<Entry<Integer, Integer>> list = new ArrayList<Entry<Integer, Integer>>(map.entrySet());
Collections.sort(list, new Comparator<Entry<Integer, Integer>>() {
public int compare(Entry<Integer, Integer> o1, Entry<Integer, Integer> o2) {
return ((Comparable<Integer>) o1.getValue()).compareTo(o2.getValue());
}
});
Map<Integer, Integer> result = new LinkedHashMap<Integer, Integer>();
for (Entry<Integer, Integer> entry : list) {
result.put(entry.getKey(), entry.getValue());
}
return result;
}
protected void createCFF() throws IOException {
//Header
writeBytes(cffReader.getHeader());
//Name Index
writeIndex(Arrays.asList(embedFontName.getBytes("UTF-8")));
Offsets offsets = new Offsets();
//Top DICT Index and Data
offsets.topDictData = currentPos + writeTopDICT();
boolean hasFDSelect = cffReader.getFDSelect() != null;
//Create the char string index data and related local / global subroutines
if (hasFDSelect) {
createCharStringDataCID();
} else {
createCharStringData();
}
//If it is a CID-Keyed font, store each FD font and add each SID
List<Integer> fontNameSIDs = null;
List<Integer> subsetFDFonts = null;
if (hasFDSelect) {
subsetFDFonts = getUsedFDFonts();
fontNameSIDs = storeFDStrings(subsetFDFonts);
}
//String index
writeStringIndex();
//Global subroutine index
writeIndex(subsetGlobalIndexSubr);
//Encoding
offsets.encoding = currentPos;
//Charset table
offsets.charset = currentPos;
writeCharsetTable(hasFDSelect);
//FDSelect table
offsets.fdSelect = currentPos;
if (hasFDSelect) {
writeFDSelect();
if (!isCharStringBeforeFD()) {
offsets.fdArray = writeFDArray(subsetFDFonts, fontNameSIDs);
}
}
//Char Strings Index
offsets.charString = currentPos;
writeIndex(subsetCharStringsIndex);
if (hasFDSelect) {
if (isCharStringBeforeFD()) {
offsets.fdArray = writeFDArray(subsetFDFonts, fontNameSIDs);
}
updateCIDOffsets(offsets);
} else {
//Keep offset to modify later with the local subroutine index offset
offsets.privateDict = currentPos;
writePrivateDict();
//Local subroutine index
offsets.localIndex = currentPos;
writeIndex(subsetLocalIndexSubr);
//Update the offsets
updateOffsets(offsets);
}
}
static class Offsets {
Integer topDictData;
Integer encoding;
Integer charset;
Integer fdSelect;
Integer charString;
Integer fdArray;
Integer privateDict;
Integer localIndex;
}
private int writeFDArray(List<Integer> subsetFDFonts, List<Integer> fontNameSIDs) throws IOException {
List<Integer> privateDictOffsets = writeCIDDictsAndSubrs(subsetFDFonts);
return writeFDArray(subsetFDFonts, privateDictOffsets, fontNameSIDs);
}
private boolean isCharStringBeforeFD() {
LinkedHashMap<String, DICTEntry> entries = cffReader.getTopDictEntries();
int len = entries.get("CharStrings").getOperandLength();
if (entries.containsKey("FDArray")) {
int len2 = entries.get("FDArray").getOperandLength();
return len < len2;
}
return true;
}
protected List<Integer> storeFDStrings(List<Integer> uniqueNewRefs) throws IOException {
List<Integer> fontNameSIDs = new ArrayList<Integer>();
List<FontDict> fdFonts = cffReader.getFDFonts();
for (int uniqueNewRef : uniqueNewRefs) {
FontDict fdFont = fdFonts.get(uniqueNewRef);
byte[] fdFontByteData = fdFont.getByteData();
Map<String, DICTEntry> fdFontDict = cffReader.parseDictData(fdFontByteData);
fontNameSIDs.add(stringIndexData.size() + NUM_STANDARD_STRINGS);
stringIndexData.add(cffReader.getStringIndex().getValue(fdFontDict.get("FontName")
.getOperands().get(0).intValue() - NUM_STANDARD_STRINGS));
}
return fontNameSIDs;
}
protected int writeTopDICT() throws IOException {
Map<String, DICTEntry> topDICT = cffReader.getTopDictEntries();
List<String> topDictStringEntries = Arrays.asList("version", "Notice", "Copyright",
"FullName", "FamilyName", "Weight", "PostScript");
ByteArrayOutputStream dict = new ByteArrayOutputStream();
int offsetExtra = 0;
for (Map.Entry<String, DICTEntry> dictEntry : topDICT.entrySet()) {
String dictKey = dictEntry.getKey();
DICTEntry entry = dictEntry.getValue();
//If the value is an SID, update the reference but keep the size the same
entry.setOffset(entry.getOffset() + offsetExtra);
if (dictKey.equals("CharStrings") && entry.getOperandLength() < 5) {
byte[] extra = new byte[5 - entry.getOperandLength()];
offsetExtra += extra.length;
dict.write(extra);
dict.write(entry.getByteData());
entry.setOperandLength(5);
} else if (dictKey.equals("ROS")) {
dict.write(writeROSEntry(entry));
} else if (dictKey.equals("CIDCount")) {
dict.write(writeCIDCount(entry));
} else if (topDictStringEntries.contains(dictKey)) {
if (entry.getOperandLength() < 2) {
entry.setOperandLength(2);
offsetExtra++;
}
dict.write(writeTopDictStringEntry(entry));
} else {
dict.write(entry.getByteData());
}
}
byte[] topDictIndex = cffReader.getTopDictIndex().getByteData();
int offSize = topDictIndex[2];
return writeIndex(Arrays.asList(dict.toByteArray()), offSize) - dict.size();
}
private byte[] writeROSEntry(DICTEntry dictEntry) throws IOException {
int sidA = dictEntry.getOperands().get(0).intValue();
if (sidA > 390) {
stringIndexData.add(cffReader.getStringIndex().getValue(sidA - NUM_STANDARD_STRINGS));
}
int sidAStringIndex = stringIndexData.size() + 390;
int sidB = dictEntry.getOperands().get(1).intValue();
if (sidB > 390) {
stringIndexData.add("Identity".getBytes("UTF-8"));
}
int sidBStringIndex = stringIndexData.size() + 390;
byte[] cidEntryByteData = dictEntry.getByteData();
updateOffset(cidEntryByteData, 0, dictEntry.getOperandLengths().get(0),
sidAStringIndex);
updateOffset(cidEntryByteData, dictEntry.getOperandLengths().get(0),
dictEntry.getOperandLengths().get(1), sidBStringIndex);
updateOffset(cidEntryByteData, dictEntry.getOperandLengths().get(0)
+ dictEntry.getOperandLengths().get(1), dictEntry.getOperandLengths().get(2), 0);
return cidEntryByteData;
}
protected byte[] writeCIDCount(DICTEntry dictEntry) throws IOException {
byte[] cidCountByteData = dictEntry.getByteData();
updateOffset(cidCountByteData, 0, dictEntry.getOperandLengths().get(0),
subsetGlyphs.size());
return cidCountByteData;
}
private byte[] writeTopDictStringEntry(DICTEntry dictEntry) throws IOException {
int sid = dictEntry.getOperands().get(0).intValue();
if (sid > 391) {
stringIndexData.add(cffReader.getStringIndex().getValue(sid - 391));
}
byte[] newDictEntry = createNewRef(stringIndexData.size() + 390, dictEntry.getOperator(),
dictEntry.getOperandLength(), true);
return newDictEntry;
}
private void writeStringIndex() throws IOException {
Map<String, DICTEntry> topDICT = cffReader.getTopDictEntries();
int charsetOffset = topDICT.get("charset").getOperands().get(0).intValue();
gidToSID = new LinkedHashMap<Integer, Integer>();
for (Entry<Integer, Integer> subsetGlyph : subsetGlyphs.entrySet()) {
int gid = subsetGlyph.getKey();
int v = subsetGlyph.getValue();
int sid = cffReader.getSIDFromGID(charsetOffset, gid);
//Check whether the SID falls into the standard string set
if (sid < NUM_STANDARD_STRINGS) {
gidToSID.put(v, sid);
if (mbFont != null) {
mbFont.mapUsedGlyphName(v, CFFStandardString.getName(sid));
}
} else {
int index = sid - NUM_STANDARD_STRINGS;
//index is 0 based, should use < not <=
if (index < cffReader.getStringIndex().getNumObjects()) {
byte[] value = cffReader.getStringIndex().getValue(index);
if (mbFont != null) {
mbFont.mapUsedGlyphName(v, new String(value, "UTF-8"));
}
gidToSID.put(v, stringIndexData.size() + 391);
stringIndexData.add(value);
} else {
if (mbFont != null) {
mbFont.mapUsedGlyphName(v, ".notdef");
}
gidToSID.put(v, index);
}
}
}
//Write the String Index
writeIndex(stringIndexData);
}
protected void createCharStringDataCID() throws IOException {
CFFIndexData charStringsIndex = cffReader.getCharStringIndex();
FDSelect fontDictionary = cffReader.getFDSelect();
if (fontDictionary instanceof Format0FDSelect) {
throw new UnsupportedOperationException("OTF CFF CID Format0 currently not implemented");
} else if (fontDictionary instanceof Format3FDSelect) {
Format3FDSelect fdSelect = (Format3FDSelect)fontDictionary;
Map<Integer, Integer> subsetGroups = new HashMap<Integer, Integer>();
List<Integer> uniqueGroups = new ArrayList<Integer>();
Map<Integer, Integer> rangeMap = fdSelect.getRanges();
Integer[] ranges = rangeMap.keySet().toArray(new Integer[rangeMap.size()]);
for (int gid : subsetGlyphs.keySet()) {
int i = 0;
for (Entry<Integer, Integer> entry : rangeMap.entrySet()) {
int nextRange;
if (i < ranges.length - 1) {
nextRange = ranges[i + 1];
} else {
nextRange = fdSelect.getSentinelGID();
}
if (gid >= entry.getKey() && gid < nextRange) {
int r = entry.getValue();
subsetGroups.put(gid, r);
if (!uniqueGroups.contains(r)) {
uniqueGroups.add(r);
}
}
i++;
}
}
//Prepare resources
globalIndexSubr = cffReader.getGlobalIndexSubr();
//Create the new char string index
subsetCharStringsIndex = new ArrayList<byte[]>();
globalUniques = new ArrayList<Integer>();
subsetFDSelect = new LinkedHashMap<Integer, FDIndexReference>();
List<List<Integer>> foundLocalUniques = new ArrayList<List<Integer>>();
for (int u : uniqueGroups) {
foundLocalUniques.add(new ArrayList<Integer>());
}
Map<Integer, Integer> gidHintMaskLengths = new HashMap<Integer, Integer>();
for (Entry<Integer, Integer> subsetGlyph : subsetGlyphs.entrySet()) {
int gid = subsetGlyph.getKey();
int group = subsetGroups.get(gid);
localIndexSubr = cffReader.getFDFonts().get(group).getLocalSubrData();
localUniques = foundLocalUniques.get(uniqueGroups.indexOf(group));
type2Parser = new Type2Parser();
FDIndexReference newFDReference = new FDIndexReference(uniqueGroups.indexOf(group), group);
subsetFDSelect.put(subsetGlyph.getValue(), newFDReference);
byte[] data = charStringsIndex.getValue(gid);
preScanForSubsetIndexSize(data);
gidHintMaskLengths.put(gid, type2Parser.getMaskLength());
}
//Create the two lists which are to store the local and global subroutines
subsetGlobalIndexSubr = new ArrayList<byte[]>();
fdSubrs = new ArrayList<List<byte[]>>();
subsetGlobalSubrCount = globalUniques.size();
globalUniques.clear();
localUniques = null;
for (List<Integer> foundLocalUnique : foundLocalUniques) {
fdSubrs.add(new ArrayList<byte[]>());
}
List<List<Integer>> foundLocalUniquesB = new ArrayList<List<Integer>>();
for (int u : uniqueGroups) {
foundLocalUniquesB.add(new ArrayList<Integer>());
}
for (Entry<Integer, Integer> subsetGlyph : subsetGlyphs.entrySet()) {
int gid = subsetGlyph.getKey();
int value = subsetGlyph.getValue();
int group = subsetGroups.get(gid);
localIndexSubr = cffReader.getFDFonts().get(group).getLocalSubrData();
int newFDIndex = subsetFDSelect.get(value).getNewFDIndex();
localUniques = foundLocalUniquesB.get(newFDIndex);
byte[] data = charStringsIndex.getValue(gid);
subsetLocalIndexSubr = fdSubrs.get(newFDIndex);
subsetLocalSubrCount = foundLocalUniques.get(newFDIndex).size();
type2Parser = new Type2Parser();
type2Parser.setMaskLength(gidHintMaskLengths.get(gid));
data = readCharStringData(data, subsetLocalSubrCount);
subsetCharStringsIndex.add(data);
}
}
}
protected void writeFDSelect() {
if (cffReader.getTopDictEntries().get("CharStrings").getOperandLength() == 2) {
Map<Integer, Integer> indexs = getFormat3Index();
writeByte(3); //Format
writeCard16(indexs.size());
int count = 0;
for (Entry<Integer, Integer> x : indexs.entrySet()) {
writeCard16(count);
writeByte(x.getKey());
count += x.getValue();
}
writeCard16(subsetFDSelect.size());
} else {
writeByte(0); //Format
for (FDIndexReference e : subsetFDSelect.values()) {
writeByte(e.getNewFDIndex());
}
}
}
private Map<Integer, Integer> getFormat3Index() {
Map<Integer, Integer> indexs = new LinkedHashMap<Integer, Integer>();
int last = -1;
int count = 0;
for (FDIndexReference e : subsetFDSelect.values()) {
int i = e.getNewFDIndex();
count++;
if (i != last) {
indexs.put(i, count);
count = 1;
}
last = i;
}
indexs.put(last, count);
return indexs;
}
protected List<Integer> getUsedFDFonts() {
List<Integer> uniqueNewRefs = new ArrayList<Integer>();
for (FDIndexReference e : subsetFDSelect.values()) {
int fdIndex = e.getOldFDIndex();
if (!uniqueNewRefs.contains(fdIndex)) {
uniqueNewRefs.add(fdIndex);
}
}
return uniqueNewRefs;
}
protected List<Integer> writeCIDDictsAndSubrs(List<Integer> uniqueNewRefs)
throws IOException {
List<Integer> privateDictOffsets = new ArrayList<Integer>();
List<FontDict> fdFonts = cffReader.getFDFonts();
int i = 0;
for (int ref : uniqueNewRefs) {
FontDict curFDFont = fdFonts.get(ref);
byte[] fdPrivateDictByteData = curFDFont.getPrivateDictData();
Map<String, DICTEntry> fdPrivateDict = cffReader.parseDictData(fdPrivateDictByteData);
int privateDictOffset = currentPos;
privateDictOffsets.add(privateDictOffset);
DICTEntry subrs = fdPrivateDict.get("Subrs");
if (subrs != null) {
fdPrivateDictByteData = resizeToFitOpLen(fdPrivateDictByteData, subrs);
updateOffset(fdPrivateDictByteData, subrs.getOffset(),
subrs.getOperandLength(),
fdPrivateDictByteData.length);
}
writeBytes(fdPrivateDictByteData);
writeIndex(fdSubrs.get(i));
i++;
}
return privateDictOffsets;
}
private byte[] resizeToFitOpLen(byte[] fdPrivateDictByteData, DICTEntry subrs) throws IOException {
if (subrs.getOperandLength() == 2 && fdPrivateDictByteData.length < 108) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write(fdPrivateDictByteData);
bos.write(new byte[108 - fdPrivateDictByteData.length]);
fdPrivateDictByteData = bos.toByteArray();
}
return fdPrivateDictByteData;
}
protected int writeFDArray(List<Integer> uniqueNewRefs, List<Integer> privateDictOffsets,
List<Integer> fontNameSIDs)
throws IOException {
int offset = currentPos;
List<FontDict> fdFonts = cffReader.getFDFonts();
List<byte[]> index = new ArrayList<byte[]>();
int i = 0;
for (int ref : uniqueNewRefs) {
FontDict fdFont = fdFonts.get(ref);
byte[] fdFontByteData = fdFont.getByteData();
Map<String, DICTEntry> fdFontDict = cffReader.parseDictData(fdFontByteData);
//Update the SID to the FontName
updateOffset(fdFontByteData, fdFontDict.get("FontName").getOffset() - 1,
fdFontDict.get("FontName").getOperandLengths().get(0),
fontNameSIDs.get(i));
//Update the Private dict reference
updateOffset(fdFontByteData, fdFontDict.get("Private").getOffset()
+ fdFontDict.get("Private").getOperandLengths().get(0),
fdFontDict.get("Private").getOperandLengths().get(1),
privateDictOffsets.get(i));
index.add(fdFontByteData);
i++;
}
writeIndex(index);
return offset;
}
private static class FDIndexReference {
private int newFDIndex;
private int oldFDIndex;
public FDIndexReference(int newFDIndex, int oldFDIndex) {
this.newFDIndex = newFDIndex;
this.oldFDIndex = oldFDIndex;
}
public int getNewFDIndex() {
return newFDIndex;
}
public int getOldFDIndex() {
return oldFDIndex;
}
}
private void createCharStringData() throws IOException {
Map<String, DICTEntry> topDICT = cffReader.getTopDictEntries();
CFFIndexData charStringsIndex = cffReader.getCharStringIndex();
DICTEntry privateEntry = topDICT.get("Private");
if (privateEntry != null) {
int privateOffset = privateEntry.getOperands().get(1).intValue();
Map<String, DICTEntry> privateDICT = cffReader.getPrivateDict(privateEntry);
if (privateDICT.get("Subrs") != null) {
int localSubrOffset = privateOffset + privateDICT.get("Subrs").getOperands().get(0).intValue();
localIndexSubr = cffReader.readIndex(localSubrOffset);
} else {
localIndexSubr = cffReader.readIndex(null);
}
}
globalIndexSubr = cffReader.getGlobalIndexSubr();
//Create the two lists which are to store the local and global subroutines
subsetLocalIndexSubr = new ArrayList<byte[]>();
subsetGlobalIndexSubr = new ArrayList<byte[]>();
//Create the new char string index
subsetCharStringsIndex = new ArrayList<byte[]>();
localUniques = new ArrayList<Integer>();
globalUniques = new ArrayList<Integer>();
Map<Integer, Integer> gidHintMaskLengths = new HashMap<Integer, Integer>();
for (int gid : subsetGlyphs.keySet()) {
type2Parser = new Type2Parser();
byte[] data = charStringsIndex.getValue(gid);
preScanForSubsetIndexSize(data);
gidHintMaskLengths.put(gid, type2Parser.getMaskLength());
}
//Store the size of each subset index and clear the unique arrays
subsetLocalSubrCount = localUniques.size();
subsetGlobalSubrCount = globalUniques.size();
localUniques.clear();
globalUniques.clear();
for (int gid : subsetGlyphs.keySet()) {
byte[] data = charStringsIndex.getValue(gid);
type2Parser = new Type2Parser();
//Retrieve modified char string data and fill local / global subroutine arrays
type2Parser.setMaskLength(gidHintMaskLengths.get(gid));
data = readCharStringData(data, subsetLocalSubrCount);
subsetCharStringsIndex.add(data);
}
}
static class Type2Parser {
/**
* logging instance
*/
protected Log log = LogFactory.getLog(Type2Parser.class);
private List<BytesNumber> stack = new ArrayList<BytesNumber>();
private int hstemCount;
private int vstemCount;
private int lastOp = -1;
private int maskLength = -1;
public void pushOperand(BytesNumber v) {
stack.add(v);
}
public BytesNumber popOperand() {
return stack.remove(stack.size() - 1);
}
public void clearStack() {
stack.clear();
}
public int[] getOperands(int numbers) {
int[] ret = new int[numbers];
while (numbers > 0) {
numbers--;
ret[numbers] = this.popOperand().getNumber();
}
return ret;
}
public void setMaskLength(int maskLength) {
this.maskLength = maskLength;
}
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;
}
private int exec(int b0, byte[] input, int curPos) throws IOException {
ByteArrayInputStream bis = new ByteArrayInputStream(input);
bis.skip(curPos + 1);
return exec(b0, bis);
}
public int exec(int b0, InputStream data) throws IOException {
int posDelta = 0;
if ((b0 >= 0 && b0 <= 27) || (b0 >= 29 && b0 <= 31)) {
if (b0 == 12) {
log.warn("May not guess the operand count correctly.");
posDelta = 1;
} else if (b0 == 1 || b0 == 18) {
// hstem(hm) operator
hstemCount += stack.size() / 2;
clearStack();
} else if (b0 == 19 || b0 == 20) {
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();
posDelta = getMaskLength();
} else if (b0 == 3 || b0 == 23) {
// vstem(hm) operator
vstemCount += stack.size() / 2;
clearStack();
}
if (b0 != 11 && b0 != 12) {
lastOp = b0;
}
} else if (b0 == 28 || (b0 >= 32 && b0 <= 255)) {
BytesNumber operand = readNumber(b0, data);
pushOperand(operand);
posDelta = operand.getNumBytes() - 1;
} else {
throw new UnsupportedOperationException("Operator:" + b0 + " is not supported");
}
return posDelta;
}
private BytesNumber readNumber(int b0, InputStream input) throws IOException {
if (b0 == 28) {
int b1 = input.read();
int b2 = input.read();
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.read();
return new BytesNumber((b0 - 247) * 256 + b1 + 108, 2);
} else if (b0 >= 251 && b0 <= 254) {
int b1 = input.read();
return new BytesNumber(-(b0 - 251) * 256 - b1 - 108, 2);
} else if (b0 == 255) {
int b1 = input.read();
int b2 = input.read();
int b3 = input.read();
int b4 = input.read();
return new BytesNumber((b1 << 24 | b2 << 16 | b3 << 8 | b4), 5);
} else {
throw new IllegalArgumentException();
}
}
}
private void preScanForSubsetIndexSize(byte[] data) throws IOException {
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 == LOCAL_SUBROUTINE && hasLocalSubroutines) {
preScanForSubsetIndexSize(localIndexSubr, localUniques);
} else if (b0 == GLOBAL_SUBROUTINE && hasGlobalSubroutines) {
preScanForSubsetIndexSize(globalIndexSubr, globalUniques);
} else {
dataPos += type2Parser.exec(b0, data, dataPos);
}
}
}
private void preScanForSubsetIndexSize(CFFIndexData indexSubr, List<Integer> uniques) throws IOException {
int subrNumber = getSubrNumber(indexSubr.getNumObjects(), type2Parser.popOperand().getNumber());
if (!uniques.contains(subrNumber) && subrNumber < indexSubr.getNumObjects()) {
uniques.add(subrNumber);
}
if (subrNumber < indexSubr.getNumObjects()) {
byte[] subr = indexSubr.getValue(subrNumber);
preScanForSubsetIndexSize(subr);
} else {
throw new IllegalArgumentException("callgsubr out of range");
}
}
private int getSubrNumber(int numSubroutines, int operand) {
int bias = getBias(numSubroutines);
return bias + operand;
}
private byte[] readCharStringData(byte[] data, int subsetLocalSubrCount) throws IOException {
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) {
BytesNumber operand = type2Parser.popOperand();
int subrNumber = getSubrNumber(localIndexSubr.getNumObjects(), operand.getNumber());
int newRef = getNewRefForReference(subrNumber, localUniques, localIndexSubr, subsetLocalIndexSubr,
subsetLocalSubrCount);
if (newRef != -1) {
byte[] newData = constructNewRefData(dataPos, data, operand, subsetLocalSubrCount,
newRef, new int[] {10});
dataPos -= data.length - newData.length;
data = newData;
}
} else if (b0 == 29 && hasGlobalSubroutines) {
BytesNumber operand = type2Parser.popOperand();
int subrNumber = getSubrNumber(globalIndexSubr.getNumObjects(), operand.getNumber());
int newRef = getNewRefForReference(subrNumber, globalUniques, globalIndexSubr, subsetGlobalIndexSubr,
subsetGlobalSubrCount);
if (newRef != -1) {
byte[] newData = constructNewRefData(dataPos, data, operand, subsetGlobalSubrCount,
newRef, new int[] {29});
dataPos -= data.length - newData.length;
data = newData;
}
} else {
dataPos += type2Parser.exec(b0, data, dataPos);
}
}
//Return the data with the modified references to our arrays
return data;
}
private int getNewRefForReference(int subrNumber, List<Integer> uniquesArray,
CFFIndexData indexSubr, List<byte[]> subsetIndexSubr, int subrCount) throws IOException {
int newRef;
if (!uniquesArray.contains(subrNumber)) {
if (subrNumber < indexSubr.getNumObjects()) {
byte[] subr = indexSubr.getValue(subrNumber);
subr = readCharStringData(subr, subrCount);
uniquesArray.add(subrNumber);
subsetIndexSubr.add(subr);
newRef = subsetIndexSubr.size() - 1;
} else {
throw new IllegalArgumentException("subrNumber out of range");
}
} else {
newRef = uniquesArray.indexOf(subrNumber);
}
return newRef;
}
private int getBias(int subrCount) {
if (subrCount < 1240) {
return 107;
} else if (subrCount < 33900) {
return 1131;
} else {
return 32768;
}
}
private byte[] constructNewRefData(int curDataPos, byte[] currentData, BytesNumber operand,
int fullSubsetIndexSize, int curSubsetIndexSize, int[] operatorCode) throws IOException {
//Create the new array with the modified reference
ByteArrayOutputStream newData = new ByteArrayOutputStream();
int startRef = curDataPos - operand.getNumBytes();
int length = operand.getNumBytes() + 1;
int newBias = getBias(fullSubsetIndexSize);
int newRef = curSubsetIndexSize - newBias;
byte[] newRefBytes = createNewRef(newRef, operatorCode, -1, false);
newData.write(currentData, 0, startRef);
newData.write(newRefBytes);
newData.write(currentData, startRef + length, currentData.length - (startRef + length));
return newData.toByteArray();
}
public static byte[] createNewRef(int newRef, int[] operatorCode, int forceLength, boolean isDict) {
ByteArrayOutputStream newRefBytes = new ByteArrayOutputStream();
if ((forceLength == -1 && newRef >= -107 && newRef <= 107) || forceLength == 1) {
//The index values are 0 indexed
newRefBytes.write(newRef + 139);
} else if ((forceLength == -1 && newRef >= -1131 && newRef <= 1131) || forceLength == 2) {
if (newRef <= -876) {
newRefBytes.write(254);
} else if (newRef <= -620) {
newRefBytes.write(253);
} else if (newRef <= -364) {
newRefBytes.write(252);
} else if (newRef <= -108) {
newRefBytes.write(251);
} else if (newRef <= 363) {
newRefBytes.write(247);
} else if (newRef <= 619) {
newRefBytes.write(248);
} else if (newRef <= 875) {
newRefBytes.write(249);
} else {
newRefBytes.write(250);
}
if (newRef > 0) {
newRefBytes.write(newRef - 108);
} else {
newRefBytes.write(-newRef - 108);
}
} else if ((forceLength == -1 && newRef >= -32768 && newRef <= 32767) || forceLength == 3) {
newRefBytes.write(28);
newRefBytes.write(newRef >> 8);
newRefBytes.write(newRef);
} else {
if (isDict) {
newRefBytes.write(29);
} else {
newRefBytes.write(255);
}
newRefBytes.write(newRef >> 24);
newRefBytes.write(newRef >> 16);
newRefBytes.write(newRef >> 8);
newRefBytes.write(newRef);
}
for (int i : operatorCode) {
newRefBytes.write(i);
}
return newRefBytes.toByteArray();
}
protected int writeIndex(List<byte[]> dataArray) {
int totLength = 1;
for (byte[] data : dataArray) {
totLength += data.length;
}
int offSize = getOffSize(totLength);
return writeIndex(dataArray, offSize);
}
protected int writeIndex(List<byte[]> dataArray, int offSize) {
int hdrTotal = 3;
//2 byte number of items
this.writeCard16(dataArray.size());
//Offset Size: 1 byte = 256, 2 bytes = 65536 etc.
//Offsets in the offset array are relative to the byte that precedes the object data.
//Therefore the first element of the offset array is always 1.
this.writeByte(offSize);
//Count the first offset 1
hdrTotal += offSize;
int total = 0;
int i = 0;
for (byte[] data : dataArray) {
hdrTotal += offSize;
int length = data.length;
switch (offSize) {
case 1:
if (i == 0) {
writeByte(1);
}
total += length;
writeByte(total + 1);
break;
case 2:
if (i == 0) {
writeCard16(1);
}
total += length;
writeCard16(total + 1);
break;
case 3:
if (i == 0) {
writeThreeByteNumber(1);
}
total += length;
writeThreeByteNumber(total + 1);
break;
case 4:
if (i == 0) {
writeULong(1);
}
total += length;
writeULong(total + 1);
break;
default:
throw new AssertionError("Offset Size was not an expected value.");
}
i++;
}
for (byte[] aDataArray : dataArray) {
writeBytes(aDataArray);
}
return hdrTotal + total;
}
private int getOffSize(int totLength) {
int offSize = 1;
if (totLength < (1 << 8)) {
offSize = 1;
} else if (totLength < (1 << 16)) {
offSize = 2;
} else if (totLength < (1 << 24)) {
offSize = 3;
} else {
offSize = 4;
}
return offSize;
}
/**
* A class used to store the last number operand and also it's size in bytes
*/
static class BytesNumber {
private int number;
private int numBytes;
public BytesNumber(int number, int numBytes) {
this.number = number;
this.numBytes = numBytes;
}
public int getNumber() {
return this.number;
}
public int getNumBytes() {
return this.numBytes;
}
public void clearNumber() {
this.number = -1;
this.numBytes = -1;
}
public String toString() {
return Integer.toString(number);
}
@Override
public boolean equals(Object entry) {
assert entry instanceof BytesNumber;
BytesNumber bnEntry = (BytesNumber)entry;
return this.number == bnEntry.getNumber()
&& this.numBytes == bnEntry.getNumBytes();
}
@Override
public int hashCode() {
int hash = 1;
hash = hash * 17 + number;
hash = hash * 31 + numBytes;
return hash;
}
}
private void writeCharsetTable(boolean cidFont) throws IOException {
if (cidFont) {
writeByte(2);
for (int entry : gidToSID.keySet()) {
if (entry == 0) {
continue;
}
writeCard16(entry);
writeCard16(gidToSID.size() - 1);
break;
}
} else {
writeByte(0);
for (int entry : gidToSID.values()) {
if (entry == 0) {
continue;
}
writeCard16(entry);
}
}
}
protected void writePrivateDict() throws IOException {
Map<String, DICTEntry> topDICT = cffReader.getTopDictEntries();
DICTEntry privateEntry = topDICT.get("Private");
if (privateEntry != null) {
writeBytes(cffReader.getPrivateDictBytes(privateEntry));
}
}
protected void updateOffsets(Offsets offsets) throws IOException {
Map<String, DICTEntry> topDICT = cffReader.getTopDictEntries();
Map<String, DICTEntry> privateDICT = null;
DICTEntry privateEntry = topDICT.get("Private");
if (privateEntry != null) {
privateDICT = cffReader.getPrivateDict(privateEntry);
}
updateFixedOffsets(topDICT, offsets);
if (privateDICT != null) {
//Private index offset in the top dict
int oldPrivateOffset = offsets.topDictData + privateEntry.getOffset();
updateOffset(oldPrivateOffset + privateEntry.getOperandLengths().get(0),
privateEntry.getOperandLengths().get(1), offsets.privateDict);
//Update the local subroutine index offset in the private dict
DICTEntry subroutines = privateDICT.get("Subrs");
if (subroutines != null) {
int oldLocalSubrOffset = offsets.privateDict + subroutines.getOffset();
updateOffset(oldLocalSubrOffset, subroutines.getOperandLength(),
(offsets.localIndex - offsets.privateDict));
}
}
}
protected void updateFixedOffsets(Map<String, DICTEntry> topDICT, Offsets offsets) throws IOException {
//Charset offset in the top dict
DICTEntry charset = topDICT.get("charset");
int oldCharsetOffset = offsets.topDictData + charset.getOffset();
updateOffset(oldCharsetOffset, charset.getOperandLength(), offsets.charset);
//Char string index offset in the private dict
DICTEntry charString = topDICT.get("CharStrings");
int oldCharStringOffset = offsets.topDictData + charString.getOffset();
updateOffset(oldCharStringOffset, charString.getOperandLength(), offsets.charString);
DICTEntry encodingEntry = topDICT.get("Encoding");
if (encodingEntry != null && encodingEntry.getOperands().get(0).intValue() != 0
&& encodingEntry.getOperands().get(0).intValue() != 1) {
int oldEncodingOffset = offsets.topDictData + encodingEntry.getOffset();
updateOffset(oldEncodingOffset, encodingEntry.getOperandLength(), offsets.encoding);
}
}
protected void updateCIDOffsets(Offsets offsets) throws IOException {
Map<String, DICTEntry> topDict = cffReader.getTopDictEntries();
DICTEntry fdArrayEntry = topDict.get("FDArray");
if (fdArrayEntry != null) {
updateOffset(offsets.topDictData + fdArrayEntry.getOffset() - 1,
fdArrayEntry.getOperandLength(), offsets.fdArray);
}
DICTEntry fdSelect = topDict.get("FDSelect");
if (fdSelect != null) {
updateOffset(offsets.topDictData + fdSelect.getOffset() - 1,
fdSelect.getOperandLength(), offsets.fdSelect);
}
updateFixedOffsets(topDict, offsets);
}
private void updateOffset(int position, int length, int replacement) throws IOException {
byte[] outBytes = output.toByteArray();
updateOffset(outBytes, position, length, replacement);
output.reset();
output.write(outBytes);
}
private void updateOffset(byte[] out, int position, int length, int replacement) {
switch (length) {
case 1:
out[position] = (byte)(replacement + 139);
break;
case 2:
assert replacement <= 1131;
if (replacement <= -876) {
out[position] = (byte)254;
} else if (replacement <= -620) {
out[position] = (byte)253;
} else if (replacement <= -364) {
out[position] = (byte)252;
} else if (replacement <= -108) {
out[position] = (byte)251;
} else if (replacement <= 363) {
out[position] = (byte)247;
} else if (replacement <= 619) {
out[position] = (byte)248;
} else if (replacement <= 875) {
out[position] = (byte)249;
} else {
out[position] = (byte)250;
}
if (replacement > 0) {
out[position + 1] = (byte)(replacement - 108);
} else {
out[position + 1] = (byte)(-replacement - 108);
}
break;
case 3:
assert replacement <= 32767;
out[position] = (byte)28;
out[position + 1] = (byte)((replacement >> 8) & 0xFF);
out[position + 2] = (byte)(replacement & 0xFF);
break;
case 5:
out[position] = (byte)29;
out[position + 1] = (byte)((replacement >> 24) & 0xFF);
out[position + 2] = (byte)((replacement >> 16) & 0xFF);
out[position + 3] = (byte)((replacement >> 8) & 0xFF);
out[position + 4] = (byte)(replacement & 0xFF);
break;
default:
}
}
/**
* Returns the parsed CFF data for the original font.
* @return The CFFDataReader contaiing the parsed data
*/
public CFFDataReader getCFFReader() {
return cffReader;
}
}