From eda39224ef4968537639dcea8f558266fa78f435 Mon Sep 17 00:00:00 2001 From: Alexander Dyuzhev Date: Tue, 24 Dec 2024 23:43:29 +0300 Subject: [PATCH] added updated classes from Apache FOP for PDFBOX3 support, metanorma/mn2pdf#311 --- .../org/apache/fop/fonts/CFFToType1Font.java | 101 ++ .../apache/fop/fonts/cff/CFFDataReader.java | 924 ++++++++++++ .../apache/fop/fonts/cff/FOPCFFDataInput.java | 95 ++ .../apache/fop/fonts/truetype/OTFFile.java | 154 ++ .../fop/fonts/truetype/OTFSubSetFile.java | 1263 +++++++++++++++++ 5 files changed, 2537 insertions(+) create mode 100644 src/main/java/org/apache/fop/fonts/CFFToType1Font.java create mode 100644 src/main/java/org/apache/fop/fonts/cff/CFFDataReader.java create mode 100644 src/main/java/org/apache/fop/fonts/cff/FOPCFFDataInput.java create mode 100644 src/main/java/org/apache/fop/fonts/truetype/OTFFile.java create mode 100644 src/main/java/org/apache/fop/fonts/truetype/OTFSubSetFile.java diff --git a/src/main/java/org/apache/fop/fonts/CFFToType1Font.java b/src/main/java/org/apache/fop/fonts/CFFToType1Font.java new file mode 100644 index 0000000..22d23e0 --- /dev/null +++ b/src/main/java/org/apache/fop/fonts/CFFToType1Font.java @@ -0,0 +1,101 @@ +/* + * 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; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.io.IOUtils; +import org.apache.fontbox.cff.CFFFont; +import org.apache.fontbox.cff.CFFParser; +import org.apache.pdfbox.io.RandomAccessReadBuffer; + +import org.apache.fop.apps.io.InternalResourceResolver; +import org.apache.fop.fonts.type1.PFBData; +import org.apache.fop.fonts.type1.PFBParser; +import org.apache.fop.fonts.type1.Type1SubsetFile; +import org.apache.fop.render.ps.Type1FontFormatter; + +public class CFFToType1Font extends MultiByteFont { + + public CFFToType1Font(InternalResourceResolver resourceResolver, EmbeddingMode embeddingMode) { + super(resourceResolver, embeddingMode); + setEmbeddingMode(EmbeddingMode.FULL); + setFontType(FontType.TYPE1); + } + + public InputStream getInputStream() throws IOException { + return null; + } + + public List getInputStreams() throws IOException { + InputStream cff = super.getInputStream(); + return convertOTFToType1(cff); + } + + private List convertOTFToType1(InputStream in) throws IOException { + CFFFont f = new CFFParser().parse(new RandomAccessReadBuffer(IOUtils.toByteArray(in))).get(0); + List fonts = new ArrayList(); + Map glyphs = cidSet.getGlyphs(); + int i = 0; + for (Map x : splitGlyphs(glyphs)) { + String iStr = "." + i; + fonts.add(convertOTFToType1(x, f, iStr)); + i++; + } + return fonts; + } + + private List> splitGlyphs(Map glyphs) { + List> allGlyphs = new ArrayList>(); + for (Map.Entry x : glyphs.entrySet()) { + int k = x.getKey(); + int v = x.getValue(); + int pot = v / 256; + v = v % 256; + while (allGlyphs.size() < pot + 1) { + Map glyphsPerFont = new HashMap(); + glyphsPerFont.put(0, 0); + allGlyphs.add(glyphsPerFont); + } + allGlyphs.get(pot).put(k, v); + } + return allGlyphs; + } + + private InputStream convertOTFToType1(Map glyphs, CFFFont cffFont, String splitGlyphsId) + throws IOException { + byte[] type1Bytes = new Type1FontFormatter(glyphs).format(cffFont, splitGlyphsId); + PFBData pfb = new PFBParser().parsePFB(new ByteArrayInputStream(type1Bytes)); + ByteArrayOutputStream s1 = new ByteArrayOutputStream(); + s1.write(pfb.getHeaderSegment()); + ByteArrayOutputStream s2 = new ByteArrayOutputStream(); + s2.write(pfb.getEncryptedSegment()); + ByteArrayOutputStream s3 = new ByteArrayOutputStream(); + s3.write(pfb.getTrailerSegment()); + byte[] out = new Type1SubsetFile().stitchFont(s1, s2, s3); + return new ByteArrayInputStream(out); + } +} diff --git a/src/main/java/org/apache/fop/fonts/cff/CFFDataReader.java b/src/main/java/org/apache/fop/fonts/cff/CFFDataReader.java new file mode 100644 index 0000000..5f68026 --- /dev/null +++ b/src/main/java/org/apache/fop/fonts/cff/CFFDataReader.java @@ -0,0 +1,924 @@ +/* + * 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.cff; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.fontbox.cff.CFFOperator; +import org.apache.fontbox.cff.DataInputByteArray; + +import org.apache.fop.fonts.truetype.FontFileReader; +import org.apache.fop.fonts.truetype.OTFFile; + +/** + * A class to read the CFF data from an OTF CFF font file. + */ +public class CFFDataReader { + private DataInputByteArray cffData; + + private byte[] header; + private CFFIndexData nameIndex; + private CFFIndexData topDICTIndex; + private CFFIndexData stringIndex; + private CFFIndexData charStringIndex; + private CFFIndexData globalIndexSubr; + private CFFIndexData localIndexSubr; + private CustomEncoding encoding; + private FDSelect fdSelect; + private List fdFonts; + + private static final int DOUBLE_BYTE_OPERATOR = 12; + private static final int NUM_STANDARD_STRINGS = 391; + + /** Commonly used parsed dictionaries */ + private LinkedHashMap topDict; + + public CFFDataReader() { + + } + + /** + * Constructor for the CFF data reader which accepts the CFF byte data + * as an argument. + * @param cffDataArray A byte array which holds the CFF data + */ + public CFFDataReader(byte[] cffDataArray) throws IOException { + cffData = new FOPCFFDataInput(cffDataArray); + readCFFData(); + } + + /** + * Constructor for the CFF data reader which accepts a FontFileReader object + * which points to the original font file as an argument. + * @param fontFile The font file as represented by a FontFileReader object + */ + public CFFDataReader(FontFileReader fontFile) throws IOException { + cffData = new FOPCFFDataInput(OTFFile.getCFFData(fontFile)); + readCFFData(); + } + + private void readCFFData() throws IOException { + header = readHeader(); + nameIndex = readIndex(); + topDICTIndex = readIndex(); + topDict = parseDictData(topDICTIndex.getData()); + stringIndex = readIndex(); + globalIndexSubr = readIndex(); + charStringIndex = readCharStringIndex(); + encoding = readEncoding(); + fdSelect = readFDSelect(); + localIndexSubr = readLocalIndexSubrs(); + fdFonts = parseCIDData(); + } + + public Map getPrivateDict(DICTEntry privateEntry) throws IOException { + return parseDictData(getPrivateDictBytes(privateEntry)); + } + + public byte[] getPrivateDictBytes(DICTEntry privateEntry) throws IOException { + int privateLength = privateEntry.getOperands().get(0).intValue(); + int privateOffset = privateEntry.getOperands().get(1).intValue(); + return getCFFOffsetBytes(privateOffset, privateLength); + } + + /** + * Retrieves a number of bytes from the CFF data stream + * @param offset The offset of the bytes to retrieve + * @param length The number of bytes to retrieve + * @return Returns a byte array of requested bytes + * @throws IOException Throws an IO Exception if an error occurs + */ + private byte[] getCFFOffsetBytes(int offset, int length) throws IOException { + cffData.setPosition(offset); + return cffData.readBytes(length); + } + + /** + * Parses the dictionary data and returns a map of objects for each entry + * @param dictData The data for the dictionary data + * @return Returns a map of type DICTEntry identified by the operand name + * @throws IOException Throws an IO Exception if an error occurs + */ + public LinkedHashMap parseDictData(byte[] dictData) throws IOException { + LinkedHashMap dictEntries = new LinkedHashMap(); + List operands = new ArrayList(); + List operandLengths = new ArrayList(); + int lastOperandLength = 0; + for (int i = 0; i < dictData.length; i++) { + int readByte = dictData[i] & 0xFF; + if (readByte < 28) { + int[] operator = new int[(readByte == DOUBLE_BYTE_OPERATOR) ? 2 : 1]; + if (readByte == DOUBLE_BYTE_OPERATOR) { + operator[0] = dictData[i]; + operator[1] = dictData[i + 1]; + i++; + } else { + operator[0] = dictData[i]; + } + String operatorName = ""; + if (operator.length > 1) { + operatorName = CFFOperator.getOperator(operator[0], operator[1]); + } else { + operatorName = CFFOperator.getOperator(operator[0]); + } + DICTEntry newEntry = new DICTEntry(); + newEntry.setOperator(operator); + newEntry.setOperands(new ArrayList(operands)); + newEntry.setOperatorName(operatorName); + newEntry.setOffset(i - lastOperandLength); + newEntry.setOperandLength(lastOperandLength); + newEntry.setOperandLengths(new ArrayList(operandLengths)); + byte[] byteData = new byte[lastOperandLength + operator.length]; + System.arraycopy(dictData, i - operator.length - (lastOperandLength - 1), + byteData, 0, operator.length + lastOperandLength); + newEntry.setByteData(byteData); + dictEntries.put(operatorName, newEntry); + operands.clear(); + operandLengths.clear(); + lastOperandLength = 0; + } else { + if (readByte >= 32 && readByte <= 246) { + operands.add(readByte - 139); + lastOperandLength += 1; + operandLengths.add(1); + } else if (readByte >= 247 && readByte <= 250) { + operands.add((readByte - 247) * 256 + (dictData[i + 1] & 0xFF) + 108); + lastOperandLength += 2; + operandLengths.add(2); + i++; + } else if (readByte >= 251 && readByte <= 254) { + operands.add(-(readByte - 251) * 256 - (dictData[i + 1] & 0xFF) - 108); + lastOperandLength += 2; + operandLengths.add(2); + i++; + } else if (readByte == 28) { + operands.add((dictData[i + 1] & 0xFF) << 8 | (dictData[i + 2] & 0xFF)); + lastOperandLength += 3; + operandLengths.add(3); + i += 2; + } else if (readByte == 29) { + operands.add((dictData[i + 1] & 0xFF) << 24 | (dictData[i + 2] & 0xFF) << 16 + | (dictData[i + 3] & 0xFF) << 8 | (dictData[i + 4] & 0xFF)); + lastOperandLength += 5; + operandLengths.add(5); + i += 4; + } else if (readByte == 30) { + boolean terminatorFound = false; + StringBuilder realNumber = new StringBuilder(); + int byteCount = 1; + do { + byte nibblesByte = dictData[++i]; + byteCount++; + terminatorFound = readNibble(realNumber, (nibblesByte >> 4) & 0x0F); + if (!terminatorFound) { + terminatorFound = readNibble(realNumber, nibblesByte & 0x0F); + } + } while (!terminatorFound); + operands.add(Double.valueOf(realNumber.toString())); + lastOperandLength += byteCount; + operandLengths.add(byteCount); + } + } + } + return dictEntries; + } + + private boolean readNibble(StringBuilder realNumber, int nibble) { + if (nibble <= 0x9) { + realNumber.append(nibble); + } else { + switch (nibble) { + case 0xa: realNumber.append("."); break; + case 0xb: realNumber.append("E"); break; + case 0xc: realNumber.append("E-"); break; + case 0xd: break; + case 0xe: realNumber.append("-"); break; + case 0xf: return true; + default: throw new AssertionError("Unexpected nibble value"); + } + } + return false; + } + + /** + * A class containing data for a dictionary entry + */ + public static class DICTEntry { + private int[] operator; + private List operands; + private List operandLengths; + private String operatorName; + private int offset; + private int operandLength; + private byte[] data = new byte[0]; + + public void setOperator(int[] operator) { + this.operator = operator; + } + + public int[] getOperator() { + return this.operator; + } + + public void setOperands(List operands) { + this.operands = operands; + } + + public List getOperands() { + return this.operands; + } + + public void setOperatorName(String operatorName) { + this.operatorName = operatorName; + } + + public String getOperatorName() { + return this.operatorName; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public int getOffset() { + return this.offset; + } + + public void setOperandLength(int operandLength) { + this.operandLength = operandLength; + } + + public int getOperandLength() { + return this.operandLength; + } + + public void setByteData(byte[] data) { + this.data = data.clone(); + } + + public byte[] getByteData() { + return data.clone(); + } + + public void setOperandLengths(List operandLengths) { + this.operandLengths = operandLengths; + } + + public List getOperandLengths() { + return operandLengths; + } + } + + private byte[] readHeader() throws IOException { + //Read known header + byte[] fixedHeader = cffData.readBytes(4); + int hdrSize = (fixedHeader[2] & 0xFF); + byte[] extra = cffData.readBytes(hdrSize - 4); + byte[] header = new byte[hdrSize]; + for (int i = 0; i < fixedHeader.length; i++) { + header[i] = fixedHeader[i]; + } + for (int i = 4; i < extra.length; i++) { + header[i] = extra[i - 4]; + } + return header; + } + + /** + * Reads a CFF index object are the specified offset position + * @param offset The position of the index object to read + * @return Returns an object representing the index + * @throws IOException Throws an IO Exception if an error occurs + */ + public CFFIndexData readIndex(int offset) throws IOException { + cffData.setPosition(offset); + return readIndex(); + } + + private CFFIndexData readIndex() throws IOException { + return readIndex(cffData); + } + + /** + * Reads an index from the current position of the CFFDataInput object + * @param input The object holding the CFF byte data + * @return Returns an object representing the index + * @throws IOException Throws an IO Exception if an error occurs + */ + public CFFIndexData readIndex(DataInputByteArray input) throws IOException { + CFFIndexData nameIndex = new CFFIndexData(); + if (input != null) { + int origPos = input.getPosition(); + nameIndex.parseIndexHeader(input); + int tableSize = input.getPosition() - origPos; + nameIndex.setByteData(input.getPosition() - tableSize, tableSize); + } + return nameIndex; + } + + /** + * Retrieves the SID for the given GID object + * @param charsetOffset The offset of the charset data + * @param gid The GID for which to retrieve the SID + * @return Returns the SID as an integer + */ + public int getSIDFromGID(int charsetOffset, int gid) throws IOException { + if (gid == 0) { + return 0; + } + cffData.setPosition(charsetOffset); + int charsetFormat = cffData.readUnsignedByte(); + switch (charsetFormat) { + case 0: //Adjust for .notdef character + cffData.setPosition(cffData.getPosition() + (--gid * 2)); + return cffData.readUnsignedShort(); + case 1: return getSIDFromGIDFormat(gid, 1); + case 2: return getSIDFromGIDFormat(gid, 2); + default: return 0; + } + } + + private int getSIDFromGIDFormat(int gid, int format) throws IOException { + int glyphCount = 0; + while (true) { + int oldGlyphCount = glyphCount; + int start = cffData.readUnsignedShort(); + glyphCount += ((format == 1) ? cffData.readUnsignedByte() : cffData.readUnsignedShort()) + 1; + if (gid <= glyphCount) { + return start + (gid - oldGlyphCount) - 1; + } + } + } + + public byte[] getHeader() { + return header.clone(); + } + + public CFFIndexData getNameIndex() { + return nameIndex; + } + + public CFFIndexData getTopDictIndex() { + return topDICTIndex; + } + + public LinkedHashMap getTopDictEntries() { + return topDict; + } + + public CFFIndexData getStringIndex() { + return stringIndex; + } + + public CFFIndexData getGlobalIndexSubr() { + return globalIndexSubr; + } + + public CFFIndexData getLocalIndexSubr() { + return localIndexSubr; + } + + public CFFIndexData getCharStringIndex() { + return charStringIndex; + } + + public DataInputByteArray getCFFData() { + return cffData; + } + + public CustomEncoding getEncoding() { + return encoding; + } + + public FDSelect getFDSelect() { + return fdSelect; + } + + public List getFDFonts() { + return fdFonts; + } + + public DataInputByteArray getLocalSubrsForGlyph(int glyph) throws IOException { + //Subsets are currently written using a Format0 FDSelect + FDSelect fontDictionary = getFDSelect(); + if (fontDictionary instanceof Format0FDSelect) { + Format0FDSelect fdSelect = (Format0FDSelect)fontDictionary; + int found = fdSelect.getFDIndexes()[glyph]; + FontDict font = getFDFonts().get(found); + byte[] localSubrData = font.getLocalSubrData().getByteData(); + if (localSubrData != null) { + return new DataInputByteArray(localSubrData); + } else { + return null; + } + } else if (fontDictionary instanceof Format3FDSelect) { + Format3FDSelect fdSelect = (Format3FDSelect)fontDictionary; + int index = 0; + for (int first : fdSelect.getRanges().keySet()) { + if (first > glyph) { + break; + } + index++; + } + FontDict font = getFDFonts().get(index); + byte[] localSubrsData = font.getLocalSubrData().getByteData(); + if (localSubrsData != null) { + return new DataInputByteArray(localSubrsData); + } else { + return null; + } + } + return null; + } + + /** + * Parses the char string index from the CFF byte data + * @return Returns the char string index object + * @throws IOException Throws an IO Exception if an error occurs + */ + public CFFIndexData readCharStringIndex() throws IOException { + int offset = topDict.get("CharStrings").getOperands().get(0).intValue(); + cffData.setPosition(offset); + return readIndex(); + } + + private CustomEncoding readEncoding() throws IOException { + CustomEncoding foundEncoding = null; + if (topDict.get("Encoding") != null) { + int offset = topDict.get("Encoding").getOperands().get(0).intValue(); + if (offset != 0 && offset != 1) { + //No need to set the offset as we are reading the data sequentially. + int format = cffData.readUnsignedByte(); + int numEntries = cffData.readUnsignedByte(); + switch (format) { + case 0: + foundEncoding = readFormat0Encoding(format, numEntries); + break; + case 1: + foundEncoding = readFormat1Encoding(format, numEntries); + break; + default: break; + } + } + } + return foundEncoding; + } + + private Format0Encoding readFormat0Encoding(int format, int numEntries) + throws IOException { + Format0Encoding newEncoding = new Format0Encoding(); + newEncoding.setFormat(format); + newEncoding.setNumEntries(numEntries); + int[] codes = new int[numEntries]; + for (int i = 0; i < numEntries; i++) { + codes[i] = cffData.readUnsignedByte(); + } + newEncoding.setCodes(codes); + return newEncoding; + } + + private Format1Encoding readFormat1Encoding(int format, int numEntries) + throws IOException { + Format1Encoding newEncoding = new Format1Encoding(); + newEncoding.setFormat(format); + newEncoding.setNumEntries(numEntries); + Map ranges = new LinkedHashMap(); + for (int i = 0; i < numEntries; i++) { + int first = cffData.readUnsignedByte(); + int left = cffData.readUnsignedByte(); + ranges.put(first, left); + } + newEncoding.setRanges(ranges); + return newEncoding; + } + + private FDSelect readFDSelect() throws IOException { + FDSelect fdSelect = null; + DICTEntry fdSelectEntry = topDict.get("FDSelect"); + if (fdSelectEntry != null) { + int fdOffset = fdSelectEntry.getOperands().get(0).intValue(); + cffData.setPosition(fdOffset); + int format = cffData.readUnsignedByte(); + switch (format) { + case 0: + fdSelect = readFormat0FDSelect(); + break; + case 3: + fdSelect = readFormat3FDSelect(); + break; + default: + } + } + return fdSelect; + } + + private Format0FDSelect readFormat0FDSelect() throws IOException { + Format0FDSelect newFDs = new Format0FDSelect(); + newFDs.setFormat(0); + int glyphCount = charStringIndex.getNumObjects(); + int[] fds = new int[glyphCount]; + for (int i = 0; i < glyphCount; i++) { + fds[i] = cffData.readUnsignedByte(); + } + newFDs.setFDIndexes(fds); + return newFDs; + } + + private Format3FDSelect readFormat3FDSelect() throws IOException { + Format3FDSelect newFDs = new Format3FDSelect(); + newFDs.setFormat(3); + int rangeCount = cffData.readUnsignedShort(); + newFDs.setRangeCount(rangeCount); + Map ranges = new LinkedHashMap(); + for (int i = 0; i < rangeCount; i++) { + int first = cffData.readUnsignedShort(); + int fd = cffData.readUnsignedByte(); + ranges.put(first, fd); + } + newFDs.setRanges(ranges); + newFDs.setSentinelGID(cffData.readUnsignedShort()); + return newFDs; + } + + private List parseCIDData() throws IOException { + List fdFonts = new ArrayList(); + if (topDict.get("ROS") != null) { + DICTEntry fdArray = topDict.get("FDArray"); + if (fdArray != null) { + int fdIndex = fdArray.getOperands().get(0).intValue(); + CFFIndexData fontDicts = readIndex(fdIndex); + for (int i = 0; i < fontDicts.getNumObjects(); i++) { + FontDict newFontDict = new FontDict(); + + byte[] fdData = fontDicts.getValue(i); + Map fdEntries = parseDictData(fdData); + newFontDict.setByteData(fontDicts.getValuePosition(i), fontDicts.getValueLength(i)); + DICTEntry fontFDEntry = fdEntries.get("FontName"); + if (fontFDEntry != null) { + newFontDict.setFontName(getString(fontFDEntry.getOperands().get(0).intValue())); + } + DICTEntry privateFDEntry = fdEntries.get("Private"); + if (privateFDEntry != null) { + newFontDict = setFDData(privateFDEntry, newFontDict); + } + + fdFonts.add(newFontDict); + } + } + } + return fdFonts; + } + + private FontDict setFDData(DICTEntry privateFDEntry, FontDict newFontDict) throws IOException { + int privateFDLength = privateFDEntry.getOperands().get(0).intValue(); + int privateFDOffset = privateFDEntry.getOperands().get(1).intValue(); + cffData.setPosition(privateFDOffset); + byte[] privateDict = cffData.readBytes(privateFDLength); + newFontDict.setPrivateDictData(privateFDOffset, privateFDLength); + Map privateEntries = parseDictData(privateDict); + DICTEntry subroutines = privateEntries.get("Subrs"); + if (subroutines != null) { + CFFIndexData localSubrs = readIndex(privateFDOffset + + subroutines.getOperands().get(0).intValue()); + newFontDict.setLocalSubrData(localSubrs); + } else { + newFontDict.setLocalSubrData(new CFFIndexData()); + } + return newFontDict; + } + + private String getString(int sid) throws IOException { + return new String(stringIndex.getValue(sid - NUM_STANDARD_STRINGS)); + } + + private CFFIndexData readLocalIndexSubrs() throws IOException { + CFFIndexData localSubrs = null; + DICTEntry privateEntry = topDict.get("Private"); + if (privateEntry != null) { + int length = privateEntry.getOperands().get(0).intValue(); + int offset = privateEntry.getOperands().get(1).intValue(); + cffData.setPosition(offset); + byte[] privateData = cffData.readBytes(length); + Map privateDict = parseDictData(privateData); + DICTEntry localSubrsEntry = privateDict.get("Subrs"); + if (localSubrsEntry != null) { + int localOffset = offset + localSubrsEntry.getOperands().get(0).intValue(); + cffData.setPosition(localOffset); + localSubrs = readIndex(); + } + } + return localSubrs; + } + + /** + * Parent class which provides the ability to retrieve byte data from + * a sub-table. + */ + public class CFFSubTable { + private DataLocation dataLocation = new DataLocation(); + + public void setByteData(int position, int length) { + dataLocation = new DataLocation(position, length); + } + + public byte[] getByteData() throws IOException { + int oldPos = cffData.getPosition(); + try { + cffData.setPosition(dataLocation.getDataPosition()); + return cffData.readBytes(dataLocation.getDataLength()); + } finally { + cffData.setPosition(oldPos); + } + } + } + + /** + * An object used to hold index data from the CFF data + */ + public class CFFIndexData extends CFFSubTable { + private int numObjects; + private int offSize; + private int[] offsets = new int[0]; + private DataLocation dataLocation = new DataLocation(); + + public void setNumObjects(int numObjects) { + this.numObjects = numObjects; + } + + public int getNumObjects() { + return this.numObjects; + } + + public void setOffSize(int offSize) { + this.offSize = offSize; + } + + public int getOffSize() { + return this.offSize; + } + + public void setOffsets(int[] offsets) { + this.offsets = offsets.clone(); + } + + public int[] getOffsets() { + return offsets.clone(); + } + + public void setData(int position, int length) { + dataLocation = new DataLocation(position, length); + } + + public byte[] getData() throws IOException { + int origPos = cffData.getPosition(); + try { + cffData.setPosition(dataLocation.getDataPosition()); + return cffData.readBytes(dataLocation.getDataLength()); + } finally { + cffData.setPosition(origPos); + } + } + + /** + * Parses index data from an index object found within the CFF byte data + * @param cffData A byte array containing the CFF data + * @throws IOException Throws an IO Exception if an error occurs + */ + public void parseIndexHeader(DataInputByteArray cffData) throws IOException { + setNumObjects(cffData.readUnsignedShort()); + setOffSize(cffData.readUnsignedByte()); + int[] offsets = new int[getNumObjects() + 1]; + byte[] bytes; + //Fills the offsets array + for (int i = 0; i <= getNumObjects(); i++) { + switch (getOffSize()) { + case 1: + offsets[i] = cffData.readUnsignedByte(); + break; + case 2: + offsets[i] = cffData.readUnsignedShort(); + break; + case 3: + bytes = cffData.readBytes(3); + offsets[i] = ((bytes[0] & 0xFF) << 16) + ((bytes[1] & 0xFF) << 8) + (bytes[2] & 0xFF); + break; + case 4: + bytes = cffData.readBytes(4); + offsets[i] = ((bytes[0] & 0xFF) << 24) + ((bytes[1] & 0xFF) << 16) + + ((bytes[2] & 0xFF) << 8) + (bytes[3] & 0xFF); + break; + default: continue; + } + } + setOffsets(offsets); + int position = cffData.getPosition(); + int dataSize = offsets[offsets.length - 1] - offsets[0]; + + cffData.setPosition(cffData.getPosition() + dataSize); + setData(position, dataSize); + } + + /** + * Retrieves data from the index data + * @param index The index position of the data to retrieve + * @return Returns the byte data for the given index + * @throws IOException Throws an IO Exception if an error occurs + */ + public byte[] getValue(int index) throws IOException { + int oldPos = cffData.getPosition(); + try { + cffData.setPosition(dataLocation.getDataPosition() + (offsets[index] - 1)); + return cffData.readBytes(offsets[index + 1] - offsets[index]); + } finally { + cffData.setPosition(oldPos); + } + } + + public int getValuePosition(int index) { + return dataLocation.getDataPosition() + (offsets[index] - 1); + } + + public int getValueLength(int index) { + return offsets[index + 1] - offsets[index]; + } + } + + public abstract class CustomEncoding { + private int format; + private int numEntries; + + public void setFormat(int format) { + this.format = format; + } + + public int getFormat() { + return format; + } + + public void setNumEntries(int numEntries) { + this.numEntries = numEntries; + } + + public int getNumEntries() { + return numEntries; + } + } + + public class Format0Encoding extends CustomEncoding { + private int[] codes = new int[0]; + + public void setCodes(int[] codes) { + this.codes = codes.clone(); + } + + public int[] getCodes() { + return codes.clone(); + } + } + + public class Format1Encoding extends CustomEncoding { + private Map ranges; + + public void setRanges(Map ranges) { + this.ranges = ranges; + } + + public Map getRanges() { + return ranges; + } + } + + public abstract class FDSelect { + private int format; + + public void setFormat(int format) { + this.format = format; + } + + public int getFormat() { + return format; + } + } + + public class Format0FDSelect extends FDSelect { + private int[] fds = new int[0]; + + public void setFDIndexes(int[] fds) { + this.fds = fds.clone(); + } + + public int[] getFDIndexes() { + return fds.clone(); + } + } + + public class Format3FDSelect extends FDSelect { + private int rangeCount; + private Map ranges; + private int sentinelGID; + + public void setRangeCount(int rangeCount) { + this.rangeCount = rangeCount; + } + + public int getRangeCount() { + return rangeCount; + } + + public void setRanges(Map ranges) { + this.ranges = ranges; + } + + public Map getRanges() { + return ranges; + } + + public void setSentinelGID(int sentinelGID) { + this.sentinelGID = sentinelGID; + } + + public int getSentinelGID() { + return sentinelGID; + } + } + + public class FontDict extends CFFSubTable { + private String fontName; + private DataLocation dataLocation = new DataLocation(); + private CFFIndexData localSubrData; + + public void setFontName(String groupName) { + this.fontName = groupName; + } + + public String getFontName() { + return fontName; + } + + public void setPrivateDictData(int position, int length) { + dataLocation = new DataLocation(position, length); + } + + public byte[] getPrivateDictData() throws IOException { + int origPos = cffData.getPosition(); + try { + cffData.setPosition(dataLocation.getDataPosition()); + return cffData.readBytes(dataLocation.getDataLength()); + } finally { + cffData.setPosition(origPos); + } + } + + public void setLocalSubrData(CFFIndexData localSubrData) { + this.localSubrData = localSubrData; + } + + public CFFIndexData getLocalSubrData() { + return localSubrData; + } + } + + private static class DataLocation { + private int dataPosition; + private int dataLength; + + public DataLocation() { + dataPosition = 0; + dataLength = 0; + } + + public DataLocation(int position, int length) { + this.dataPosition = position; + this.dataLength = length; + } + + public int getDataPosition() { + return dataPosition; + } + + public int getDataLength() { + return dataLength; + } + } +} diff --git a/src/main/java/org/apache/fop/fonts/cff/FOPCFFDataInput.java b/src/main/java/org/apache/fop/fonts/cff/FOPCFFDataInput.java new file mode 100644 index 0000000..5202851 --- /dev/null +++ b/src/main/java/org/apache/fop/fonts/cff/FOPCFFDataInput.java @@ -0,0 +1,95 @@ +/* + * 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.cff; + +import java.io.IOException; + +import org.apache.fontbox.cff.DataInputByteArray; + +public class FOPCFFDataInput extends DataInputByteArray { + private final byte[] inputBuffer; + private int bufferPosition; + + public FOPCFFDataInput(byte[] buffer) { + super(buffer); + this.inputBuffer = buffer; + } + + public boolean hasRemaining() throws IOException { + return this.bufferPosition < this.inputBuffer.length; + } + + public int getPosition() { + return this.bufferPosition; + } + + public void setPosition(int position) throws IOException { + if (position < 0) { + throw new IOException("position is negative"); +// } else if (position >= this.inputBuffer.length) { +// throw new IOException("New position is out of range " + position + " >= " + this.inputBuffer.length); + } else { + this.bufferPosition = position; + } + } + + public byte readByte() throws IOException { + if (!this.hasRemaining()) { + throw new IOException("End off buffer reached"); + } else { + return this.inputBuffer[this.bufferPosition++]; + } + } + + public int readUnsignedByte() throws IOException { + if (!this.hasRemaining()) { + throw new IOException("End off buffer reached"); + } else { + return this.inputBuffer[this.bufferPosition++] & 255; + } + } + + public int peekUnsignedByte(int offset) throws IOException { + if (offset < 0) { + throw new IOException("offset is negative"); + } else if (this.bufferPosition + offset >= this.inputBuffer.length) { + throw new IOException("Offset position is out of range " + (this.bufferPosition + offset) + + " >= " + this.inputBuffer.length); + } else { + return this.inputBuffer[this.bufferPosition + offset] & 255; + } + } + + public byte[] readBytes(int length) throws IOException { + if (length < 0) { + throw new IOException("length is negative"); + } else if (this.inputBuffer.length - this.bufferPosition < length) { + throw new IOException("Premature end of buffer reached"); + } else { + byte[] bytes = new byte[length]; + System.arraycopy(this.inputBuffer, this.bufferPosition, bytes, 0, length); + this.bufferPosition += length; + return bytes; + } + } + + public int length() throws IOException { + return this.inputBuffer.length; + } +} diff --git a/src/main/java/org/apache/fop/fonts/truetype/OTFFile.java b/src/main/java/org/apache/fop/fonts/truetype/OTFFile.java new file mode 100644 index 0000000..befbb94 --- /dev/null +++ b/src/main/java/org/apache/fop/fonts/truetype/OTFFile.java @@ -0,0 +1,154 @@ +/* + * 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.IOException; +import java.util.List; + +import org.apache.fontbox.cff.CFFFont; +import org.apache.fontbox.cff.CFFParser; +import org.apache.fontbox.cff.CFFType1Font; +import org.apache.fontbox.cff.DataInputByteArray; +import org.apache.pdfbox.io.RandomAccessReadBuffer; + +public class OTFFile extends OpenFont { + + protected CFFFont fileFont; + + public OTFFile() throws IOException { + this(true, false); + } + + public OTFFile(boolean useKerning, boolean useAdvanced) throws IOException { + super(useKerning, useAdvanced); + checkForFontbox(); + } + + private void checkForFontbox() throws IOException { + try { + Class.forName("org.apache.fontbox.cff.CFFFont"); + } catch (ClassNotFoundException ex) { + throw new IOException("The Fontbox jar was not found in the classpath. This is " + + "required for OTF CFF ssupport."); + } + } + + @Override + protected void updateBBoxAndOffset() throws IOException { + Object bbox = fileFont.getTopDict().get("FontBBox"); + if (bbox != null) { + List bboxList = (List) bbox; + int[] bboxInt = new int[4]; + for (int i = 0; i < bboxInt.length; i++) { + bboxInt[i] = (Integer) bboxList.get(i); + } + for (OFMtxEntry o : mtxTab) { + o.setBoundingBox(bboxInt); + } + } + } + + private static class Mapping { + private int sid; + private String name; + private byte[] bytes; + + public void setSID(int sid) { + this.sid = sid; + } + + public int getSID() { + return sid; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setBytes(byte[] bytes) { + this.bytes = bytes; + } + + public byte[] getBytes() { + return bytes; + } + } + + + @Override + protected void initializeFont(FontFileReader in) throws IOException { + fontFile = in; + fontFile.seekSet(0); + CFFParser parser = new CFFParser(); + fileFont = parser.parse(new RandomAccessReadBuffer(in.getAllBytes())).get(0); + embedFontName = fileFont.getName(); + } + + protected void readName() throws IOException { + Object familyName = fileFont.getTopDict().get("FamilyName"); + if (familyName != null && !familyName.equals("")) { + familyNames.add(familyName.toString()); + fullName = familyName.toString(); + } else { + fullName = fileFont.getName(); + familyNames.add(fullName); + } + } + + /** + * Reads the CFFData from a given font file + * @param fontFile The font file being read + * @return The byte data found in the CFF table + */ + public static byte[] getCFFData(FontFileReader fontFile) throws IOException { + byte[] cff = fontFile.getAllBytes(); + DataInputByteArray input = new DataInputByteArray(fontFile.getAllBytes()); + input.readBytes(4); //OTTO + short numTables = input.readShort(); + input.readShort(); //searchRange + input.readShort(); //entrySelector + input.readShort(); //rangeShift + + for (int q = 0; q < numTables; q++) { + String tagName = new String(input.readBytes(4)); + readLong(input); //Checksum + long offset = readLong(input); + long length = readLong(input); + if (tagName.equals("CFF ")) { + cff = new byte[(int)length]; + System.arraycopy(fontFile.getAllBytes(), (int)offset, cff, 0, cff.length); + break; + } + } + return cff; + } + + private static long readLong(DataInputByteArray input) throws IOException { + return (input.readUnsignedShort() << 16) | input.readUnsignedShort(); + } + + public boolean isType1() { + return fileFont instanceof CFFType1Font; + } +} diff --git a/src/main/java/org/apache/fop/fonts/truetype/OTFSubSetFile.java b/src/main/java/org/apache/fop/fonts/truetype/OTFSubSetFile.java new file mode 100644 index 0000000..2ea2ed8 --- /dev/null +++ b/src/main/java/org/apache/fop/fonts/truetype/OTFSubSetFile.java @@ -0,0 +1,1263 @@ +/* + * 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.lang.reflect.Field; +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.Type1CharString; +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 subsetGlyphs = new LinkedHashMap(); + + /** A map of the new GID to SID used to construct the charset table **/ + protected Map gidToSID; + + protected CFFIndexData localIndexSubr; + protected CFFIndexData globalIndexSubr; + + /** List of subroutines to write to the local / global indexes in the subset font **/ + protected List subsetLocalIndexSubr; + protected List 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> 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 subsetFDSelect; + + /** A list of unique subroutines from the global / local subroutine indexes */ + protected List localUniques; + protected List 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 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 stringIndexData = new ArrayList(); + + /** 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 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 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 stack = new ArrayList(); + List type1Sequence = getType1Sequence(type2CharString); + for (Object obj : type1Sequence) { + if (obj instanceof CharStringCommand) { + if (ACCENT_CMD.equals(obj.toString())) { + 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 List getType1Sequence(Type1CharString type1CharString) { + try { + Field f = Type1CharString.class.getDeclaredField("type1Sequence"); + f.setAccessible(true); + return (List) f.get(type1CharString); + } catch (IllegalAccessException | NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + private Map sortByValue(Map map) { + List> list = new ArrayList>(map.entrySet()); + Collections.sort(list, new Comparator>() { + public int compare(Entry o1, Entry o2) { + return ((Comparable) o1.getValue()).compareTo(o2.getValue()); + } + }); + + Map result = new LinkedHashMap(); + for (Entry 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 fontNameSIDs = null; + List 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 subsetFDFonts, List fontNameSIDs) throws IOException { + List privateDictOffsets = writeCIDDictsAndSubrs(subsetFDFonts); + return writeFDArray(subsetFDFonts, privateDictOffsets, fontNameSIDs); + } + + private boolean isCharStringBeforeFD() { + LinkedHashMap 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 storeFDStrings(List uniqueNewRefs) throws IOException { + List fontNameSIDs = new ArrayList(); + List fdFonts = cffReader.getFDFonts(); + for (int uniqueNewRef : uniqueNewRefs) { + FontDict fdFont = fdFonts.get(uniqueNewRef); + byte[] fdFontByteData = fdFont.getByteData(); + Map 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 topDICT = cffReader.getTopDictEntries(); + List topDictStringEntries = Arrays.asList("version", "Notice", "Copyright", + "FullName", "FamilyName", "Weight", "PostScript"); + ByteArrayOutputStream dict = new ByteArrayOutputStream(); + int offsetExtra = 0; + for (Map.Entry 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 topDICT = cffReader.getTopDictEntries(); + int charsetOffset = topDICT.get("charset").getOperands().get(0).intValue(); + + gidToSID = new LinkedHashMap(); + + for (Entry 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 subsetGroups = new HashMap(); + + List uniqueGroups = new ArrayList(); + Map rangeMap = fdSelect.getRanges(); + Integer[] ranges = rangeMap.keySet().toArray(new Integer[rangeMap.size()]); + for (int gid : subsetGlyphs.keySet()) { + int i = 0; + for (Entry 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(); + + globalUniques = new ArrayList(); + + subsetFDSelect = new LinkedHashMap(); + + List> foundLocalUniques = new ArrayList>(); + for (int u : uniqueGroups) { + foundLocalUniques.add(new ArrayList()); + } + Map gidHintMaskLengths = new HashMap(); + for (Entry 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(); + + fdSubrs = new ArrayList>(); + subsetGlobalSubrCount = globalUniques.size(); + globalUniques.clear(); + localUniques = null; + + for (List foundLocalUnique : foundLocalUniques) { + fdSubrs.add(new ArrayList()); + } + List> foundLocalUniquesB = new ArrayList>(); + for (int u : uniqueGroups) { + foundLocalUniquesB.add(new ArrayList()); + } + for (Entry 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 indexs = getFormat3Index(); + writeByte(3); //Format + writeCard16(indexs.size()); + int count = 0; + for (Entry 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 getFormat3Index() { + Map indexs = new LinkedHashMap(); + 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 getUsedFDFonts() { + List uniqueNewRefs = new ArrayList(); + for (FDIndexReference e : subsetFDSelect.values()) { + int fdIndex = e.getOldFDIndex(); + if (!uniqueNewRefs.contains(fdIndex)) { + uniqueNewRefs.add(fdIndex); + } + } + return uniqueNewRefs; + } + + protected List writeCIDDictsAndSubrs(List uniqueNewRefs) + throws IOException { + List privateDictOffsets = new ArrayList(); + List fdFonts = cffReader.getFDFonts(); + int i = 0; + for (int ref : uniqueNewRefs) { + FontDict curFDFont = fdFonts.get(ref); + byte[] fdPrivateDictByteData = curFDFont.getPrivateDictData(); + Map 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 uniqueNewRefs, List privateDictOffsets, + List fontNameSIDs) + throws IOException { + int offset = currentPos; + List fdFonts = cffReader.getFDFonts(); + List index = new ArrayList(); + + int i = 0; + for (int ref : uniqueNewRefs) { + FontDict fdFont = fdFonts.get(ref); + byte[] fdFontByteData = fdFont.getByteData(); + Map 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 topDICT = cffReader.getTopDictEntries(); + + CFFIndexData charStringsIndex = cffReader.getCharStringIndex(); + + DICTEntry privateEntry = topDICT.get("Private"); + if (privateEntry != null) { + int privateOffset = privateEntry.getOperands().get(1).intValue(); + Map 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(); + subsetGlobalIndexSubr = new ArrayList(); + + //Create the new char string index + subsetCharStringsIndex = new ArrayList(); + + localUniques = new ArrayList(); + globalUniques = new ArrayList(); + Map gidHintMaskLengths = new HashMap(); + 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 stack = new ArrayList(); + 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 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 uniquesArray, + CFFIndexData indexSubr, List 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 dataArray) { + int totLength = 1; + for (byte[] data : dataArray) { + totLength += data.length; + } + int offSize = getOffSize(totLength); + return writeIndex(dataArray, offSize); + } + + protected int writeIndex(List 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 topDICT = cffReader.getTopDictEntries(); + + DICTEntry privateEntry = topDICT.get("Private"); + if (privateEntry != null) { + writeBytes(cffReader.getPrivateDictBytes(privateEntry)); + } + } + + protected void updateOffsets(Offsets offsets) throws IOException { + Map topDICT = cffReader.getTopDictEntries(); + Map 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 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 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; + } +}