| /* |
| * 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.Iterator; |
| import java.util.Map; |
| import java.util.Set; |
| |
| |
| /** |
| * Reads a TrueType file and generates a subset |
| * that can be used to embed a TrueType CID font. |
| * TrueType tables needed for embedded CID fonts are: |
| * "head", "hhea", "loca", "maxp", "cvt ", "prep", "glyf", "hmtx" and "fpgm". |
| * The TrueType spec can be found at the Microsoft |
| * Typography site: http://www.microsoft.com/truetype/ |
| */ |
| public class TTFSubSetFile extends TTFFile { |
| |
| private byte[] output = null; |
| private int realSize = 0; |
| private int currentPos = 0; |
| |
| /* |
| * Offsets in name table to be filled out by table. |
| * The offsets are to the checkSum field |
| */ |
| private int cvtDirOffset = 0; |
| private int fpgmDirOffset = 0; |
| private int glyfDirOffset = 0; |
| private int headDirOffset = 0; |
| private int hheaDirOffset = 0; |
| private int hmtxDirOffset = 0; |
| private int locaDirOffset = 0; |
| private int maxpDirOffset = 0; |
| private int prepDirOffset = 0; |
| |
| private int checkSumAdjustmentOffset = 0; |
| private int locaOffset = 0; |
| |
| /** |
| * Default Constructor |
| */ |
| public TTFSubSetFile() { |
| this(false, false); |
| } |
| |
| /** |
| * Constructor |
| * @param useKerning true if kerning data should be loaded |
| * @param useAdvanced true if advanced typographic tables should be loaded |
| */ |
| public TTFSubSetFile ( boolean useKerning, boolean useAdvanced ) { |
| super(useKerning, useAdvanced); |
| } |
| |
| /** |
| * Initalize the output array |
| */ |
| private void init(int size) { |
| output = new byte[size]; |
| realSize = 0; |
| currentPos = 0; |
| |
| // createDirectory() |
| } |
| |
| private int determineTableCount() { |
| int numTables = 4; //4 req'd tables: head,hhea,hmtx,maxp |
| if (isCFF()) { |
| throw new UnsupportedOperationException( |
| "OpenType fonts with CFF glyphs are not supported"); |
| } else { |
| numTables += 2; //1 req'd table: glyf,loca |
| if (hasCvt()) { |
| numTables++; |
| } |
| if (hasFpgm()) { |
| numTables++; |
| } |
| if (hasPrep()) { |
| numTables++; |
| } |
| } |
| return numTables; |
| } |
| |
| /** |
| * Create the directory table |
| */ |
| private void createDirectory() { |
| int numTables = determineTableCount(); |
| // Create the TrueType header |
| writeByte((byte)0); |
| writeByte((byte)1); |
| writeByte((byte)0); |
| writeByte((byte)0); |
| realSize += 4; |
| |
| writeUShort(numTables); |
| realSize += 2; |
| |
| // Create searchRange, entrySelector and rangeShift |
| int maxPow = maxPow2(numTables); |
| int searchRange = maxPow * 16; |
| writeUShort(searchRange); |
| realSize += 2; |
| |
| writeUShort(maxPow); |
| realSize += 2; |
| |
| writeUShort((numTables * 16) - searchRange); |
| realSize += 2; |
| |
| // Create space for the table entries |
| if (hasCvt()) { |
| writeString("cvt "); |
| cvtDirOffset = currentPos; |
| currentPos += 12; |
| realSize += 16; |
| } |
| |
| if (hasFpgm()) { |
| writeString("fpgm"); |
| fpgmDirOffset = currentPos; |
| currentPos += 12; |
| realSize += 16; |
| } |
| |
| writeString("glyf"); |
| glyfDirOffset = currentPos; |
| currentPos += 12; |
| realSize += 16; |
| |
| writeString("head"); |
| headDirOffset = currentPos; |
| currentPos += 12; |
| realSize += 16; |
| |
| writeString("hhea"); |
| hheaDirOffset = currentPos; |
| currentPos += 12; |
| realSize += 16; |
| |
| writeString("hmtx"); |
| hmtxDirOffset = currentPos; |
| currentPos += 12; |
| realSize += 16; |
| |
| writeString("loca"); |
| locaDirOffset = currentPos; |
| currentPos += 12; |
| realSize += 16; |
| |
| writeString("maxp"); |
| maxpDirOffset = currentPos; |
| currentPos += 12; |
| realSize += 16; |
| |
| if (hasPrep()) { |
| writeString("prep"); |
| prepDirOffset = currentPos; |
| currentPos += 12; |
| realSize += 16; |
| } |
| } |
| |
| |
| /** |
| * Copy the cvt table as is from original font to subset font |
| */ |
| private boolean createCvt(FontFileReader in) throws IOException { |
| TTFDirTabEntry entry = (TTFDirTabEntry)dirTabs.get("cvt "); |
| if (entry != null) { |
| pad4(); |
| seekTab(in, "cvt ", 0); |
| System.arraycopy(in.getBytes((int)entry.getOffset(), (int)entry.getLength()), |
| 0, output, currentPos, (int)entry.getLength()); |
| |
| int checksum = getCheckSum(currentPos, (int)entry.getLength()); |
| writeULong(cvtDirOffset, checksum); |
| writeULong(cvtDirOffset + 4, currentPos); |
| writeULong(cvtDirOffset + 8, (int)entry.getLength()); |
| currentPos += (int)entry.getLength(); |
| realSize += (int)entry.getLength(); |
| return true; |
| } else { |
| return false; |
| //throw new IOException("Can't find cvt table"); |
| } |
| } |
| |
| private boolean hasCvt() { |
| return dirTabs.containsKey("cvt "); |
| } |
| |
| private boolean hasFpgm() { |
| return dirTabs.containsKey("fpgm"); |
| } |
| |
| private boolean hasPrep() { |
| return dirTabs.containsKey("prep"); |
| } |
| |
| /** |
| * Copy the fpgm table as is from original font to subset font |
| */ |
| private boolean createFpgm(FontFileReader in) throws IOException { |
| TTFDirTabEntry entry = (TTFDirTabEntry)dirTabs.get("fpgm"); |
| if (entry != null) { |
| pad4(); |
| seekTab(in, "fpgm", 0); |
| System.arraycopy(in.getBytes((int)entry.getOffset(), (int)entry.getLength()), |
| 0, output, currentPos, (int)entry.getLength()); |
| int checksum = getCheckSum(currentPos, (int)entry.getLength()); |
| writeULong(fpgmDirOffset, checksum); |
| writeULong(fpgmDirOffset + 4, currentPos); |
| writeULong(fpgmDirOffset + 8, (int)entry.getLength()); |
| currentPos += (int)entry.getLength(); |
| realSize += (int)entry.getLength(); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| |
| |
| /** |
| * Create an empty loca table without updating checksum |
| */ |
| private void createLoca(int size) throws IOException { |
| pad4(); |
| locaOffset = currentPos; |
| writeULong(locaDirOffset + 4, currentPos); |
| writeULong(locaDirOffset + 8, size * 4 + 4); |
| currentPos += size * 4 + 4; |
| realSize += size * 4 + 4; |
| } |
| |
| |
| /** |
| * Copy the maxp table as is from original font to subset font |
| * and set num glyphs to size |
| */ |
| private void createMaxp(FontFileReader in, int size) throws IOException { |
| TTFDirTabEntry entry = (TTFDirTabEntry)dirTabs.get("maxp"); |
| if (entry != null) { |
| pad4(); |
| seekTab(in, "maxp", 0); |
| System.arraycopy(in.getBytes((int)entry.getOffset(), (int)entry.getLength()), |
| 0, output, currentPos, (int)entry.getLength()); |
| writeUShort(currentPos + 4, size); |
| |
| int checksum = getCheckSum(currentPos, (int)entry.getLength()); |
| writeULong(maxpDirOffset, checksum); |
| writeULong(maxpDirOffset + 4, currentPos); |
| writeULong(maxpDirOffset + 8, (int)entry.getLength()); |
| currentPos += (int)entry.getLength(); |
| realSize += (int)entry.getLength(); |
| } else { |
| throw new IOException("Can't find maxp table"); |
| } |
| } |
| |
| |
| /** |
| * Copy the prep table as is from original font to subset font |
| */ |
| private boolean createPrep(FontFileReader in) throws IOException { |
| TTFDirTabEntry entry = (TTFDirTabEntry)dirTabs.get("prep"); |
| if (entry != null) { |
| pad4(); |
| seekTab(in, "prep", 0); |
| System.arraycopy(in.getBytes((int)entry.getOffset(), (int)entry.getLength()), |
| 0, output, currentPos, (int)entry.getLength()); |
| |
| int checksum = getCheckSum(currentPos, (int)entry.getLength()); |
| writeULong(prepDirOffset, checksum); |
| writeULong(prepDirOffset + 4, currentPos); |
| writeULong(prepDirOffset + 8, (int)entry.getLength()); |
| currentPos += (int)entry.getLength(); |
| realSize += (int)entry.getLength(); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| |
| /** |
| * Copy the hhea table as is from original font to subset font |
| * and fill in size of hmtx table |
| */ |
| private void createHhea(FontFileReader in, int size) throws IOException { |
| TTFDirTabEntry entry = (TTFDirTabEntry)dirTabs.get("hhea"); |
| if (entry != null) { |
| pad4(); |
| seekTab(in, "hhea", 0); |
| System.arraycopy(in.getBytes((int)entry.getOffset(), (int)entry.getLength()), |
| 0, output, currentPos, (int)entry.getLength()); |
| writeUShort((int)entry.getLength() + currentPos - 2, size); |
| |
| int checksum = getCheckSum(currentPos, (int)entry.getLength()); |
| writeULong(hheaDirOffset, checksum); |
| writeULong(hheaDirOffset + 4, currentPos); |
| writeULong(hheaDirOffset + 8, (int)entry.getLength()); |
| currentPos += (int)entry.getLength(); |
| realSize += (int)entry.getLength(); |
| } else { |
| throw new IOException("Can't find hhea table"); |
| } |
| } |
| |
| |
| /** |
| * Copy the head table as is from original font to subset font |
| * and set indexToLocaFormat to long and set |
| * checkSumAdjustment to 0, store offset to checkSumAdjustment |
| * in checkSumAdjustmentOffset |
| */ |
| private void createHead(FontFileReader in) throws IOException { |
| TTFDirTabEntry entry = (TTFDirTabEntry)dirTabs.get("head"); |
| if (entry != null) { |
| pad4(); |
| seekTab(in, "head", 0); |
| System.arraycopy(in.getBytes((int)entry.getOffset(), (int)entry.getLength()), |
| 0, output, currentPos, (int)entry.getLength()); |
| |
| checkSumAdjustmentOffset = currentPos + 8; |
| output[currentPos + 8] = 0; // Set checkSumAdjustment to 0 |
| output[currentPos + 9] = 0; |
| output[currentPos + 10] = 0; |
| output[currentPos + 11] = 0; |
| output[currentPos + 50] = 0; // long locaformat |
| output[currentPos + 51] = 1; // long locaformat |
| |
| int checksum = getCheckSum(currentPos, (int)entry.getLength()); |
| writeULong(headDirOffset, checksum); |
| writeULong(headDirOffset + 4, currentPos); |
| writeULong(headDirOffset + 8, (int)entry.getLength()); |
| |
| currentPos += (int)entry.getLength(); |
| realSize += (int)entry.getLength(); |
| } else { |
| throw new IOException("Can't find head table"); |
| } |
| } |
| |
| |
| /** |
| * Create the glyf table and fill in loca table |
| */ |
| private void createGlyf(FontFileReader in, |
| Map glyphs) throws IOException { |
| TTFDirTabEntry entry = (TTFDirTabEntry)dirTabs.get("glyf"); |
| int size = 0; |
| int start = 0; |
| int endOffset = 0; // Store this as the last loca |
| if (entry != null) { |
| pad4(); |
| start = currentPos; |
| |
| /* Loca table must be in order by glyph index, so build |
| * an array first and then write the glyph info and |
| * location offset. |
| */ |
| int[] origIndexes = new int[glyphs.size()]; |
| |
| Iterator e = glyphs.keySet().iterator(); |
| while (e.hasNext()) { |
| Integer origIndex = (Integer)e.next(); |
| Integer subsetIndex = (Integer)glyphs.get(origIndex); |
| origIndexes[subsetIndex.intValue()] = origIndex.intValue(); |
| } |
| |
| for (int i = 0; i < origIndexes.length; i++) { |
| int glyphLength = 0; |
| int nextOffset = 0; |
| int origGlyphIndex = origIndexes[i]; |
| if (origGlyphIndex >= (mtxTab.length - 1)) { |
| nextOffset = (int)lastLoca; |
| } else { |
| nextOffset = (int)mtxTab[origGlyphIndex + 1].getOffset(); |
| } |
| glyphLength = nextOffset - (int)mtxTab[origGlyphIndex].getOffset(); |
| |
| // Copy glyph |
| System.arraycopy( |
| in.getBytes((int)entry.getOffset() + (int)mtxTab[origGlyphIndex].getOffset(), |
| glyphLength), 0, |
| output, currentPos, |
| glyphLength); |
| |
| |
| // Update loca table |
| writeULong(locaOffset + i * 4, currentPos - start); |
| if ((currentPos - start + glyphLength) > endOffset) { |
| endOffset = (currentPos - start + glyphLength); |
| } |
| |
| currentPos += glyphLength; |
| realSize += glyphLength; |
| |
| } |
| |
| size = currentPos - start; |
| |
| int checksum = getCheckSum(start, size); |
| writeULong(glyfDirOffset, checksum); |
| writeULong(glyfDirOffset + 4, start); |
| writeULong(glyfDirOffset + 8, size); |
| currentPos += 12; |
| realSize += 12; |
| |
| // Update loca checksum and last loca index |
| writeULong(locaOffset + glyphs.size() * 4, endOffset); |
| |
| checksum = getCheckSum(locaOffset, glyphs.size() * 4 + 4); |
| writeULong(locaDirOffset, checksum); |
| } else { |
| throw new IOException("Can't find glyf table"); |
| } |
| } |
| |
| |
| /** |
| * Create the hmtx table by copying metrics from original |
| * font to subset font. The glyphs Map contains an |
| * Integer key and Integer value that maps the original |
| * metric (key) to the subset metric (value) |
| */ |
| private void createHmtx(FontFileReader in, |
| Map glyphs) throws IOException { |
| TTFDirTabEntry entry = (TTFDirTabEntry)dirTabs.get("hmtx"); |
| |
| int longHorMetricSize = glyphs.size() * 2; |
| int leftSideBearingSize = glyphs.size() * 2; |
| int hmtxSize = longHorMetricSize + leftSideBearingSize; |
| |
| if (entry != null) { |
| pad4(); |
| //int offset = (int)entry.offset; |
| Iterator e = glyphs.keySet().iterator(); |
| while (e.hasNext()) { |
| Integer origIndex = (Integer)e.next(); |
| Integer subsetIndex = (Integer)glyphs.get(origIndex); |
| |
| writeUShort(currentPos + subsetIndex.intValue() * 4, |
| mtxTab[origIndex.intValue()].getWx()); |
| writeUShort(currentPos + subsetIndex.intValue() * 4 + 2, |
| mtxTab[origIndex.intValue()].getLsb()); |
| } |
| |
| int checksum = getCheckSum(currentPos, hmtxSize); |
| writeULong(hmtxDirOffset, checksum); |
| writeULong(hmtxDirOffset + 4, currentPos); |
| writeULong(hmtxDirOffset + 8, hmtxSize); |
| currentPos += hmtxSize; |
| realSize += hmtxSize; |
| } else { |
| throw new IOException("Can't find hmtx table"); |
| } |
| } |
| |
| /** |
| * Returns a subset of the original font. |
| * |
| * @param in FontFileReader to read from |
| * @param name Name to be checked for in the font file |
| * @param glyphs Map of glyphs (glyphs has old index as (Integer) key and |
| * new index as (Integer) value) |
| * @return A subset of the original font |
| * @throws IOException in case of an I/O problem |
| */ |
| public byte[] readFont(FontFileReader in, String name, |
| Map<Integer, Integer> glyphs) throws IOException { |
| |
| //Check if TrueType collection, and that the name exists in the collection |
| if (!checkTTC(in, name)) { |
| throw new IOException("Failed to read font"); |
| } |
| |
| //Copy the Map as we're going to modify it |
| Map<Integer, Integer> subsetGlyphs = new java.util.HashMap<Integer, Integer>(glyphs); |
| |
| output = new byte[in.getFileSize()]; |
| |
| readDirTabs(in); |
| readFontHeader(in); |
| getNumGlyphs(in); |
| readHorizontalHeader(in); |
| readHorizontalMetrics(in); |
| readIndexToLocation(in); |
| |
| scanGlyphs(in, subsetGlyphs); |
| |
| createDirectory(); // Create the TrueType header and directory |
| |
| createHead(in); |
| createHhea(in, subsetGlyphs.size()); // Create the hhea table |
| createHmtx(in, subsetGlyphs); // Create hmtx table |
| createMaxp(in, subsetGlyphs.size()); // copy the maxp table |
| |
| boolean optionalTableFound; |
| optionalTableFound = createCvt(in); // copy the cvt table |
| if (!optionalTableFound) { |
| // cvt is optional (used in TrueType fonts only) |
| log.debug("TrueType: ctv table not present. Skipped."); |
| } |
| |
| optionalTableFound = createFpgm(in); // copy fpgm table |
| if (!optionalTableFound) { |
| // fpgm is optional (used in TrueType fonts only) |
| log.debug("TrueType: fpgm table not present. Skipped."); |
| } |
| |
| optionalTableFound = createPrep(in); // copy prep table |
| if (!optionalTableFound) { |
| // prep is optional (used in TrueType fonts only) |
| log.debug("TrueType: prep table not present. Skipped."); |
| } |
| |
| createLoca(subsetGlyphs.size()); // create empty loca table |
| createGlyf(in, subsetGlyphs); //create glyf table and update loca table |
| |
| pad4(); |
| createCheckSumAdjustment(); |
| |
| byte[] ret = new byte[realSize]; |
| System.arraycopy(output, 0, ret, 0, realSize); |
| |
| return ret; |
| } |
| |
| private void scanGlyphs(FontFileReader in, Map<Integer, Integer> subsetGlyphs) |
| throws IOException { |
| TTFDirTabEntry glyfTableInfo = (TTFDirTabEntry) dirTabs.get("glyf"); |
| if (glyfTableInfo == null) { |
| throw new IOException("Glyf table could not be found"); |
| } |
| |
| GlyfTable glyfTable = new GlyfTable(in, mtxTab, glyfTableInfo, subsetGlyphs); |
| glyfTable.populateGlyphsWithComposites(); |
| } |
| |
| /** |
| * writes a ISO-8859-1 string at the currentPosition |
| * updates currentPosition but not realSize |
| * @return number of bytes written |
| */ |
| private int writeString(String str) { |
| int length = 0; |
| try { |
| byte[] buf = str.getBytes("ISO-8859-1"); |
| System.arraycopy(buf, 0, output, currentPos, buf.length); |
| length = buf.length; |
| currentPos += length; |
| } catch (java.io.UnsupportedEncodingException e) { |
| // This should never happen! |
| } |
| |
| return length; |
| } |
| |
| /** |
| * Appends a byte to the output array, |
| * updates currentPost but not realSize |
| */ |
| private void writeByte(byte b) { |
| output[currentPos++] = b; |
| } |
| |
| /** |
| * Appends a USHORT to the output array, |
| * updates currentPost but not realSize |
| */ |
| private void writeUShort(int s) { |
| byte b1 = (byte)((s >> 8) & 0xff); |
| byte b2 = (byte)(s & 0xff); |
| writeByte(b1); |
| writeByte(b2); |
| } |
| |
| /** |
| * Appends a USHORT to the output array, |
| * at the given position without changing currentPos |
| */ |
| private void writeUShort(int pos, int s) { |
| byte b1 = (byte)((s >> 8) & 0xff); |
| byte b2 = (byte)(s & 0xff); |
| output[pos] = b1; |
| output[pos + 1] = b2; |
| } |
| |
| /** |
| * Appends a ULONG to the output array, |
| * updates currentPos but not realSize |
| */ |
| private void writeULong(int s) { |
| byte b1 = (byte)((s >> 24) & 0xff); |
| byte b2 = (byte)((s >> 16) & 0xff); |
| byte b3 = (byte)((s >> 8) & 0xff); |
| byte b4 = (byte)(s & 0xff); |
| writeByte(b1); |
| writeByte(b2); |
| writeByte(b3); |
| writeByte(b4); |
| } |
| |
| /** |
| * Appends a ULONG to the output array, |
| * at the given position without changing currentPos |
| */ |
| private void writeULong(int pos, int s) { |
| byte b1 = (byte)((s >> 24) & 0xff); |
| byte b2 = (byte)((s >> 16) & 0xff); |
| byte b3 = (byte)((s >> 8) & 0xff); |
| byte b4 = (byte)(s & 0xff); |
| output[pos] = b1; |
| output[pos + 1] = b2; |
| output[pos + 2] = b3; |
| output[pos + 3] = b4; |
| } |
| |
| /** |
| * Read a signed short value at given position |
| */ |
| private short readShort(int pos) { |
| int ret = readUShort(pos); |
| return (short)ret; |
| } |
| |
| /** |
| * Read a unsigned short value at given position |
| */ |
| private int readUShort(int pos) { |
| int ret = output[pos]; |
| if (ret < 0) { |
| ret += 256; |
| } |
| ret = ret << 8; |
| if (output[pos + 1] < 0) { |
| ret |= output[pos + 1] + 256; |
| } else { |
| ret |= output[pos + 1]; |
| } |
| |
| return ret; |
| } |
| |
| /** |
| * Create a padding in the fontfile to align |
| * on a 4-byte boundary |
| */ |
| private void pad4() { |
| int padSize = currentPos % 4; |
| for (int i = 0; i < padSize; i++) { |
| output[currentPos++] = 0; |
| realSize++; |
| } |
| } |
| |
| /** |
| * Returns the maximum power of 2 <= max |
| */ |
| private int maxPow2(int max) { |
| int i = 0; |
| while (Math.pow(2, i) < max) { |
| i++; |
| } |
| |
| return (i - 1); |
| } |
| |
| private int log2(int num) { |
| return (int)(Math.log(num) / Math.log(2)); |
| } |
| |
| |
| private int getCheckSum(int start, int size) { |
| return (int)getLongCheckSum(start, size); |
| } |
| |
| private long getLongCheckSum(int start, int size) { |
| // All the tables here are aligned on four byte boundaries |
| // Add remainder to size if it's not a multiple of 4 |
| int remainder = size % 4; |
| if (remainder != 0) { |
| size += remainder; |
| } |
| |
| long sum = 0; |
| |
| for (int i = 0; i < size; i += 4) { |
| int l = (output[start + i] << 24); |
| l += (output[start + i + 1] << 16); |
| l += (output[start + i + 2] << 16); |
| l += (output[start + i + 3] << 16); |
| sum += l; |
| if (sum > 0xffffffff) { |
| sum = sum - 0xffffffff; |
| } |
| } |
| |
| return sum; |
| } |
| |
| private void createCheckSumAdjustment() { |
| long sum = getLongCheckSum(0, realSize); |
| int checksum = (int)(0xb1b0afba - sum); |
| writeULong(checkSumAdjustmentOffset, checksum); |
| } |
| |
| } |
| |
| |
| |