org.apache.fop.fonts.truetype.TTFSubSetFile Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of fop Show documentation
Show all versions of fop Show documentation
Apache FOP (Formatting Objects Processor) is the world's first print formatter driven by XSL formatting objects (XSL-FO) and the world's first output independent formatter. It is a Java application that reads a formatting object (FO) tree and renders the resulting pages to a specified output. Output formats currently supported include PDF, PCL, PS, AFP, TIFF, PNG, SVG, XML (area tree representation), Print, AWT and TXT. The primary output target is PDF.
/*
* 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: TTFSubSetFile.java 1761019 2016-09-16 10:43:45Z ssteiner $ */
package org.apache.fop.fonts.truetype;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedSet;
/**
* 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 {
protected byte[] output;
protected int realSize;
protected int currentPos;
/*
* Offsets in name table to be filled out by table.
* The offsets are to the checkSum field
*/
protected Map offsets = new HashMap();
private int checkSumAdjustmentOffset;
protected int locaOffset;
/** Stores the glyph offsets so that we can end strings at glyph boundaries */
protected int[] glyphOffsets;
/**
* Default Constructor
*/
public TTFSubSetFile() {
}
/**
* 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);
}
/** The dir tab entries in the new subset font. */
protected Map newDirTabs
= new HashMap();
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 += 5; //5 req'd tables: glyf,loca,post,name,OS/2
if (hasCvt()) {
numTables++;
}
if (hasFpgm()) {
numTables++;
}
if (hasPrep()) {
numTables++;
}
if (!cid) {
numTables++; //cmap
}
}
return numTables;
}
/**
* Create the directory table
*/
protected 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 = (int) Math.pow(2, maxPow) * 16;
writeUShort(searchRange);
realSize += 2;
writeUShort(maxPow);
realSize += 2;
writeUShort((numTables * 16) - searchRange);
realSize += 2;
// Create space for the table entries (these must be in ASCII alphabetical order[A-Z] then[a-z])
writeTableName(OFTableName.OS2);
if (!cid) {
writeTableName(OFTableName.CMAP);
}
if (hasCvt()) {
writeTableName(OFTableName.CVT);
}
if (hasFpgm()) {
writeTableName(OFTableName.FPGM);
}
writeTableName(OFTableName.GLYF);
writeTableName(OFTableName.HEAD);
writeTableName(OFTableName.HHEA);
writeTableName(OFTableName.HMTX);
writeTableName(OFTableName.LOCA);
writeTableName(OFTableName.MAXP);
writeTableName(OFTableName.NAME);
writeTableName(OFTableName.POST);
if (hasPrep()) {
writeTableName(OFTableName.PREP);
}
newDirTabs.put(OFTableName.TABLE_DIRECTORY, new OFDirTabEntry(0, currentPos));
}
private void writeTableName(OFTableName tableName) {
writeString(tableName.getName());
offsets.put(tableName, currentPos);
currentPos += 12;
realSize += 16;
}
private boolean hasCvt() {
return dirTabs.containsKey(OFTableName.CVT);
}
private boolean hasFpgm() {
return dirTabs.containsKey(OFTableName.FPGM);
}
private boolean hasPrep() {
return dirTabs.containsKey(OFTableName.PREP);
}
/**
* Create an empty loca table without updating checksum
*/
protected void createLoca(int size) throws IOException {
pad4();
locaOffset = currentPos;
int dirTableOffset = offsets.get(OFTableName.LOCA);
writeULong(dirTableOffset + 4, currentPos);
writeULong(dirTableOffset + 8, size * 4 + 4);
currentPos += size * 4 + 4;
realSize += size * 4 + 4;
}
private boolean copyTable(FontFileReader in, OFTableName tableName) throws IOException {
OFDirTabEntry entry = dirTabs.get(tableName);
if (entry != null) {
pad4();
seekTab(in, tableName, 0);
writeBytes(in.getBytes((int) entry.getOffset(), (int) entry.getLength()));
updateCheckSum(currentPos, (int) entry.getLength(), tableName);
currentPos += (int) entry.getLength();
realSize += (int) entry.getLength();
return true;
} else {
return false;
}
}
/**
* Copy the cvt table as is from original font to subset font
*/
protected boolean createCvt(FontFileReader in) throws IOException {
return copyTable(in, OFTableName.CVT);
}
/**
* Copy the fpgm table as is from original font to subset font
*/
protected boolean createFpgm(FontFileReader in) throws IOException {
return copyTable(in, OFTableName.FPGM);
}
/**
* Copy the name table as is from the original.
*/
protected boolean createName(FontFileReader in) throws IOException {
return copyTable(in, OFTableName.NAME);
}
/**
* Copy the OS/2 table as is from the original.
*/
protected boolean createOS2(FontFileReader in) throws IOException {
return copyTable(in, OFTableName.OS2);
}
/**
* Copy the maxp table as is from original font to subset font
* and set num glyphs to size
*/
protected void createMaxp(FontFileReader in, int size) throws IOException {
OFTableName maxp = OFTableName.MAXP;
OFDirTabEntry entry = dirTabs.get(maxp);
if (entry != null) {
pad4();
seekTab(in, maxp, 0);
writeBytes(in.getBytes((int) entry.getOffset(), (int) entry.getLength()));
writeUShort(currentPos + 4, size);
updateCheckSum(currentPos, (int)entry.getLength(), maxp);
currentPos += (int)entry.getLength();
realSize += (int)entry.getLength();
} else {
throw new IOException("Can't find maxp table");
}
}
protected void createPost(FontFileReader in) throws IOException {
OFTableName post = OFTableName.POST;
OFDirTabEntry entry = dirTabs.get(post);
if (entry != null) {
pad4();
seekTab(in, post, 0);
int newTableSize = 32; // This is the post table size with glyphs truncated
byte[] newPostTable = new byte[newTableSize];
// We only want the first 28 bytes (truncate the glyph names);
System.arraycopy(in.getBytes((int) entry.getOffset(), newTableSize),
0, newPostTable, 0, newTableSize);
// set the post table to Format 3.0
newPostTable[1] = 0x03;
writeBytes(newPostTable);
updateCheckSum(currentPos, newTableSize, post);
currentPos += newTableSize;
realSize += newTableSize;
} else {
// throw new IOException("Can't find post table");
}
}
/**
* Copy the prep table as is from original font to subset font
*/
protected boolean createPrep(FontFileReader in) throws IOException {
return copyTable(in, OFTableName.PREP);
}
/**
* Copy the hhea table as is from original font to subset font
* and fill in size of hmtx table
*/
protected void createHhea(FontFileReader in, int size) throws IOException {
OFDirTabEntry entry = dirTabs.get(OFTableName.HHEA);
if (entry != null) {
pad4();
seekTab(in, OFTableName.HHEA, 0);
writeBytes(in.getBytes((int) entry.getOffset(), (int) entry.getLength()));
writeUShort((int) entry.getLength() + currentPos - 2, size);
updateCheckSum(currentPos, (int) entry.getLength(), OFTableName.HHEA);
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
*/
protected void createHead(FontFileReader in) throws IOException {
OFTableName head = OFTableName.HEAD;
OFDirTabEntry entry = dirTabs.get(head);
if (entry != null) {
pad4();
seekTab(in, head, 0);
writeBytes(in.getBytes((int) entry.getOffset(), (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
if (cid) {
output[currentPos + 51] = 1; // long locaformat
}
updateCheckSum(currentPos, (int)entry.getLength(), head);
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 {
OFTableName glyf = OFTableName.GLYF;
OFDirTabEntry entry = dirTabs.get(glyf);
int size = 0;
int startPos = 0;
int endOffset = 0; // Store this as the last loca
if (entry != null) {
pad4();
startPos = 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 = buildSubsetIndexToOrigIndexMap(glyphs);
glyphOffsets = new int[origIndexes.length];
for (int i = 0; i < origIndexes.length; i++) {
int nextOffset = 0;
int origGlyphIndex = origIndexes[i];
if (origGlyphIndex >= (mtxTab.length - 1)) {
nextOffset = (int)lastLoca;
} else {
nextOffset = (int)mtxTab[origGlyphIndex + 1].getOffset();
}
int glyphOffset = (int)mtxTab[origGlyphIndex].getOffset();
int glyphLength = nextOffset - glyphOffset;
byte[] glyphData = in.getBytes(
(int)entry.getOffset() + glyphOffset,
glyphLength);
int endOffset1 = endOffset;
// Copy glyph
writeBytes(glyphData);
// Update loca table
writeULong(locaOffset + i * 4, currentPos - startPos);
if ((currentPos - startPos + glyphLength) > endOffset1) {
endOffset1 = (currentPos - startPos + glyphLength);
}
// Store the glyph boundary positions relative to the start of the font
glyphOffsets[i] = currentPos;
currentPos += glyphLength;
realSize += glyphLength;
endOffset = endOffset1;
}
size = currentPos - startPos;
currentPos += 12;
realSize += 12;
updateCheckSum(startPos, size + 12, glyf);
// Update loca checksum and last loca index
writeULong(locaOffset + glyphs.size() * 4, endOffset);
int locaSize = glyphs.size() * 4 + 4;
int checksum = getCheckSum(output, locaOffset, locaSize);
writeULong(offsets.get(OFTableName.LOCA), checksum);
int padSize = (locaOffset + locaSize) % 4;
newDirTabs.put(OFTableName.LOCA,
new OFDirTabEntry(locaOffset, locaSize + padSize));
} else {
throw new IOException("Can't find glyf table");
}
}
protected int[] buildSubsetIndexToOrigIndexMap(Map glyphs) {
int[] origIndexes = new int[glyphs.size()];
for (Map.Entry glyph : glyphs.entrySet()) {
int origIndex = glyph.getKey();
int subsetIndex = glyph.getValue();
if (origIndexes.length > subsetIndex) {
origIndexes[subsetIndex] = origIndex;
}
}
return origIndexes;
}
/**
* 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)
*/
protected void createHmtx(FontFileReader in,
Map glyphs) throws IOException {
OFTableName hmtx = OFTableName.HMTX;
OFDirTabEntry entry = 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;
for (Map.Entry glyph : glyphs.entrySet()) {
Integer origIndex = glyph.getKey();
Integer subsetIndex = glyph.getValue();
writeUShort(currentPos + subsetIndex * 4,
mtxTab[origIndex].getWx());
writeUShort(currentPos + subsetIndex * 4 + 2,
mtxTab[origIndex].getLsb());
}
updateCheckSum(currentPos, hmtxSize, hmtx);
currentPos += hmtxSize;
realSize += hmtxSize;
} else {
throw new IOException("Can't find hmtx table");
}
}
/**
* Reads a font and creates a subset of the 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)
* @throws IOException in case of an I/O problem
*/
public void readFont(FontFileReader in, String name, String header,
Map glyphs) throws IOException {
fontFile = in;
//Check if TrueType collection, and that the name exists in the collection
if (!checkTTC(header, name)) {
throw new IOException("Failed to read font");
}
//Copy the Map as we're going to modify it
Map subsetGlyphs = new HashMap(glyphs);
output = new byte[in.getFileSize()];
readDirTabs();
readFontHeader();
getNumGlyphs();
readHorizontalHeader();
readHorizontalMetrics();
readIndexToLocation();
scanGlyphs(in, subsetGlyphs);
createDirectory(); // Create the TrueType header and directory
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.");
}
createLoca(subsetGlyphs.size()); // create empty loca table
createGlyf(in, subsetGlyphs); //create glyf table and update loca table
createOS2(in); // copy the OS/2 table
createHead(in);
createHhea(in, subsetGlyphs.size()); // Create the hhea table
createHmtx(in, subsetGlyphs); // Create hmtx table
createMaxp(in, subsetGlyphs.size()); // copy the maxp table
createName(in); // copy the name table
createPost(in); // copy the post table
optionalTableFound = createPrep(in); // copy prep table
if (!optionalTableFound) {
// prep is optional (used in TrueType fonts only)
log.debug("TrueType: prep table not present. Skipped.");
}
pad4();
createCheckSumAdjustment();
}
/**
* Returns a subset of the fonts (readFont() MUST be called first in order to create the
* subset).
* @return byte array
*/
public byte[] getFontSubset() {
byte[] ret = new byte[realSize];
System.arraycopy(output, 0, ret, 0, realSize);
return ret;
}
private void handleGlyphSubset(TTFGlyphOutputStream glyphOut) throws IOException {
glyphOut.startGlyphStream();
// Stream all but the last glyph
for (int i = 0; i < glyphOffsets.length - 1; i++) {
glyphOut.streamGlyph(output, glyphOffsets[i],
glyphOffsets[i + 1] - glyphOffsets[i]);
}
// Stream the last glyph
OFDirTabEntry glyf = newDirTabs.get(OFTableName.GLYF);
long lastGlyphLength = glyf.getLength()
- (glyphOffsets[glyphOffsets.length - 1] - glyf.getOffset());
glyphOut.streamGlyph(output, glyphOffsets[glyphOffsets.length - 1],
(int) lastGlyphLength);
glyphOut.endGlyphStream();
}
@Override
public void stream(TTFOutputStream ttfOut) throws IOException {
SortedSet> sortedDirTabs
= sortDirTabMap(newDirTabs);
TTFTableOutputStream tableOut = ttfOut.getTableOutputStream();
TTFGlyphOutputStream glyphOut = ttfOut.getGlyphOutputStream();
ttfOut.startFontStream();
for (Map.Entry entry : sortedDirTabs) {
if (entry.getKey().equals(OFTableName.GLYF)) {
handleGlyphSubset(glyphOut);
} else {
tableOut.streamTable(output, (int) entry.getValue().getOffset(),
(int) entry.getValue().getLength());
}
}
ttfOut.endFontStream();
}
protected void scanGlyphs(FontFileReader in, Map subsetGlyphs)
throws IOException {
OFDirTabEntry glyfTableInfo = dirTabs.get(OFTableName.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");
writeBytes(buf);
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;
}
protected void writeBytes(byte[] b) {
if (b.length + currentPos > output.length) {
byte[] newoutput = new byte[output.length * 2];
System.arraycopy(output, 0, newoutput, 0, output.length);
output = newoutput;
}
System.arraycopy(b, 0, output, currentPos, b.length);
}
/**
* Appends a USHORT to the output array,
* updates currentPost but not realSize
*/
protected 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
*/
protected 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,
* at the given position without changing currentPos
*/
protected 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;
}
/**
* Create a padding in the fontfile to align
* on a 4-byte boundary
*/
protected void pad4() {
int padSize = getPadSize(currentPos);
if (padSize < 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);
}
protected void updateCheckSum(int tableStart, int tableSize, OFTableName tableName) {
int checksum = getCheckSum(output, tableStart, tableSize);
int offset = offsets.get(tableName);
int padSize = getPadSize(tableStart + tableSize);
newDirTabs.put(tableName, new OFDirTabEntry(tableStart, tableSize + padSize));
writeULong(offset, checksum);
writeULong(offset + 4, tableStart);
writeULong(offset + 8, tableSize);
}
protected static int getCheckSum(byte[] data, 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) {
long l = 0;
for (int j = 0; j < 4; j++) {
l <<= 8;
if (data.length > (start + i + j)) {
l |= data[start + i + j] & 0xff;
}
}
sum += l;
}
return (int) sum;
}
protected void createCheckSumAdjustment() {
long sum = getCheckSum(output, 0, realSize);
int checksum = (int)(0xb1b0afba - sum);
writeULong(checkSumAdjustmentOffset, checksum);
}
}