/* * $Id: CffTable.java,v 1.4 2007-07-26 11:15:06 davidsch Exp $ * * Typecast - The Font Development Environment * * Copyright (c) 2004-2007 David Schweinsberg * * Licensed 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 jogamp.graph.font.typecast.ot.table; import java.io.ByteArrayInputStream; import java.io.DataInput; import java.io.DataInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Dictionary; import java.util.Enumeration; import java.util.Hashtable; /** * Compact Font Format Table * @version $Id: CffTable.java,v 1.4 2007-07-26 11:15:06 davidsch Exp $ * @author <a href="mailto:davidsch@dev.java.net">David Schweinsberg</a> */ public class CffTable implements Table { public static class Dict { private final Dictionary<Integer, Object> _entries = new Hashtable<Integer, Object>(); private final int[] _data; private int _index; protected Dict(final int[] data, final int offset, final int length) { _data = data; _index = offset; while (_index < offset + length) { addKeyAndValueEntry(); } } public Object getValue(final int key) { return _entries.get(key); } private boolean addKeyAndValueEntry() { final ArrayList<Object> operands = new ArrayList<Object>(); Object operand = null; while (isOperandAtIndex()) { operand = nextOperand(); operands.add(operand); } int operator = _data[_index++]; if (operator == 12) { operator <<= 8; operator |= _data[_index++]; } if (operands.size() == 1) { _entries.put(operator, operand); } else { _entries.put(operator, operands); } return true; } private boolean isOperandAtIndex() { final int b0 = _data[_index]; if ((32 <= b0 && b0 <= 254) || b0 == 28 || b0 == 29 || b0 == 30) { return true; } return false; } private boolean isOperatorAtIndex() { final int b0 = _data[_index]; if (0 <= b0 && b0 <= 21) { return true; } return false; } private Object nextOperand() { final int b0 = _data[_index]; if (32 <= b0 && b0 <= 246) { // 1 byte integer ++_index; return Integer.valueOf(b0 - 139); } else if (247 <= b0 && b0 <= 250) { // 2 byte integer final int b1 = _data[_index + 1]; _index += 2; return Integer.valueOf((b0 - 247) * 256 + b1 + 108); } else if (251 <= b0 && b0 <= 254) { // 2 byte integer final int b1 = _data[_index + 1]; _index += 2; return Integer.valueOf(-(b0 - 251) * 256 - b1 - 108); } else if (b0 == 28) { // 3 byte integer final int b1 = _data[_index + 1]; final int b2 = _data[_index + 2]; _index += 3; return Integer.valueOf(b1 << 8 | b2); } else if (b0 == 29) { // 5 byte integer final int b1 = _data[_index + 1]; final int b2 = _data[_index + 2]; final int b3 = _data[_index + 3]; final int b4 = _data[_index + 4]; _index += 5; return Integer.valueOf(b1 << 24 | b2 << 16 | b3 << 8 | b4); } else if (b0 == 30) { // Real number final StringBuilder fString = new StringBuilder(); int nibble1 = 0; int nibble2 = 0; ++_index; while ((nibble1 != 0xf) && (nibble2 != 0xf)) { nibble1 = _data[_index] >> 4; nibble2 = _data[_index] & 0xf; ++_index; fString.append(decodeRealNibble(nibble1)); fString.append(decodeRealNibble(nibble2)); } return Float.valueOf(fString.toString()); } else { return null; } } private String decodeRealNibble(final int nibble) { if (nibble < 0xa) { return Integer.toString(nibble); } else if (nibble == 0xa) { return "."; } else if (nibble == 0xb) { return "E"; } else if (nibble == 0xc) { return "E-"; } else if (nibble == 0xe) { return "-"; } return ""; } @Override public String toString() { final StringBuilder sb = new StringBuilder(); final Enumeration<Integer> keys = _entries.keys(); while (keys.hasMoreElements()) { final Integer key = keys.nextElement(); if ((key.intValue() & 0xc00) == 0xc00) { sb.append("12 ").append(key.intValue() & 0xff).append(": "); } else { sb.append(key.toString()).append(": "); } sb.append(_entries.get(key).toString()).append("\n"); } return sb.toString(); } } public class Index { private final int _count; private final int _offSize; private final int[] _offset; private final int[] _data; protected Index(final DataInput di) throws IOException { _count = di.readUnsignedShort(); _offset = new int[_count + 1]; _offSize = di.readUnsignedByte(); for (int i = 0; i < _count + 1; ++i) { int thisOffset = 0; for (int j = 0; j < _offSize; ++j) { thisOffset |= di.readUnsignedByte() << ((_offSize - j - 1) * 8); } _offset[i] = thisOffset; } _data = new int[getDataLength()]; for (int i = 0; i < getDataLength(); ++i) { _data[i] = di.readUnsignedByte(); } } public int getCount() { return _count; } public int getOffset(final int index) { return _offset[index]; } public int getDataLength() { return _offset[_offset.length - 1] - 1; } public int[] getData() { return _data; } @Override public String toString() { final StringBuilder sb = new StringBuilder(); sb.append("DICT\n"); sb.append("count: ").append(_count).append("\n"); sb.append("offSize: ").append(_offSize).append("\n"); for (int i = 0; i < _count + 1; ++i) { sb.append("offset[").append(i).append("]: ").append(_offset[i]).append("\n"); } sb.append("data:"); for (int i = 0; i < _data.length; ++i) { if (i % 8 == 0) { sb.append("\n"); } else { sb.append(" "); } sb.append(_data[i]); } sb.append("\n"); return sb.toString(); } } public class TopDictIndex extends Index { protected TopDictIndex(final DataInput di) throws IOException { super(di); } public Dict getTopDict(final int index) { final int offset = getOffset(index) - 1; final int len = getOffset(index + 1) - offset - 1; return new Dict(getData(), offset, len); } @Override public String toString() { final StringBuilder sb = new StringBuilder(); for (int i = 0; i < getCount(); ++i) { sb.append(getTopDict(i).toString()).append("\n"); } return sb.toString(); } } public class NameIndex extends Index { protected NameIndex(final DataInput di) throws IOException { super(di); } public String getName(final int index) { String name = null; final int offset = getOffset(index) - 1; final int len = getOffset(index + 1) - offset - 1; // Ensure the name hasn't been deleted if (getData()[offset] != 0) { final StringBuilder sb = new StringBuilder(); for (int i = offset; i < offset + len; ++i) { sb.append((char) getData()[i]); } name = sb.toString(); } else { name = "DELETED NAME"; } return name; } @Override public String toString() { final StringBuilder sb = new StringBuilder(); for (int i = 0; i < getCount(); ++i) { sb.append(getName(i)).append("\n"); } return sb.toString(); } } public class StringIndex extends Index { protected StringIndex(final DataInput di) throws IOException { super(di); } public String getString(int index) { if (index < CffStandardStrings.standardStrings.length) { return CffStandardStrings.standardStrings[index]; } else { index -= CffStandardStrings.standardStrings.length; if (index >= getCount()) { return null; } final int offset = getOffset(index) - 1; final int len = getOffset(index + 1) - offset - 1; final StringBuilder sb = new StringBuilder(); for (int i = offset; i < offset + len; ++i) { sb.append((char) getData()[i]); } return sb.toString(); } } @Override public String toString() { final int nonStandardBase = CffStandardStrings.standardStrings.length; final StringBuilder sb = new StringBuilder(); for (int i = 0; i < getCount(); ++i) { sb.append(nonStandardBase + i).append(": "); sb.append(getString(nonStandardBase + i)).append("\n"); } return sb.toString(); } } private class CharsetRange { private int _first; private int _left; public int getFirst() { return _first; } protected void setFirst(final int first) { _first = first; } public int getLeft() { return _left; } protected void setLeft(final int left) { _left = left; } } private class CharsetRange1 extends CharsetRange { protected CharsetRange1(final DataInput di) throws IOException { setFirst(di.readUnsignedShort()); setLeft(di.readUnsignedByte()); } } private class CharsetRange2 extends CharsetRange { protected CharsetRange2(final DataInput di) throws IOException { setFirst(di.readUnsignedShort()); setLeft(di.readUnsignedShort()); } } private abstract class Charset { public abstract int getFormat(); public abstract int getSID(int gid); } private class CharsetFormat0 extends Charset { private final int[] _glyph; protected CharsetFormat0(final DataInput di, final int glyphCount) throws IOException { _glyph = new int[glyphCount - 1]; // minus 1 because .notdef is omitted for (int i = 0; i < glyphCount - 1; ++i) { _glyph[i] = di.readUnsignedShort(); } } @Override public int getFormat() { return 0; } @Override public int getSID(final int gid) { if (gid == 0) { return 0; } return _glyph[gid - 1]; } } private class CharsetFormat1 extends Charset { private final ArrayList<CharsetRange> _charsetRanges = new ArrayList<CharsetRange>(); protected CharsetFormat1(final DataInput di, final int glyphCount) throws IOException { int glyphsCovered = glyphCount - 1; // minus 1 because .notdef is omitted while (glyphsCovered > 0) { final CharsetRange range = new CharsetRange1(di); _charsetRanges.add(range); glyphsCovered -= range.getLeft() + 1; } } @Override public int getFormat() { return 1; } @Override public int getSID(final int gid) { if (gid == 0) { return 0; } // Count through the ranges to find the one of interest int count = 0; for (final CharsetRange range : _charsetRanges) { count += range.getLeft(); if (gid < count) { final int sid = gid - count + range.getFirst(); return sid; } } return 0; } } private class CharsetFormat2 extends Charset { private final ArrayList<CharsetRange> _charsetRanges = new ArrayList<CharsetRange>(); protected CharsetFormat2(final DataInput di, final int glyphCount) throws IOException { int glyphsCovered = glyphCount - 1; // minus 1 because .notdef is omitted while (glyphsCovered > 0) { final CharsetRange range = new CharsetRange2(di); _charsetRanges.add(range); glyphsCovered -= range.getLeft() + 1; } } @Override public int getFormat() { return 2; } @Override public int getSID(final int gid) { if (gid == 0) { return 0; } // Count through the ranges to find the one of interest int count = 0; for (final CharsetRange range : _charsetRanges) { if (gid < range.getLeft() + count) { final int sid = gid - count + range.getFirst() - 1; return sid; } count += range.getLeft(); } return 0; } } private final DirectoryEntry _de; private final int _major; private final int _minor; private final int _hdrSize; private final int _offSize; private final NameIndex _nameIndex; private final TopDictIndex _topDictIndex; private final StringIndex _stringIndex; private final Index _globalSubrIndex; private final Index _charStringsIndexArray[]; private final Charset[] _charsets; private final Charstring[][] _charstringsArray; private final byte[] _buf; /** Creates a new instance of CffTable */ protected CffTable(final DirectoryEntry de, final DataInput di) throws IOException { _de = (DirectoryEntry) de.clone(); // Load entire table into a buffer, and create another input stream _buf = new byte[de.getLength()]; di.readFully(_buf); DataInput di2 = getDataInputForOffset(0); // Header _major = di2.readUnsignedByte(); _minor = di2.readUnsignedByte(); _hdrSize = di2.readUnsignedByte(); _offSize = di2.readUnsignedByte(); // Name INDEX di2 = getDataInputForOffset(_hdrSize); _nameIndex = new NameIndex(di2); // Top DICT INDEX _topDictIndex = new TopDictIndex(di2); // String INDEX _stringIndex = new StringIndex(di2); // Global Subr INDEX _globalSubrIndex = new Index(di2); // Encodings go here -- but since this is an OpenType font will this // not always be a CIDFont? In which case there are no encodings // within the CFF data. // Load each of the fonts _charStringsIndexArray = new Index[_topDictIndex.getCount()]; _charsets = new Charset[_topDictIndex.getCount()]; _charstringsArray = new Charstring[_topDictIndex.getCount()][]; for (int i = 0; i < _topDictIndex.getCount(); ++i) { // Charstrings INDEX // We load this before Charsets because we may need to know the number // of glyphs final Integer charStringsOffset = (Integer) _topDictIndex.getTopDict(i).getValue(17); di2 = getDataInputForOffset(charStringsOffset); _charStringsIndexArray[i] = new Index(di2); final int glyphCount = _charStringsIndexArray[i].getCount(); // Charsets final Integer charsetOffset = (Integer) _topDictIndex.getTopDict(i).getValue(15); di2 = getDataInputForOffset(charsetOffset); final int format = di2.readUnsignedByte(); switch (format) { case 0: _charsets[i] = new CharsetFormat0(di2, glyphCount); break; case 1: _charsets[i] = new CharsetFormat1(di2, glyphCount); break; case 2: _charsets[i] = new CharsetFormat2(di2, glyphCount); break; } // Create the charstrings _charstringsArray[i] = new Charstring[glyphCount]; for (int j = 0; j < glyphCount; ++j) { final int offset = _charStringsIndexArray[i].getOffset(j) - 1; final int len = _charStringsIndexArray[i].getOffset(j + 1) - offset - 1; _charstringsArray[i][j] = new CharstringType2( i, _stringIndex.getString(_charsets[i].getSID(j)), _charStringsIndexArray[i].getData(), offset, len, null, null); } } } private DataInput getDataInputForOffset(final int offset) { return new DataInputStream(new ByteArrayInputStream( _buf, offset, _de.getLength() - offset)); } public NameIndex getNameIndex() { return _nameIndex; } public Charset getCharset(final int fontIndex) { return _charsets[fontIndex]; } public Charstring getCharstring(final int fontIndex, final int gid) { return _charstringsArray[fontIndex][gid]; } public int getCharstringCount(final int fontIndex) { return _charstringsArray[fontIndex].length; } @Override public int getType() { return CFF; } @Override public String toString() { final StringBuilder sb = new StringBuilder(); sb.append("'CFF' Table - Compact Font Format\n---------------------------------\n"); sb.append("\nName INDEX\n"); sb.append(_nameIndex.toString()); sb.append("\nTop DICT INDEX\n"); sb.append(_topDictIndex.toString()); sb.append("\nString INDEX\n"); sb.append(_stringIndex.toString()); sb.append("\nGlobal Subr INDEX\n"); sb.append(_globalSubrIndex.toString()); for (int i = 0; i < _charStringsIndexArray.length; ++i) { sb.append("\nCharStrings INDEX ").append(i).append("\n"); sb.append(_charStringsIndexArray[i].toString()); } return sb.toString(); } /** * Get a directory entry for this table. This uniquely identifies the * table in collections where there may be more than one instance of a * particular table. * @return A directory entry */ @Override public DirectoryEntry getDirectoryEntry() { return _de; } }