blob: 889a6bed507e8356772e6b8724196579b5a2c79f [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.
*/
package org.apache.fontbox.cff;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.pdfbox.io.RandomAccessRead;
/**
* This class represents a parser for a CFF font.
* @author Villu Ruusmann
*/
public class CFFParser
{
/**
* Log instance.
*/
private static final Logger LOG = LogManager.getLogger(CFFParser.class);
private static final String TAG_OTTO = "OTTO";
private static final String TAG_TTCF = "ttcf";
private static final String TAG_TTFONLY = "\u0000\u0001\u0000\u0000";
private String[] stringIndex = null;
private ByteSource source;
// for debugging only
private String debugFontName;
/**
* Source from which bytes may be read in the future.
*/
public interface ByteSource
{
/**
* Returns the source bytes. May be called more than once.
*
* @return the source data as byte array
* @throws IOException if the data could not be read
*/
byte[] getBytes() throws IOException;
}
/**
* Parse CFF font using byte array, also passing in a byte source for future use.
*
* @param bytes source bytes
* @param source source to re-read bytes from in the future
* @return the parsed CFF fonts
* @throws IOException If there is an error reading from the stream
*/
public List<CFFFont> parse(byte[] bytes, ByteSource source) throws IOException
{
// TODO do we need to store the source data of the font? It isn't used at all
this.source = source;
return parse(new DataInputByteArray(bytes));
}
/**
* Parse CFF font using a RandomAccessRead as input.
*
* @param randomAccessRead the source to be parsed
* @return the parsed CFF fonts
* @throws IOException If there is an error reading from the stream
*/
public List<CFFFont> parse(RandomAccessRead randomAccessRead) throws IOException
{
// TODO do we need to store the source data of the font? It isn't used at all
byte[] bytes = new byte[(int) randomAccessRead.length()];
randomAccessRead.seek(0);
int remainingBytes = bytes.length;
int amountRead;
while ((amountRead = randomAccessRead.read(bytes, bytes.length - remainingBytes,
remainingBytes)) > 0)
{
remainingBytes -= amountRead;
}
randomAccessRead.seek(0);
this.source = new CFFBytesource(bytes);
return parse(new DataInputRandomAccessRead(randomAccessRead));
}
/**
* Parse CFF font using a DataInput as input.
*
* @param input the source to be parsed
* @return the parsed CFF fonts
* @throws IOException If there is an error reading from the stream
*/
private List<CFFFont> parse(DataInput input) throws IOException
{
String firstTag = readTagName(input);
// try to determine which kind of font we have
switch (firstTag)
{
case TAG_OTTO:
input = createTaggedCFFDataInput(input);
break;
case TAG_TTCF:
throw new IOException("True Type Collection fonts are not supported.");
case TAG_TTFONLY:
throw new IOException("OpenType fonts containing a true type font are not supported.");
default:
input.setPosition(0);
break;
}
@SuppressWarnings("unused")
Header header = readHeader(input);
String[] nameIndex = readStringIndexData(input);
if (nameIndex.length == 0)
{
throw new IOException("Name index missing in CFF font");
}
byte[][] topDictIndex = readIndexData(input);
if (topDictIndex.length == 0)
{
throw new IOException("Top DICT INDEX missing in CFF font");
}
stringIndex = readStringIndexData(input);
byte[][] globalSubrIndex = readIndexData(input);
List<CFFFont> fonts = new ArrayList<>(nameIndex.length);
for (int i = 0; i < nameIndex.length; i++)
{
CFFFont font = parseFont(input, nameIndex[i], topDictIndex[i]);
font.setGlobalSubrIndex(globalSubrIndex);
font.setData(source);
fonts.add(font);
}
return fonts;
}
private DataInput createTaggedCFFDataInput(DataInput input) throws IOException
{
// this is OpenType font containing CFF data
// so find CFF tag
short numTables = input.readShort();
@SuppressWarnings({"unused", "squid:S1854"})
short searchRange = input.readShort();
@SuppressWarnings({"unused", "squid:S1854"})
short entrySelector = input.readShort();
@SuppressWarnings({"unused", "squid:S1854"})
short rangeShift = input.readShort();
for (int q = 0; q < numTables; q++)
{
String tagName = readTagName(input);
@SuppressWarnings("unused")
long checksum = readLong(input);
long offset = readLong(input);
long length = readLong(input);
if ("CFF ".equals(tagName))
{
input.setPosition((int)offset);
byte[] bytes2 = input.readBytes((int) length);
return new DataInputByteArray(bytes2);
}
}
throw new IOException("CFF tag not found in this OpenType font.");
}
private static String readTagName(DataInput input) throws IOException
{
byte[] b = input.readBytes(4);
return new String(b, StandardCharsets.ISO_8859_1);
}
private static long readLong(DataInput input) throws IOException
{
return (input.readUnsignedShort() << 16) | input.readUnsignedShort();
}
private static int readOffSize(DataInput input) throws IOException
{
int offSize = input.readUnsignedByte();
if (offSize < 1 || offSize > 4)
{
throw new IOException("Illegal (< 1 or > 4) offSize value " + offSize
+ " in CFF font at position " + (input.getPosition() - 1));
}
return offSize;
}
private static Header readHeader(DataInput input) throws IOException
{
int major = input.readUnsignedByte();
int minor = input.readUnsignedByte();
int hdrSize = input.readUnsignedByte();
int offSize = readOffSize(input);
return new Header(major, minor, hdrSize, offSize);
}
private static int[] readIndexDataOffsets(DataInput input) throws IOException
{
int count = input.readUnsignedShort();
if (count == 0)
{
return new int[0];
}
int offSize = readOffSize(input);
int[] offsets = new int[count + 1];
for (int i = 0; i <= count; i++)
{
int offset = input.readOffset(offSize);
if (offset > input.length())
{
throw new IOException("illegal offset value " + offset + " in CFF font");
}
offsets[i] = offset;
}
return offsets;
}
private static byte[][] readIndexData(DataInput input) throws IOException
{
int[] offsets = readIndexDataOffsets(input);
if (offsets.length == 0)
{
return new byte[0][];
}
int count = offsets.length-1;
byte[][] indexDataValues = new byte[count][];
for (int i = 0; i < count; i++)
{
int length = offsets[i + 1] - offsets[i];
indexDataValues[i] = input.readBytes(length);
}
return indexDataValues;
}
private static String[] readStringIndexData(DataInput input) throws IOException
{
int[] offsets = readIndexDataOffsets(input);
if (offsets.length == 0)
{
return new String[0];
}
int count = offsets.length-1;
String[] indexDataValues = new String[count];
for (int i = 0; i < count; i++)
{
int length = offsets[i + 1] - offsets[i];
if (length < 0)
{
throw new IOException("Negative index data length + " + length + " at " +
i + ": offsets[" + (i + 1) + "]=" + offsets[i + 1] +
", offsets[" + i + "]=" + offsets[i]);
}
indexDataValues[i] = new String(input.readBytes(length), StandardCharsets.ISO_8859_1);
}
return indexDataValues;
}
private static DictData readDictData(DataInput input) throws IOException
{
DictData dict = new DictData();
while (input.hasRemaining())
{
dict.add(readEntry(input));
}
return dict;
}
private static DictData readDictData(DataInput input, int offset, int dictSize)
throws IOException
{
DictData dict = new DictData();
if (dictSize > 0)
{
input.setPosition(offset);
int endPosition = offset + dictSize;
while (input.getPosition() < endPosition)
{
dict.add(readEntry(input));
}
}
return dict;
}
private static DictData.Entry readEntry(DataInput input) throws IOException
{
DictData.Entry entry = new DictData.Entry();
while (true)
{
int b0 = input.readUnsignedByte();
if (b0 >= 0 && b0 <= 21)
{
entry.operatorName = readOperator(input, b0);
break;
}
else if (b0 == 28 || b0 == 29)
{
entry.addOperand(readIntegerNumber(input, b0));
}
else if (b0 == 30)
{
entry.addOperand(readRealNumber(input));
}
else if (b0 >= 32 && b0 <= 254)
{
entry.addOperand(readIntegerNumber(input, b0));
}
else
{
throw new IOException("invalid DICT data b0 byte: " + b0);
}
}
return entry;
}
private static String readOperator(DataInput input, int b0) throws IOException
{
if (b0 == 12)
{
int b1 = input.readUnsignedByte();
return CFFOperator.getOperator(b0, b1);
}
return CFFOperator.getOperator(b0);
}
private static Integer readIntegerNumber(DataInput input, int b0) throws IOException
{
if (b0 == 28)
{
return (int) input.readShort();
}
else if (b0 == 29)
{
return input.readInt();
}
else if (b0 >= 32 && b0 <= 246)
{
return b0 - 139;
}
else if (b0 >= 247 && b0 <= 250)
{
int b1 = input.readUnsignedByte();
return (b0 - 247) * 256 + b1 + 108;
}
else if (b0 >= 251 && b0 <= 254)
{
int b1 = input.readUnsignedByte();
return -(b0 - 251) * 256 - b1 - 108;
}
else
{
throw new IllegalArgumentException();
}
}
private static Double readRealNumber(DataInput input) throws IOException
{
StringBuilder sb = new StringBuilder();
boolean done = false;
boolean exponentMissing = false;
boolean hasExponent = false;
int[] nibbles = new int[2];
while (!done)
{
int b = input.readUnsignedByte();
nibbles[0] = b / 16;
nibbles[1] = b % 16;
for (int nibble : nibbles)
{
switch (nibble)
{
case 0x0:
case 0x1:
case 0x2:
case 0x3:
case 0x4:
case 0x5:
case 0x6:
case 0x7:
case 0x8:
case 0x9:
sb.append(nibble);
exponentMissing = false;
break;
case 0xa:
sb.append('.');
break;
case 0xb:
if (hasExponent)
{
LOG.warn("duplicate 'E' ignored after {}", sb);
break;
}
sb.append('E');
exponentMissing = true;
hasExponent = true;
break;
case 0xc:
if (hasExponent)
{
LOG.warn("duplicate 'E-' ignored after {}", sb);
break;
}
sb.append("E-");
exponentMissing = true;
hasExponent = true;
break;
case 0xd:
break;
case 0xe:
sb.append('-');
break;
case 0xf:
done = true;
break;
default:
// can only be a programming error because a nibble is between 0 and F
throw new IllegalArgumentException("illegal nibble " + nibble);
}
}
}
if (exponentMissing)
{
// the exponent is missing, just append "0" to avoid an exception
// not sure if 0 is the correct value, but it seems to fit
// see PDFBOX-1522
sb.append('0');
}
if (sb.length() == 0)
{
return 0d;
}
try
{
return Double.valueOf(sb.toString());
}
catch (NumberFormatException ex)
{
throw new IOException(ex);
}
}
private CFFFont parseFont(DataInput input, String name, byte[] topDictIndex) throws IOException
{
// top dict
DataInputByteArray topDictInput = new DataInputByteArray(topDictIndex);
DictData topDict = readDictData(topDictInput);
// we don't support synthetic fonts
DictData.Entry syntheticBaseEntry = topDict.getEntry("SyntheticBase");
if (syntheticBaseEntry != null)
{
throw new IOException("Synthetic Fonts are not supported");
}
// determine if this is a Type 1-equivalent font or a CIDFont
CFFFont font;
boolean isCIDFont = topDict.getEntry("ROS") != null;
if (isCIDFont)
{
CFFCIDFont cffCIDFont = new CFFCIDFont();
DictData.Entry rosEntry = topDict.getEntry("ROS");
if (rosEntry == null || rosEntry.size() < 3)
{
throw new IOException("ROS entry must have 3 elements");
}
cffCIDFont.setRegistry(readString(rosEntry.getNumber(0).intValue()));
cffCIDFont.setOrdering(readString(rosEntry.getNumber(1).intValue()));
cffCIDFont.setSupplement(rosEntry.getNumber(2).intValue());
font = cffCIDFont;
}
else
{
font = new CFFType1Font();
}
// name
debugFontName = name;
font.setName(name);
// top dict
font.addValueToTopDict("version", getString(topDict, "version"));
font.addValueToTopDict("Notice", getString(topDict, "Notice"));
font.addValueToTopDict("Copyright", getString(topDict, "Copyright"));
font.addValueToTopDict("FullName", getString(topDict, "FullName"));
font.addValueToTopDict("FamilyName", getString(topDict, "FamilyName"));
font.addValueToTopDict("Weight", getString(topDict, "Weight"));
font.addValueToTopDict("isFixedPitch", topDict.getBoolean("isFixedPitch", false));
font.addValueToTopDict("ItalicAngle", topDict.getNumber("ItalicAngle", 0));
font.addValueToTopDict("UnderlinePosition", topDict.getNumber("UnderlinePosition", -100));
font.addValueToTopDict("UnderlineThickness", topDict.getNumber("UnderlineThickness", 50));
font.addValueToTopDict("PaintType", topDict.getNumber("PaintType", 0));
font.addValueToTopDict("CharstringType", topDict.getNumber("CharstringType", 2));
font.addValueToTopDict("FontMatrix", topDict.getArray("FontMatrix", List.of(
0.001, 0.0, 0.0, 0.001,
0.0, 0.0)));
font.addValueToTopDict("UniqueID", topDict.getNumber("UniqueID", null));
font.addValueToTopDict("FontBBox", topDict.getArray("FontBBox",
List.of(0, 0, 0, 0)));
font.addValueToTopDict("StrokeWidth", topDict.getNumber("StrokeWidth", 0));
font.addValueToTopDict("XUID", topDict.getArray("XUID", null));
// charstrings index
DictData.Entry charStringsEntry = topDict.getEntry("CharStrings");
if (charStringsEntry == null || !charStringsEntry.hasOperands())
{
throw new IOException("CharStrings is missing or empty");
}
int charStringsOffset = charStringsEntry.getNumber(0).intValue();
input.setPosition(charStringsOffset);
byte[][] charStringsIndex = readIndexData(input);
// charset
DictData.Entry charsetEntry = topDict.getEntry("charset");
CFFCharset charset;
if (charsetEntry != null && charsetEntry.hasOperands())
{
int charsetId = charsetEntry.getNumber(0).intValue();
if (!isCIDFont && charsetId == 0)
{
charset = CFFISOAdobeCharset.getInstance();
}
else if (!isCIDFont && charsetId == 1)
{
charset = CFFExpertCharset.getInstance();
}
else if (!isCIDFont && charsetId == 2)
{
charset = CFFExpertSubsetCharset.getInstance();
}
else if (charStringsIndex.length > 0)
{
input.setPosition(charsetId);
charset = readCharset(input, charStringsIndex.length, isCIDFont);
}
// that should not happen
else
{
LOG.debug("Couldn't read CharStrings index - returning empty charset instead");
charset = new EmptyCharsetType1();
}
}
else
{
if (isCIDFont)
{
// a CID font with no charset does not default to any predefined charset
charset = new EmptyCharsetCID(charStringsIndex.length);
}
else
{
charset = CFFISOAdobeCharset.getInstance();
}
}
font.setCharset(charset);
// charstrings dict
font.charStrings = charStringsIndex;
// format-specific dictionaries
if (isCIDFont)
{
// CharStrings index could be null if the index data couldn't be read
int numEntries = 0;
if (charStringsIndex.length == 0)
{
LOG.debug("Couldn't read CharStrings index - parsing CIDFontDicts with number of char strings set to 0");
}
else
{
numEntries = charStringsIndex.length;
}
parseCIDFontDicts(input, topDict, (CFFCIDFont) font, numEntries);
List<Number> privMatrix = null;
List<Map<String, Object>> fontDicts = ((CFFCIDFont) font).getFontDicts();
if (!fontDicts.isEmpty() && fontDicts.get(0).containsKey("FontMatrix"))
{
privMatrix = (List<Number>) fontDicts.get(0).get("FontMatrix");
}
// some malformed fonts have FontMatrix in their Font DICT, see PDFBOX-2495
List<Number> matrix = topDict.getArray("FontMatrix", null);
if (matrix == null)
{
if (privMatrix != null)
{
font.addValueToTopDict("FontMatrix", privMatrix);
}
else
{
// default
font.addValueToTopDict("FontMatrix", topDict.getArray("FontMatrix",
List.of(0.001, 0.0, 0.0, 0.001, 0.0, 0.0)));
}
}
else if (privMatrix != null)
{
// we have to multiply the font matrix from the top directory with the font matrix
// from the private directory. This should be done for synthetic fonts only but in
// case of PDFBOX-3579 it's needed as well to get the right scaling
concatenateMatrix(matrix, privMatrix);
}
}
else
{
parseType1Dicts(input, topDict, (CFFType1Font) font, charset);
}
return font;
}
private void concatenateMatrix(List<Number> matrixDest, List<Number> matrixConcat)
{
// concatenate matrices
// (a b 0)
// (c d 0)
// (x y 1)
double a1 = matrixDest.get(0).doubleValue();
double b1 = matrixDest.get(1).doubleValue();
double c1 = matrixDest.get(2).doubleValue();
double d1 = matrixDest.get(3).doubleValue();
double x1 = matrixDest.get(4).doubleValue();
double y1 = matrixDest.get(5).doubleValue();
double a2 = matrixConcat.get(0).doubleValue();
double b2 = matrixConcat.get(1).doubleValue();
double c2 = matrixConcat.get(2).doubleValue();
double d2 = matrixConcat.get(3).doubleValue();
double x2 = matrixConcat.get(4).doubleValue();
double y2 = matrixConcat.get(5).doubleValue();
matrixDest.set(0, a1 * a2 + b1 * c2);
matrixDest.set(1, a1 * b2 + b1 * d1);
matrixDest.set(2, c1 * a2 + d1 * c2);
matrixDest.set(3, c1 * b2 + d1 * d2);
matrixDest.set(4, x1 * a2 + y1 * c2 + x2);
matrixDest.set(5, x1 * b2 + y1 * d2 + y2);
}
/**
* Parse dictionaries specific to a CIDFont.
*/
private void parseCIDFontDicts(DataInput input, DictData topDict, CFFCIDFont font,
int nrOfcharStrings)
throws IOException
{
// In a CIDKeyed Font, the Private dictionary isn't in the Top Dict but in the Font dict
// which can be accessed by a lookup using FDArray and FDSelect
DictData.Entry fdArrayEntry = topDict.getEntry("FDArray");
if (fdArrayEntry == null || !fdArrayEntry.hasOperands())
{
throw new IOException("FDArray is missing for a CIDKeyed Font.");
}
// font dict index
int fontDictOffset = fdArrayEntry.getNumber(0).intValue();
input.setPosition(fontDictOffset);
byte[][] fdIndex = readIndexData(input);
if (fdIndex.length == 0)
{
throw new IOException("Font dict index is missing for a CIDKeyed Font");
}
List<Map<String, Object>> privateDictionaries = new LinkedList<>();
List<Map<String, Object>> fontDictionaries = new LinkedList<>();
for (byte[] bytes : fdIndex)
{
DataInputByteArray fontDictInput = new DataInputByteArray(bytes);
DictData fontDict = readDictData(fontDictInput);
// read private dict
DictData.Entry privateEntry = fontDict.getEntry("Private");
if (privateEntry == null || privateEntry.size() < 2)
{
throw new IOException("Font DICT invalid without \"Private\" entry");
}
// font dict
Map<String, Object> fontDictMap = new LinkedHashMap<>(4);
fontDictMap.put("FontName", getString(fontDict, "FontName"));
fontDictMap.put("FontType", fontDict.getNumber("FontType", 0));
fontDictMap.put("FontBBox", fontDict.getArray("FontBBox", null));
fontDictMap.put("FontMatrix", fontDict.getArray("FontMatrix", null));
// TODO OD-4 : Add here other keys
fontDictionaries.add(fontDictMap);
int privateOffset = privateEntry.getNumber(1).intValue();
int privateSize = privateEntry.getNumber(0).intValue();
DictData privateDict = readDictData(input, privateOffset, privateSize);
// populate private dict
Map<String, Object> privDict = readPrivateDict(privateDict);
privateDictionaries.add(privDict);
// local subrs
Number localSubrOffset = privateDict.getNumber("Subrs", 0);
if (localSubrOffset instanceof Integer && ((int) localSubrOffset) > 0)
{
input.setPosition(privateOffset + (int) localSubrOffset);
privDict.put("Subrs", readIndexData(input));
}
}
// font-dict (FD) select
DictData.Entry fdSelectEntry = topDict.getEntry("FDSelect");
if (fdSelectEntry == null || !fdSelectEntry.hasOperands())
{
throw new IOException("FDSelect is missing or empty");
}
int fdSelectPos = fdSelectEntry.getNumber(0).intValue();
input.setPosition(fdSelectPos);
FDSelect fdSelect = readFDSelect(input, nrOfcharStrings);
// TODO almost certainly erroneous - CIDFonts do not have a top-level private dict
// font.addValueToPrivateDict("defaultWidthX", 1000);
// font.addValueToPrivateDict("nominalWidthX", 0);
font.setFontDict(fontDictionaries);
font.setPrivDict(privateDictionaries);
font.setFdSelect(fdSelect);
}
private Map<String, Object> readPrivateDict(DictData privateDict)
{
Map<String, Object> privDict = new LinkedHashMap<>(17);
privDict.put("BlueValues", privateDict.getDelta("BlueValues", null));
privDict.put("OtherBlues", privateDict.getDelta("OtherBlues", null));
privDict.put("FamilyBlues", privateDict.getDelta("FamilyBlues", null));
privDict.put("FamilyOtherBlues", privateDict.getDelta("FamilyOtherBlues", null));
privDict.put("BlueScale", privateDict.getNumber("BlueScale", 0.039625));
privDict.put("BlueShift", privateDict.getNumber("BlueShift", 7));
privDict.put("BlueFuzz", privateDict.getNumber("BlueFuzz", 1));
privDict.put("StdHW", privateDict.getNumber("StdHW", null));
privDict.put("StdVW", privateDict.getNumber("StdVW", null));
privDict.put("StemSnapH", privateDict.getDelta("StemSnapH", null));
privDict.put("StemSnapV", privateDict.getDelta("StemSnapV", null));
privDict.put("ForceBold", privateDict.getBoolean("ForceBold", false));
privDict.put("LanguageGroup", privateDict.getNumber("LanguageGroup", 0));
privDict.put("ExpansionFactor", privateDict.getNumber("ExpansionFactor", 0.06));
privDict.put("initialRandomSeed", privateDict.getNumber("initialRandomSeed", 0));
privDict.put("defaultWidthX", privateDict.getNumber("defaultWidthX", 0));
privDict.put("nominalWidthX", privateDict.getNumber("nominalWidthX", 0));
return privDict;
}
/**
* Parse dictionaries specific to a Type 1-equivalent font.
*/
private void parseType1Dicts(DataInput input, DictData topDict, CFFType1Font font,
CFFCharset charset)
throws IOException
{
// encoding
DictData.Entry encodingEntry = topDict.getEntry("Encoding");
CFFEncoding encoding;
int encodingId = encodingEntry != null && encodingEntry.hasOperands() ?
encodingEntry.getNumber(0).intValue() : 0;
switch (encodingId)
{
case 0:
encoding = CFFStandardEncoding.getInstance();
break;
case 1:
encoding = CFFExpertEncoding.getInstance();
break;
default:
input.setPosition(encodingId);
encoding = readEncoding(input, charset);
break;
}
font.setEncoding(encoding);
// read private dict
DictData.Entry privateEntry = topDict.getEntry("Private");
if (privateEntry == null || privateEntry.size() < 2)
{
throw new IOException("Private dictionary entry missing for font " + font.getName());
}
int privateOffset = privateEntry.getNumber(1).intValue();
int privateSize = privateEntry.getNumber(0).intValue();
DictData privateDict = readDictData(input, privateOffset, privateSize);
// populate private dict
Map<String, Object> privDict = readPrivateDict(privateDict);
privDict.forEach(font::addToPrivateDict);
// local subrs
Number localSubrOffset = privateDict.getNumber("Subrs", 0);
if (localSubrOffset instanceof Integer && ((int) localSubrOffset) > 0)
{
input.setPosition(privateOffset + (int) localSubrOffset);
font.addToPrivateDict("Subrs", readIndexData(input));
}
}
private String readString(int index) throws IOException
{
if (index < 0)
{
throw new IOException("Invalid negative index when reading a string");
}
if (index <= 390)
{
return CFFStandardString.getName(index);
}
if (stringIndex != null && index - 391 < stringIndex.length)
{
return stringIndex[index - 391];
}
// technically this maps to .notdef, but we need a unique sid name
return "SID" + index;
}
private String getString(DictData dict, String name) throws IOException
{
DictData.Entry entry = dict.getEntry(name);
return entry != null && entry.hasOperands() ? readString(entry.getNumber(0).intValue()) : null;
}
private CFFEncoding readEncoding(DataInput dataInput, CFFCharset charset) throws IOException
{
int format = dataInput.readUnsignedByte();
int baseFormat = format & 0x7f;
switch (baseFormat)
{
case 0:
return readFormat0Encoding(dataInput, charset, format);
case 1:
return readFormat1Encoding(dataInput, charset, format);
default:
throw new IOException("Invalid encoding base format " + baseFormat);
}
}
private Format0Encoding readFormat0Encoding(DataInput dataInput, CFFCharset charset,
int format)
throws IOException
{
Format0Encoding encoding = new Format0Encoding(dataInput.readUnsignedByte());
encoding.add(0, 0, ".notdef");
for (int gid = 1; gid <= encoding.nCodes; gid++)
{
int code = dataInput.readUnsignedByte();
int sid = charset.getSIDForGID(gid);
encoding.add(code, sid, readString(sid));
}
if ((format & 0x80) != 0)
{
readSupplement(dataInput, encoding);
}
return encoding;
}
private Format1Encoding readFormat1Encoding(DataInput dataInput, CFFCharset charset,
int format) throws IOException
{
Format1Encoding encoding = new Format1Encoding(dataInput.readUnsignedByte());
encoding.add(0, 0, ".notdef");
int gid = 1;
for (int i = 0; i < encoding.nRanges; i++)
{
int rangeFirst = dataInput.readUnsignedByte(); // First code in range
int rangeLeft = dataInput.readUnsignedByte(); // Codes left in range (excluding first)
for (int j = 0; j <= rangeLeft; j++)
{
int sid = charset.getSIDForGID(gid);
encoding.add(rangeFirst + j, sid, readString(sid));
gid++;
}
}
if ((format & 0x80) != 0)
{
readSupplement(dataInput, encoding);
}
return encoding;
}
private void readSupplement(DataInput dataInput, CFFBuiltInEncoding encoding)
throws IOException
{
int nSups = dataInput.readUnsignedByte();
encoding.supplement = new CFFBuiltInEncoding.Supplement[nSups];
for (int i = 0; i < nSups; i++)
{
int code = dataInput.readUnsignedByte();
int sid = dataInput.readUnsignedShort();
encoding.supplement[i] = new CFFBuiltInEncoding.Supplement(code, sid, readString(sid));
encoding.add(encoding.supplement[i]);
}
}
/**
* Read the FDSelect Data according to the format.
* @param dataInput
* @param nGlyphs
* @return the FDSelect data
* @throws IOException
*/
private static FDSelect readFDSelect(DataInput dataInput, int nGlyphs) throws IOException
{
int format = dataInput.readUnsignedByte();
switch (format)
{
case 0:
return readFormat0FDSelect(dataInput, nGlyphs);
case 3:
return readFormat3FDSelect(dataInput);
default:
throw new IllegalArgumentException();
}
}
/**
* Read the Format 0 of the FDSelect data structure.
* @param dataInput
* @param nGlyphs
* @return the Format 0 of the FDSelect data
* @throws IOException
*/
private static Format0FDSelect readFormat0FDSelect(DataInput dataInput, int nGlyphs)
throws IOException
{
int[] fds = new int[nGlyphs];
for (int i = 0; i < nGlyphs; i++)
{
fds[i] = dataInput.readUnsignedByte();
}
return new Format0FDSelect(fds);
}
/**
* Read the Format 3 of the FDSelect data structure.
*
* @param dataInput
* @return the Format 3 of the FDSelect data
* @throws IOException
*/
private static Format3FDSelect readFormat3FDSelect(DataInput dataInput)
throws IOException
{
int nbRanges = dataInput.readUnsignedShort();
Range3[] range3 = new Range3[nbRanges];
for (int i = 0; i < nbRanges; i++)
{
range3[i] = new Range3(dataInput.readUnsignedShort(), dataInput.readUnsignedByte());
}
return new Format3FDSelect(range3, dataInput.readUnsignedShort());
}
/**
* Format 3 FDSelect data.
*/
private static final class Format3FDSelect implements FDSelect
{
private final Range3[] range3;
private final int sentinel;
private Format3FDSelect(Range3[] range3, int sentinel)
{
this.range3 = range3;
this.sentinel = sentinel;
}
@Override
public int getFDIndex(int gid)
{
for (int i = 0; i < range3.length; ++i)
{
if (range3[i].first <= gid)
{
if (i + 1 < range3.length)
{
if (range3[i + 1].first > gid)
{
return range3[i].fd;
}
// go to next range
}
else
{
// last range reach, the sentinel must be greater than gid
if (sentinel > gid)
{
return range3[i].fd;
}
return -1;
}
}
}
return 0;
}
@Override
public String toString()
{
return getClass().getName() + "[nbRanges=" + range3.length + ", range3="
+ Arrays.toString(range3) + " sentinel=" + sentinel + "]";
}
}
/**
* Structure of a Range3 element.
*/
private static final class Range3
{
private final int first;
private final int fd;
private Range3(int first, int fd)
{
this.first = first;
this.fd = fd;
}
@Override
public String toString()
{
return getClass().getName() + "[first=" + first + ", fd=" + fd + "]";
}
}
/**
* Format 0 FDSelect.
*/
private static class Format0FDSelect implements FDSelect
{
private final int[] fds;
private Format0FDSelect(int[] fds)
{
this.fds = fds;
}
@Override
public int getFDIndex(int gid)
{
if (gid < fds.length)
{
return fds[gid];
}
return 0;
}
@Override
public String toString()
{
return getClass().getName() + "[fds=" + Arrays.toString(fds) + "]";
}
}
private CFFCharset readCharset(DataInput dataInput, int nGlyphs, boolean isCIDFont)
throws IOException
{
int format = dataInput.readUnsignedByte();
switch (format)
{
case 0:
return readFormat0Charset(dataInput, nGlyphs, isCIDFont);
case 1:
return readFormat1Charset(dataInput, nGlyphs, isCIDFont);
case 2:
return readFormat2Charset(dataInput, nGlyphs, isCIDFont);
default:
// we can't return new EmptyCharset(0), because this will bring more mayhem
throw new IOException("Incorrect charset format " + format);
}
}
private Format0Charset readFormat0Charset(DataInput dataInput, int nGlyphs,
boolean isCIDFont) throws IOException
{
Format0Charset charset = new Format0Charset(isCIDFont);
if (isCIDFont)
{
charset.addCID(0, 0);
for (int gid = 1; gid < nGlyphs; gid++)
{
charset.addCID(gid, dataInput.readUnsignedShort());
}
}
else
{
charset.addSID(0, 0, ".notdef");
for (int gid = 1; gid < nGlyphs; gid++)
{
int sid = dataInput.readUnsignedShort();
charset.addSID(gid, sid, readString(sid));
}
}
return charset;
}
private Format1Charset readFormat1Charset(DataInput dataInput, int nGlyphs,
boolean isCIDFont) throws IOException
{
Format1Charset charset = new Format1Charset(isCIDFont);
if (isCIDFont)
{
charset.addCID(0, 0);
int gid = 1;
while (gid < nGlyphs)
{
int rangeFirst = dataInput.readUnsignedShort();
int rangeLeft = dataInput.readUnsignedByte();
charset.addRangeMapping(new RangeMapping(gid, rangeFirst, rangeLeft));
gid += rangeLeft + 1;
}
}
else
{
charset.addSID(0, 0, ".notdef");
int gid = 1;
while (gid < nGlyphs)
{
int rangeFirst = dataInput.readUnsignedShort();
int rangeLeft = dataInput.readUnsignedByte() + 1;
for (int j = 0; j < rangeLeft; j++)
{
int sid = rangeFirst + j;
charset.addSID(gid + j, sid, readString(sid));
}
gid += rangeLeft;
}
}
return charset;
}
private Format2Charset readFormat2Charset(DataInput dataInput, int nGlyphs,
boolean isCIDFont) throws IOException
{
Format2Charset charset = new Format2Charset(isCIDFont);
if (isCIDFont)
{
charset.addCID(0, 0);
int gid = 1;
while (gid < nGlyphs)
{
int first = dataInput.readUnsignedShort();
int nLeft = dataInput.readUnsignedShort();
charset.addRangeMapping(new RangeMapping(gid, first, nLeft));
gid += nLeft + 1;
}
}
else
{
charset.addSID(0, 0, ".notdef");
int gid = 1;
while (gid < nGlyphs)
{
int first = dataInput.readUnsignedShort();
int nLeft = dataInput.readUnsignedShort() + 1;
for (int j = 0; j < nLeft; j++)
{
int sid = first + j;
charset.addSID(gid + j, sid, readString(sid));
}
gid += nLeft;
}
}
return charset;
}
/**
* Inner class holding the header of a CFF font.
*/
private static class Header
{
private final int major;
private final int minor;
private final int hdrSize;
private final int offSize;
private Header(int major, int minor, int hdrSize, int offSize)
{
this.major = major;
this.minor = minor;
this.hdrSize = hdrSize;
this.offSize = offSize;
}
@Override
public String toString()
{
return getClass().getName() + "[major=" + major + ", minor=" + minor + ", hdrSize=" + hdrSize
+ ", offSize=" + offSize + "]";
}
}
/**
* Inner class holding the DictData of a CFF font.
*/
private static class DictData
{
private final Map<String, Entry> entries = new HashMap<>();
public void add(Entry entry)
{
if (entry.operatorName != null)
{
entries.put(entry.operatorName, entry);
}
}
public Entry getEntry(String name)
{
return entries.get(name);
}
public Boolean getBoolean(String name, boolean defaultValue)
{
Entry entry = getEntry(name);
return entry != null && entry.hasOperands() ? entry.getBoolean(0, defaultValue) : defaultValue;
}
public List<Number> getArray(String name, List<Number> defaultValue)
{
Entry entry = getEntry(name);
return entry != null && entry.hasOperands() ? entry.getOperands() : defaultValue;
}
public Number getNumber(String name, Number defaultValue)
{
Entry entry = getEntry(name);
return entry != null && entry.hasOperands() ? entry.getNumber(0) : defaultValue;
}
public List<Number> getDelta(String name, List<Number> defaultValue)
{
Entry entry = getEntry(name);
return entry != null && entry.hasOperands() ? entry.getDelta() : defaultValue;
}
/**
* {@inheritDoc}
*/
@Override
public String toString()
{
return getClass().getName() + "[entries=" + entries + "]";
}
/**
* Inner class holding an operand of a CFF font.
*/
private static class Entry
{
private final List<Number> operands = new ArrayList<>();
private String operatorName = null;
public Number getNumber(int index)
{
return operands.get(index);
}
public int size()
{
return operands.size();
}
public Boolean getBoolean(int index, Boolean defaultValue)
{
Number operand = operands.get(index);
if (operand instanceof Integer)
{
switch (operand.intValue())
{
case 0:
return Boolean.FALSE;
case 1:
return Boolean.TRUE;
default:
break;
}
}
LOG.warn("Expected boolean, got {}, returning default {}", operand, defaultValue);
return defaultValue;
}
public void addOperand(Number operand)
{
operands.add(operand);
}
public boolean hasOperands()
{
return !operands.isEmpty();
}
public List<Number> getOperands()
{
return operands;
}
public List<Number> getDelta()
{
List<Number> result = new ArrayList<>(operands);
for (int i = 1; i < result.size(); i++)
{
Number previous = result.get(i - 1);
Number current = result.get(i);
int sum = previous.intValue() + current.intValue();
result.set(i, sum);
}
return result;
}
@Override
public String toString()
{
return getClass().getName() + "[operands=" + operands + ", operator=" + operatorName
+ "]";
}
}
}
/**
* Inner class representing a font's built-in CFF encoding.
*/
abstract static class CFFBuiltInEncoding extends CFFEncoding
{
private Supplement[] supplement;
/**
* Inner class representing a supplement for an encoding.
*/
private static class Supplement
{
private final int code;
private final int sid;
private final String name;
private Supplement(int code, int sid, String name)
{
this.code = code;
this.sid = sid;
this.name = name;
}
@Override
public String toString()
{
return getClass().getName() + "[code=" + code + ", sid=" + sid + "]";
}
}
public void add(Supplement supplement)
{
add(supplement.code, supplement.sid, supplement.name);
}
}
/**
* Inner class representing a Format0 encoding.
*/
private static class Format0Encoding extends CFFBuiltInEncoding
{
private final int nCodes;
private Format0Encoding(int nCodes)
{
this.nCodes = nCodes;
}
@Override
public String toString()
{
return getClass().getName() + "[nCodes=" + nCodes
+ ", supplement=" + Arrays.toString(super.supplement) + "]";
}
}
/**
* Inner class representing a Format1 encoding.
*/
private static class Format1Encoding extends CFFBuiltInEncoding
{
private final int nRanges;
private Format1Encoding(int nRanges)
{
this.nRanges = nRanges;
}
@Override
public String toString()
{
return getClass().getName() + "[nRanges=" + nRanges
+ ", supplement=" + Arrays.toString(super.supplement) + "]";
}
}
/**
* An empty charset in a malformed CID font.
*/
private static class EmptyCharsetCID extends CFFCharsetCID
{
private EmptyCharsetCID(int numCharStrings)
{
addCID(0, 0); // .notdef
// Adobe Reader treats CID as GID, PDFBOX-2571 p11.
for (int i = 1; i <= numCharStrings; i++)
{
addCID(i, i);
}
}
@Override
public String toString()
{
return getClass().getName();
}
}
/**
* An empty charset in a malformed Type1 font.
*/
private static class EmptyCharsetType1 extends CFFCharsetType1
{
private EmptyCharsetType1()
{
addSID(0, 0, ".notdef");
}
@Override
public String toString()
{
return getClass().getName();
}
}
/**
* Inner class representing a Format0 charset.
*/
private static class Format0Charset extends EmbeddedCharset
{
private Format0Charset(boolean isCIDFont)
{
super(isCIDFont);
}
}
/**
* Inner class representing a Format1 charset.
*/
private static class Format1Charset extends EmbeddedCharset
{
private final List<RangeMapping> rangesCID2GID;
private Format1Charset(boolean isCIDFont)
{
super(isCIDFont);
rangesCID2GID = new ArrayList<>();
}
/**
* Add the given range mapping.
*
* @param rangeMapping the range mapping to be added.
*/
public void addRangeMapping(RangeMapping rangeMapping)
{
rangesCID2GID.add(rangeMapping);
}
@Override
public int getCIDForGID(int gid)
{
if (isCIDFont())
{
for (RangeMapping mapping : rangesCID2GID)
{
if (mapping.isInRange(gid))
{
return mapping.mapValue(gid);
}
}
}
return super.getCIDForGID(gid);
}
@Override
public int getGIDForCID(int cid)
{
if (isCIDFont())
{
for (RangeMapping mapping : rangesCID2GID)
{
if (mapping.isInReverseRange(cid))
{
return mapping.mapReverseValue(cid);
}
}
}
return super.getGIDForCID(cid);
}
}
/**
* Inner class representing a Format2 charset.
*/
private static class Format2Charset extends EmbeddedCharset
{
private final List<RangeMapping> rangesCID2GID;
private Format2Charset(boolean isCIDFont)
{
super(isCIDFont);
rangesCID2GID = new ArrayList<>();
}
/**
* Add the given range mapping.
*
* @param rangeMapping the range mapping to be added.
*/
public void addRangeMapping(RangeMapping rangeMapping)
{
rangesCID2GID.add(rangeMapping);
}
@Override
public int getCIDForGID(int gid)
{
for (RangeMapping mapping : rangesCID2GID)
{
if (mapping.isInRange(gid))
{
return mapping.mapValue(gid);
}
}
return super.getCIDForGID(gid);
}
@Override
public int getGIDForCID(int cid)
{
for (RangeMapping mapping : rangesCID2GID)
{
if (mapping.isInReverseRange(cid))
{
return mapping.mapReverseValue(cid);
}
}
return super.getGIDForCID(cid);
}
}
/**
* Inner class representing a rang mapping for a CID charset.
*/
private static final class RangeMapping
{
private final int startValue;
private final int endValue;
private final int startMappedValue;
private final int endMappedValue;
private RangeMapping(int startGID, int first, int nLeft)
{
this.startValue = startGID;
endValue = startValue + nLeft;
this.startMappedValue = first;
endMappedValue = startMappedValue + nLeft;
}
boolean isInRange(int value)
{
return value >= startValue && value <= endValue;
}
boolean isInReverseRange(int value)
{
return value >= startMappedValue && value <= endMappedValue;
}
int mapValue(int value)
{
return isInRange(value) ? startMappedValue + (value - startValue) : 0;
}
int mapReverseValue(int value)
{
return isInReverseRange(value) ? startValue + (value - startMappedValue) : 0;
}
@Override
public String toString()
{
return getClass().getName() + "[start value=" + startValue + ", end value=" + endValue + ", start mapped-value=" + startMappedValue + ", end mapped-value=" + endMappedValue +"]";
}
}
/**
* Allows bytes to be re-read later by CFFParser.
*/
private static class CFFBytesource implements CFFParser.ByteSource
{
private final byte[] bytes;
CFFBytesource(byte[] bytes)
{
this.bytes = bytes;
}
@Override
public byte[] getBytes() throws IOException
{
return bytes;
}
}
@Override
public String toString()
{
return getClass().getSimpleName() + "[" + debugFontName + "]";
}
}