org.apache.fontbox.cff.CFFParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of org.apache.fop Show documentation
Show all versions of org.apache.fop Show documentation
The core maven build properties
The newest version!
/*
* 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.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.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.fontbox.util.Charsets;
/**
* This class represents a parser for a CFF font.
* @author Villu Ruusmann
*/
public class CFFParser
{
/**
* Log instance.
*/
private static final Log LOG = LogFactory.getLog(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.
*/
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 parse(byte[] bytes, ByteSource source) throws IOException
{
this.source = source;
return parse(bytes);
}
/**
* Parse CFF font using a byte array as input.
*
* @param bytes the given byte array
* @return the parsed CFF fonts
* @throws IOException If there is an error reading from the stream
*/
public List parse(byte[] bytes) throws IOException
{
CFFDataInput input = new CFFDataInput(bytes);
String firstTag = readTagName(input);
// try to determine which kind of font we have
if (TAG_OTTO.equals(firstTag))
{
input = createTaggedCFFDataInput(input, bytes);
}
else if (TAG_TTCF.equals(firstTag))
{
throw new IOException("True Type Collection fonts are not supported.");
}
else if (TAG_TTFONLY.equals(firstTag))
{
throw new IOException("OpenType fonts containing a true type font are not supported.");
}
else
{
input.setPosition(0);
}
@SuppressWarnings("unused")
Header header = readHeader(input);
String[] nameIndex = readStringIndexData(input);
if (nameIndex == null)
{
throw new IOException("Name index missing in CFF font");
}
byte[][] topDictIndex = readIndexData(input);
stringIndex = readStringIndexData(input);
byte[][] globalSubrIndex = readIndexData(input);
List 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 CFFDataInput createTaggedCFFDataInput(CFFDataInput input, byte[] bytes) throws IOException
{
// this is OpenType font containing CFF data
// so find CFF tag
short numTables = input.readShort();
@SuppressWarnings("unused")
short searchRange = input.readShort();
@SuppressWarnings("unused")
short entrySelector = input.readShort();
@SuppressWarnings("unused")
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))
{
byte[] bytes2 = Arrays.copyOfRange(bytes, (int) offset, (int) (offset + length));
return new CFFDataInput(bytes2);
}
}
throw new IOException("CFF tag not found in this OpenType font.");
}
private static String readTagName(CFFDataInput input) throws IOException
{
byte[] b = input.readBytes(4);
return new String(b, Charsets.ISO_8859_1);
}
private static long readLong(CFFDataInput input) throws IOException
{
return (input.readCard16() << 16) | input.readCard16();
}
private static Header readHeader(CFFDataInput input) throws IOException
{
Header cffHeader = new Header();
cffHeader.major = input.readCard8();
cffHeader.minor = input.readCard8();
cffHeader.hdrSize = input.readCard8();
cffHeader.offSize = input.readOffSize();
return cffHeader;
}
private static int[] readIndexDataOffsets(CFFDataInput input) throws IOException
{
int count = input.readCard16();
if (count == 0)
{
return null;
}
int offSize = input.readOffSize();
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(CFFDataInput input) throws IOException
{
int[] offsets = readIndexDataOffsets(input);
if (offsets == null)
{
return null;
}
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(CFFDataInput input) throws IOException
{
int[] offsets = readIndexDataOffsets(input);
if (offsets == null)
{
return null;
}
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), Charsets.ISO_8859_1);
}
return indexDataValues;
}
private static DictData readDictData(CFFDataInput input) throws IOException
{
DictData dict = new DictData();
while (input.hasRemaining())
{
dict.add(readEntry(input));
}
return dict;
}
private static DictData readDictData(CFFDataInput input, int dictSize) throws IOException
{
DictData dict = new DictData();
int endPosition = input.getPosition() + dictSize;
while (input.getPosition() < endPosition)
{
dict.add(readEntry(input));
}
return dict;
}
private static DictData.Entry readEntry(CFFDataInput input) throws IOException
{
DictData.Entry entry = new DictData.Entry();
while (true)
{
int b0 = input.readUnsignedByte();
if (b0 >= 0 && b0 <= 21)
{
entry.operator = readOperator(input, b0);
break;
}
else if (b0 == 28 || b0 == 29)
{
entry.operands.add(readIntegerNumber(input, b0));
}
else if (b0 == 30)
{
entry.operands.add(readRealNumber(input));
}
else if (b0 >= 32 && b0 <= 254)
{
entry.operands.add(readIntegerNumber(input, b0));
}
else
{
throw new IOException("invalid DICT data b0 byte: " + b0);
}
}
return entry;
}
private static CFFOperator readOperator(CFFDataInput input, int b0) throws IOException
{
CFFOperator.Key key = readOperatorKey(input, b0);
return CFFOperator.getOperator(key);
}
private static CFFOperator.Key readOperatorKey(CFFDataInput input, int b0) throws IOException
{
if (b0 == 12)
{
int b1 = input.readUnsignedByte();
return new CFFOperator.Key(b0, b1);
}
return new CFFOperator.Key(b0);
}
private static Integer readIntegerNumber(CFFDataInput 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(CFFDataInput 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(CFFDataInput input, String name, byte[] topDictIndex) throws IOException
{
// top dict
CFFDataInput topDictInput = new CFFDataInput(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", Arrays.asList(
0.001, (double) 0, (double) 0, 0.001,
(double) 0, (double) 0)));
font.addValueToTopDict("UniqueID", topDict.getNumber("UniqueID", null));
font.addValueToTopDict("FontBBox", topDict.getArray("FontBBox",
Arrays. asList(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
{
input.setPosition(charsetId);
charset = readCharset(input, charStringsIndex.length, isCIDFont);
}
}
else
{
if (isCIDFont)
{
// a CID font with no charset does not default to any predefined charset
charset = new EmptyCharset(charStringsIndex.length);
}
else
{
charset = CFFISOAdobeCharset.getInstance();
}
}
font.setCharset(charset);
// charstrings dict
font.charStrings = charStringsIndex;
// format-specific dictionaries
if (isCIDFont)
{
parseCIDFontDicts(input, topDict, (CFFCIDFont) font, charStringsIndex.length);
List privMatrix = null;
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy