blob: 535e71ca5b5f0e223b19c5e84f701a18b28a8f87 [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.ttf;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.TreeSet;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Subsetter for TrueType (TTF) fonts.
*
* <p>Originally developed by Wolfgang Glas for
* <a href="https://clazzes.org/display/SKETCH/Clazzes.org+Sketch+Home">Sketch</a>.
*
* @author Wolfgang Glas
*/
public final class TTFSubsetter
{
private static final Logger LOG = LogManager.getLogger(TTFSubsetter.class);
private static final byte[] PAD_BUF = { 0, 0, 0 };
private static final TimeZone TIMEZONE_UTC = TimeZone.getTimeZone("UTC"); // clone before using
private final TrueTypeFont ttf;
private final CmapLookup unicodeCmap;
private final SortedMap<Integer, Integer> uniToGID;
private final List<String> keepTables;
private final SortedSet<Integer> glyphIds; // new glyph ids
private String prefix;
private boolean hasAddedCompoundReferences;
/**
* Creates a subsetter for the given font.
*
* @param ttf the font to be subset
*
* @throws IOException if there is an error reading the font data
*/
public TTFSubsetter(TrueTypeFont ttf) throws IOException
{
this(ttf, null);
}
/**
* Creates a subsetter for the given font.
*
* @param ttf the font to be subset
* @param tables optional tables to keep if present
*
* @throws IOException if there is an error reading the font data
*/
public TTFSubsetter(TrueTypeFont ttf, List<String> tables) throws IOException
{
this.ttf = ttf;
this.keepTables = tables;
uniToGID = new TreeMap<>();
glyphIds = new TreeSet<>();
// find the best Unicode cmap
this.unicodeCmap = ttf.getUnicodeCmapLookup();
// always copy GID 0
glyphIds.add(0);
}
/**
* Sets the prefix to add to the font's PostScript name.
*
* @param prefix to be used as prefix for the PostScript name of the font
*/
public void setPrefix(String prefix)
{
this.prefix = prefix;
}
/**
* Add the given character code to the subset.
*
* @param unicode character code
*/
public void add(int unicode)
{
int gid = unicodeCmap.getGlyphId(unicode);
if (gid != 0)
{
uniToGID.put(unicode, gid);
glyphIds.add(gid);
}
}
/**
* Add the given character codes to the subset.
*
* @param unicodeSet character code set
*/
public void addAll(Set<Integer> unicodeSet)
{
unicodeSet.forEach(this::add);
}
/**
* Returns the map of new -&gt; old GIDs.
*
* @return the GID map
*
* @throws IOException if the font data could not be read
*/
public Map<Integer, Integer> getGIDMap() throws IOException
{
addCompoundReferences();
Map<Integer, Integer> newToOld = new HashMap<>();
int newGID = 0;
for (int oldGID : glyphIds)
{
newToOld.put(newGID, oldGID);
newGID++;
}
return newToOld;
}
/**
* @param out The data output stream.
* @param nTables The number of table.
* @return The file offset of the first TTF table to write.
* @throws IOException Upon errors.
*/
private long writeFileHeader(DataOutputStream out, int nTables) throws IOException
{
out.writeInt(0x00010000);
out.writeShort(nTables);
int mask = Integer.highestOneBit(nTables);
int searchRange = mask * 16;
out.writeShort(searchRange);
int entrySelector = log2(mask);
out.writeShort(entrySelector);
// numTables * 16 - searchRange
int last = 16 * nTables - searchRange;
out.writeShort(last);
return 0x00010000L + toUInt32(nTables, searchRange) + toUInt32(entrySelector, last);
}
private long writeTableHeader(DataOutputStream out, String tag, long offset, byte[] bytes)
throws IOException
{
long checksum = 0;
for (int nup = 0, n = bytes.length; nup < n; nup++)
{
checksum += (bytes[nup] & 0xffL) << 24 - nup % 4 * 8;
}
checksum &= 0xffffffffL;
byte[] tagbytes = tag.getBytes(StandardCharsets.US_ASCII);
out.write(tagbytes, 0, 4);
out.writeInt((int)checksum);
out.writeInt((int)offset);
out.writeInt(bytes.length);
// account for the checksum twice, once for the header field, once for the content itself
return toUInt32(tagbytes) + checksum + checksum + offset + bytes.length;
}
private void writeTableBody(OutputStream os, byte[] bytes) throws IOException
{
int n = bytes.length;
os.write(bytes);
if (n % 4 != 0)
{
os.write(PAD_BUF, 0, 4 - n % 4);
}
}
private byte[] buildHeadTable() throws IOException
{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(bos);
HeaderTable h = ttf.getHeader();
writeFixed(out, h.getVersion());
writeFixed(out, h.getFontRevision());
writeUint32(out, 0); // h.getCheckSumAdjustment()
writeUint32(out, h.getMagicNumber());
writeUint16(out, h.getFlags());
writeUint16(out, h.getUnitsPerEm());
writeLongDateTime(out, h.getCreated());
writeLongDateTime(out, h.getModified());
writeSInt16(out, h.getXMin());
writeSInt16(out, h.getYMin());
writeSInt16(out, h.getXMax());
writeSInt16(out, h.getYMax());
writeUint16(out, h.getMacStyle());
writeUint16(out, h.getLowestRecPPEM());
writeSInt16(out, h.getFontDirectionHint());
// force long format of 'loca' table
writeSInt16(out, (short)1); // h.getIndexToLocFormat()
writeSInt16(out, h.getGlyphDataFormat());
out.flush();
return bos.toByteArray();
}
private byte[] buildHheaTable() throws IOException
{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(bos);
HorizontalHeaderTable h = ttf.getHorizontalHeader();
writeFixed(out, h.getVersion());
writeSInt16(out, h.getAscender());
writeSInt16(out, h.getDescender());
writeSInt16(out, h.getLineGap());
writeUint16(out, h.getAdvanceWidthMax());
writeSInt16(out, h.getMinLeftSideBearing());
writeSInt16(out, h.getMinRightSideBearing());
writeSInt16(out, h.getXMaxExtent());
writeSInt16(out, h.getCaretSlopeRise());
writeSInt16(out, h.getCaretSlopeRun());
writeSInt16(out, h.getReserved1()); // caretOffset
writeSInt16(out, h.getReserved2());
writeSInt16(out, h.getReserved3());
writeSInt16(out, h.getReserved4());
writeSInt16(out, h.getReserved5());
writeSInt16(out, h.getMetricDataFormat());
// is there a GID >= numberOfHMetrics ? Then keep the last entry of original hmtx table,
// (add if it isn't in our set of GIDs), see also in buildHmtxTable()
int hmetrics = glyphIds.subSet(0, h.getNumberOfHMetrics()).size();
if (glyphIds.last() >= h.getNumberOfHMetrics() && !glyphIds.contains(h.getNumberOfHMetrics()-1))
{
++hmetrics;
}
writeUint16(out, hmetrics);
out.flush();
return bos.toByteArray();
}
private boolean shouldCopyNameRecord(NameRecord nr)
{
return nr.getPlatformId() == NameRecord.PLATFORM_WINDOWS
&& nr.getPlatformEncodingId() == NameRecord.ENCODING_WINDOWS_UNICODE_BMP
&& nr.getLanguageId() == NameRecord.LANGUAGE_WINDOWS_EN_US
&& nr.getNameId() >= 0 && nr.getNameId() < 7;
}
private byte[] buildNameTable() throws IOException
{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(bos);
NamingTable name = ttf.getNaming();
if (name == null || keepTables != null && !keepTables.contains(NamingTable.TAG))
{
return null;
}
List<NameRecord> nameRecords = name.getNameRecords();
int numRecords = (int) nameRecords.stream().filter(this::shouldCopyNameRecord).count();
writeUint16(out, 0);
writeUint16(out, numRecords);
writeUint16(out, 2*3 + 2*6 * numRecords);
if (numRecords == 0)
{
return null;
}
byte[][] names = new byte[numRecords][];
int j = 0;
for (NameRecord nameRecord : nameRecords)
{
if (shouldCopyNameRecord(nameRecord))
{
int platform = nameRecord.getPlatformId();
int encoding = nameRecord.getPlatformEncodingId();
Charset charset = StandardCharsets.ISO_8859_1;
if (platform == CmapTable.PLATFORM_WINDOWS &&
encoding == CmapTable.ENCODING_WIN_UNICODE_BMP)
{
charset = StandardCharsets.UTF_16BE;
}
else if (platform == 2) // ISO [deprecated]=
{
if (encoding == 0) // 7-bit ASCII
{
charset = StandardCharsets.US_ASCII;
}
else if (encoding == 1) // ISO 10646=
{
//not sure is this is correct??
charset = StandardCharsets.UTF_16BE;
}
}
String value = nameRecord.getString();
if (nameRecord.getNameId() == 6 && prefix != null)
{
value = prefix + value;
}
names[j] = value.getBytes(charset);
j++;
}
}
int offset = 0;
j = 0;
for (NameRecord nr : nameRecords)
{
if (shouldCopyNameRecord(nr))
{
writeUint16(out, nr.getPlatformId());
writeUint16(out, nr.getPlatformEncodingId());
writeUint16(out, nr.getLanguageId());
writeUint16(out, nr.getNameId());
writeUint16(out, names[j].length);
writeUint16(out, offset);
offset += names[j].length;
j++;
}
}
for (int i = 0; i < numRecords; i++)
{
out.write(names[i]);
}
out.flush();
return bos.toByteArray();
}
private byte[] buildMaxpTable() throws IOException
{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(bos);
MaximumProfileTable p = ttf.getMaximumProfile();
writeFixed(out, p.getVersion());
writeUint16(out, glyphIds.size());
if (p.getVersion() >= 1.0f)
{
writeUint16(out, p.getMaxPoints());
writeUint16(out, p.getMaxContours());
writeUint16(out, p.getMaxCompositePoints());
writeUint16(out, p.getMaxCompositeContours());
writeUint16(out, p.getMaxZones());
writeUint16(out, p.getMaxTwilightPoints());
writeUint16(out, p.getMaxStorage());
writeUint16(out, p.getMaxFunctionDefs());
writeUint16(out, p.getMaxInstructionDefs());
writeUint16(out, p.getMaxStackElements());
writeUint16(out, p.getMaxSizeOfInstructions());
writeUint16(out, p.getMaxComponentElements());
writeUint16(out, p.getMaxComponentDepth());
}
out.flush();
return bos.toByteArray();
}
private byte[] buildOS2Table() throws IOException
{
OS2WindowsMetricsTable os2 = ttf.getOS2Windows();
if (os2 == null || uniToGID.isEmpty()
|| keepTables != null && !keepTables.contains(OS2WindowsMetricsTable.TAG))
{
return null;
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(bos);
writeUint16(out, os2.getVersion());
writeSInt16(out, os2.getAverageCharWidth());
writeUint16(out, os2.getWeightClass());
writeUint16(out, os2.getWidthClass());
writeSInt16(out, os2.getFsType());
writeSInt16(out, os2.getSubscriptXSize());
writeSInt16(out, os2.getSubscriptYSize());
writeSInt16(out, os2.getSubscriptXOffset());
writeSInt16(out, os2.getSubscriptYOffset());
writeSInt16(out, os2.getSuperscriptXSize());
writeSInt16(out, os2.getSuperscriptYSize());
writeSInt16(out, os2.getSuperscriptXOffset());
writeSInt16(out, os2.getSuperscriptYOffset());
writeSInt16(out, os2.getStrikeoutSize());
writeSInt16(out, os2.getStrikeoutPosition());
writeSInt16(out, (short)os2.getFamilyClass());
out.write(os2.getPanose());
writeUint32(out, 0);
writeUint32(out, 0);
writeUint32(out, 0);
writeUint32(out, 0);
out.write(os2.getAchVendId().getBytes(StandardCharsets.US_ASCII));
writeUint16(out, os2.getFsSelection());
writeUint16(out, uniToGID.firstKey());
writeUint16(out, uniToGID.lastKey());
writeUint16(out, os2.getTypoAscender());
writeUint16(out, os2.getTypoDescender());
writeUint16(out, os2.getTypoLineGap());
writeUint16(out, os2.getWinAscent());
writeUint16(out, os2.getWinDescent());
out.flush();
return bos.toByteArray();
}
// never returns null
private byte[] buildLocaTable(long[] newOffsets) throws IOException
{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(bos);
for (long offset : newOffsets)
{
writeUint32(out, offset);
}
out.flush();
return bos.toByteArray();
}
/**
* Resolve compound glyph references.
*/
private void addCompoundReferences() throws IOException
{
if (hasAddedCompoundReferences)
{
return;
}
hasAddedCompoundReferences = true;
boolean hasNested;
GlyphTable g = ttf.getGlyph();
long[] offsets = ttf.getIndexToLocation().getOffsets();
do
{
Set<Integer> glyphIdsToAdd = null;
try (InputStream is = ttf.getOriginalData())
{
long isResult = is.skip(g.getOffset());
if (Long.compare(isResult, g.getOffset()) != 0)
{
LOG.debug("Tried skipping {} bytes but skipped only {} bytes", g.getOffset(),
isResult);
}
long lastOff = 0L;
for (Integer glyphId : glyphIds)
{
long offset = offsets[glyphId];
long len = offsets[glyphId + 1] - offset;
isResult = is.skip(offset - lastOff);
if (Long.compare(isResult, offset - lastOff) != 0)
{
LOG.debug("Tried skipping {} bytes but skipped only {} bytes",
offset - lastOff, isResult);
}
byte[] buf = new byte[(int)len];
isResult = is.read(buf);
if (Long.compare(isResult, len) != 0)
{
LOG.debug("Tried reading {} bytes but only {} bytes read", len, isResult);
}
// rewrite glyphIds for compound glyphs
if (buf.length >= 2 && buf[0] == -1 && buf[1] == -1)
{
int off = 2*5;
int flags;
do
{
flags = (buf[off] & 0xff) << 8 | buf[off + 1] & 0xff;
off +=2;
int ogid = (buf[off] & 0xff) << 8 | buf[off + 1] & 0xff;
if (!glyphIds.contains(ogid))
{
if (glyphIdsToAdd == null)
{
glyphIdsToAdd = new TreeSet<>();
}
glyphIdsToAdd.add(ogid);
}
off += 2;
// ARG_1_AND_2_ARE_WORDS
if ((flags & 1 << 0) != 0)
{
off += 2 * 2;
}
else
{
off += 2;
}
// WE_HAVE_A_TWO_BY_TWO
if ((flags & 1 << 7) != 0)
{
off += 2 * 4;
}
// WE_HAVE_AN_X_AND_Y_SCALE
else if ((flags & 1 << 6) != 0)
{
off += 2 * 2;
}
// WE_HAVE_A_SCALE
else if ((flags & 1 << 3) != 0)
{
off += 2;
}
}
while ((flags & 1 << 5) != 0); // MORE_COMPONENTS
}
lastOff = offsets[glyphId + 1];
}
}
hasNested = glyphIdsToAdd != null;
if (hasNested)
{
glyphIds.addAll(glyphIdsToAdd);
}
}
while (hasNested);
}
// never returns null
private byte[] buildGlyfTable(long[] newOffsets) throws IOException
{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
GlyphTable g = ttf.getGlyph();
long[] offsets = ttf.getIndexToLocation().getOffsets();
try (InputStream is = ttf.getOriginalData())
{
long isResult = is.skip(g.getOffset());
if (Long.compare(isResult, g.getOffset()) != 0)
{
LOG.debug("Tried skipping {} bytes but skipped only {} bytes", g.getOffset(),
isResult);
}
long prevEnd = 0; // previously read glyph offset
long newOffset = 0; // new offset for the glyph in the subset font
int newGid = 0; // new GID in subset font
// for each glyph in the subset
for (Integer gid : glyphIds)
{
long offset = offsets[gid];
long length = offsets[gid + 1] - offset;
newOffsets[newGid++] = newOffset;
isResult = is.skip(offset - prevEnd);
if (Long.compare(isResult, offset - prevEnd) != 0)
{
LOG.debug("Tried skipping {} bytes but skipped only {} bytes", offset - prevEnd,
isResult);
}
byte[] buf = new byte[(int)length];
isResult = is.read(buf);
if (Long.compare(isResult, length) != 0)
{
LOG.debug("Tried reading {} bytes but only {} bytes read", length, isResult);
}
// detect glyph type
if (buf.length >= 2 && buf[0] == -1 && buf[1] == -1)
{
// compound glyph
int off = 2*5;
int flags;
do
{
// flags
flags = (buf[off] & 0xff) << 8 | buf[off + 1] & 0xff;
off += 2;
// glyphIndex
int componentGid = (buf[off] & 0xff) << 8 | buf[off + 1] & 0xff;
glyphIds.add(componentGid);
int newComponentGid = getNewGlyphId(componentGid);
buf[off] = (byte)(newComponentGid >>> 8);
buf[off + 1] = (byte)newComponentGid;
off += 2;
// ARG_1_AND_2_ARE_WORDS
if ((flags & 1 << 0) != 0)
{
off += 2 * 2;
}
else
{
off += 2;
}
// WE_HAVE_A_TWO_BY_TWO
if ((flags & 1 << 7) != 0)
{
off += 2 * 4;
}
// WE_HAVE_AN_X_AND_Y_SCALE
else if ((flags & 1 << 6) != 0)
{
off += 2 * 2;
}
// WE_HAVE_A_SCALE
else if ((flags & 1 << 3) != 0)
{
off += 2;
}
}
while ((flags & 1 << 5) != 0); // MORE_COMPONENTS
// WE_HAVE_INSTRUCTIONS
if ((flags & 0x0100) == 0x0100)
{
// USHORT numInstr
int numInstr = (buf[off] & 0xff) << 8 | buf[off + 1] & 0xff;
off += 2;
// BYTE instr[numInstr]
off += numInstr;
}
// write the compound glyph
bos.write(buf, 0, off);
// offset to start next glyph
newOffset += off;
}
else if (buf.length > 0)
{
// copy the entire glyph
bos.write(buf, 0, buf.length);
// offset to start next glyph
newOffset += buf.length;
}
// 4-byte alignment
if (newOffset % 4 != 0)
{
int len = 4 - (int)(newOffset % 4);
bos.write(PAD_BUF, 0, len);
newOffset += len;
}
prevEnd = offset + length;
}
newOffsets[newGid++] = newOffset;
}
return bos.toByteArray();
}
private int getNewGlyphId(Integer oldGid)
{
return glyphIds.headSet(oldGid).size();
}
private byte[] buildCmapTable() throws IOException
{
if (ttf.getCmap() == null || uniToGID.isEmpty()
|| keepTables != null && !keepTables.contains(CmapTable.TAG))
{
return null;
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(bos);
// cmap header
writeUint16(out, 0); // version
writeUint16(out, 1); // numberSubtables
// encoding record
writeUint16(out, CmapTable.PLATFORM_WINDOWS); // platformID
writeUint16(out, CmapTable.ENCODING_WIN_UNICODE_BMP); // platformSpecificID
writeUint32(out, 12); // offset 4 * 2 + 4
// build Format 4 subtable (Unicode BMP)
Iterator<Entry<Integer, Integer>> it = uniToGID.entrySet().iterator();
Entry<Integer, Integer> lastChar = it.next();
Entry<Integer, Integer> prevChar = lastChar;
int lastGid = getNewGlyphId(lastChar.getValue());
// +1 because .notdef is missing in uniToGID
int[] startCode = new int[uniToGID.size()+1];
int[] endCode = new int[startCode.length];
int[] idDelta = new int[startCode.length];
int segCount = 0;
while(it.hasNext())
{
Entry<Integer, Integer> curChar2Gid = it.next();
int curGid = getNewGlyphId(curChar2Gid.getValue());
// todo: need format Format 12 for non-BMP
if (curChar2Gid.getKey() > 0xFFFF)
{
throw new UnsupportedOperationException("non-BMP Unicode character");
}
if (curChar2Gid.getKey() != prevChar.getKey()+1 ||
curGid - lastGid != curChar2Gid.getKey() - lastChar.getKey())
{
if (lastGid != 0)
{
// don't emit ranges, which map to GID 0, the
// undef glyph is emitted a the very last segment
startCode[segCount] = lastChar.getKey();
endCode[segCount] = prevChar.getKey();
idDelta[segCount] = lastGid - lastChar.getKey();
segCount++;
}
else if (!lastChar.getKey().equals(prevChar.getKey()))
{
// shorten ranges which start with GID 0 by one
startCode[segCount] = lastChar.getKey() + 1;
endCode[segCount] = prevChar.getKey();
idDelta[segCount] = lastGid - lastChar.getKey();
segCount++;
}
lastGid = curGid;
lastChar = curChar2Gid;
}
prevChar = curChar2Gid;
}
// trailing segment
startCode[segCount] = lastChar.getKey();
endCode[segCount] = prevChar.getKey();
idDelta[segCount] = lastGid -lastChar.getKey();
segCount++;
// GID 0
startCode[segCount] = 0xffff;
endCode[segCount] = 0xffff;
idDelta[segCount] = 1;
segCount++;
// write format 4 subtable
int searchRange = 2 * (int)Math.pow(2, log2(segCount));
writeUint16(out, 4); // format
writeUint16(out, 8 * 2 + segCount * 4*2); // length
writeUint16(out, 0); // language
writeUint16(out, segCount * 2); // segCountX2
writeUint16(out, searchRange); // searchRange
writeUint16(out, log2(searchRange / 2)); // entrySelector
writeUint16(out, 2 * segCount - searchRange); // rangeShift
// endCode[segCount]
for (int i = 0; i < segCount; i++)
{
writeUint16(out, endCode[i]);
}
// reservedPad
writeUint16(out, 0);
// startCode[segCount]
for (int i = 0; i < segCount; i++)
{
writeUint16(out, startCode[i]);
}
// idDelta[segCount]
for (int i = 0; i < segCount; i++)
{
writeUint16(out, idDelta[i]);
}
for (int i = 0; i < segCount; i++)
{
writeUint16(out, 0);
}
return bos.toByteArray();
}
private byte[] buildPostTable() throws IOException
{
PostScriptTable post = ttf.getPostScript();
if (post == null || post.getGlyphNames() == null ||
keepTables != null && !keepTables.contains(PostScriptTable.TAG))
{
return null;
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(bos);
writeFixed(out, 2.0); // version
writeFixed(out, post.getItalicAngle());
writeSInt16(out, post.getUnderlinePosition());
writeSInt16(out, post.getUnderlineThickness());
writeUint32(out, post.getIsFixedPitch());
writeUint32(out, post.getMinMemType42());
writeUint32(out, post.getMaxMemType42());
writeUint32(out, post.getMinMemType1());
writeUint32(out, post.getMaxMemType1());
// version 2.0
// numberOfGlyphs
writeUint16(out, glyphIds.size());
// glyphNameIndex[numGlyphs]
Map<String, Integer> names = new LinkedHashMap<>();
for (int gid : glyphIds)
{
String name = post.getName(gid);
Integer macId = WGL4Names.getGlyphIndex(name);
if (macId != null)
{
// the name is implicit, as it's from MacRoman
writeUint16(out, macId);
}
else
{
// the name will be written explicitly
Integer ordinal = names.computeIfAbsent(name, dummy -> names.size());
writeUint16(out, 258 + ordinal);
}
}
// names[numberNewGlyphs]
for (String name : names.keySet())
{
byte[] buf = name.getBytes(StandardCharsets.US_ASCII);
writeUint8(out, buf.length);
out.write(buf);
}
out.flush();
return bos.toByteArray();
}
private byte[] buildHmtxTable() throws IOException
{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
HorizontalHeaderTable h = ttf.getHorizontalHeader();
HorizontalMetricsTable hm = ttf.getHorizontalMetrics();
InputStream is = ttf.getOriginalData();
// more info: https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6hmtx.html
int lastgid = h.getNumberOfHMetrics() - 1;
// true if lastgid is not in the set: we'll need its width (but not its left side bearing) later
boolean needLastGidWidth = glyphIds.last() > lastgid && !glyphIds.contains(lastgid);
try
{
long isResult = is.skip(hm.getOffset());
if (Long.compare(isResult, hm.getOffset()) != 0)
{
LOG.debug("Tried skipping {} bytes but only {} bytes skipped", hm.getOffset(),
isResult);
}
long lastOffset = 0;
for (Integer glyphId : glyphIds)
{
// offset in original file
long offset;
if (glyphId <= lastgid)
{
// copy width and lsb
offset = glyphId * 4l;
lastOffset = copyBytes(is, bos, offset, lastOffset, 4);
}
else
{
if (needLastGidWidth)
{
// one time only: copy width from lastgid, whose width applies
// to all later glyphs
needLastGidWidth = false;
offset = lastgid * 4l;
lastOffset = copyBytes(is, bos, offset, lastOffset, 2);
// then go on with lsb from actual glyph (lsb are individual even in monotype fonts)
}
// copy lsb only, as we are beyond numOfHMetrics
offset = h.getNumberOfHMetrics() * 4l + (glyphId - h.getNumberOfHMetrics()) * 2l;
lastOffset = copyBytes(is, bos, offset, lastOffset, 2);
}
}
return bos.toByteArray();
}
finally
{
is.close();
}
}
private long copyBytes(InputStream is, OutputStream os, long newOffset, long lastOffset, int count)
throws IOException
{
// skip over from last original offset
long nskip = newOffset - lastOffset;
if (nskip != is.skip(nskip))
{
throw new EOFException("Unexpected EOF exception parsing glyphId of hmtx table.");
}
byte[] buf = new byte[count];
if (count != is.read(buf, 0, count))
{
throw new EOFException("Unexpected EOF exception parsing glyphId of hmtx table.");
}
os.write(buf, 0, count);
return newOffset + count;
}
/**
* Write the subfont to the given output stream.
*
* @param os the stream used for writing. It will be closed by this method.
* @throws IOException if something went wrong.
* @throws IllegalStateException if the subset is empty.
*/
public void writeToStream(OutputStream os) throws IOException
{
if (glyphIds.isEmpty() && uniToGID.isEmpty())
{
LOG.info("font subset is empty");
}
addCompoundReferences();
try (DataOutputStream out = new DataOutputStream(os))
{
long[] newLoca = new long[glyphIds.size() + 1];
// generate tables in dependency order
byte[] head = buildHeadTable();
byte[] hhea = buildHheaTable();
byte[] maxp = buildMaxpTable();
byte[] name = buildNameTable();
byte[] os2 = buildOS2Table();
byte[] glyf = buildGlyfTable(newLoca);
byte[] loca = buildLocaTable(newLoca);
byte[] cmap = buildCmapTable();
byte[] hmtx = buildHmtxTable();
byte[] post = buildPostTable();
// save to TTF in optimized order
Map<String, byte[]> tables = new TreeMap<>();
if (os2 != null)
{
tables.put(OS2WindowsMetricsTable.TAG, os2);
}
if (cmap != null)
{
tables.put(CmapTable.TAG, cmap);
}
tables.put(GlyphTable.TAG, glyf);
tables.put(HeaderTable.TAG, head);
tables.put(HorizontalHeaderTable.TAG, hhea);
tables.put(HorizontalMetricsTable.TAG, hmtx);
tables.put(IndexToLocationTable.TAG, loca);
tables.put(MaximumProfileTable.TAG, maxp);
if (name != null)
{
tables.put(NamingTable.TAG, name);
}
if (post != null)
{
tables.put(PostScriptTable.TAG, post);
}
// copy all other tables
for (Map.Entry<String, TTFTable> entry : ttf.getTableMap().entrySet())
{
String tag = entry.getKey();
TTFTable table = entry.getValue();
if (!tables.containsKey(tag) && (keepTables == null || keepTables.contains(tag)))
{
tables.put(tag, ttf.getTableBytes(table));
}
}
// calculate checksum
long checksum = writeFileHeader(out, tables.size());
long offset = 12L + 16L * tables.size();
for (Map.Entry<String, byte[]> entry : tables.entrySet())
{
checksum += writeTableHeader(out, entry.getKey(), offset, entry.getValue());
offset += (entry.getValue().length + 3L) / 4 * 4;
}
checksum = 0xB1B0AFBAL - (checksum & 0xffffffffL);
// update checksumAdjustment in 'head' table
head[8] = (byte)(checksum >>> 24);
head[9] = (byte)(checksum >>> 16);
head[10] = (byte)(checksum >>> 8);
head[11] = (byte)checksum;
for (byte[] bytes : tables.values())
{
writeTableBody(out, bytes);
}
}
}
private void writeFixed(DataOutputStream out, double f) throws IOException
{
double ip = Math.floor(f);
double fp = (f-ip) * 65536.0;
out.writeShort((int)ip);
out.writeShort((int)fp);
}
private void writeUint32(DataOutputStream out, long l) throws IOException
{
out.writeInt((int)l);
}
private void writeUint16(DataOutputStream out, int i) throws IOException
{
out.writeShort(i);
}
private void writeSInt16(DataOutputStream out, short i) throws IOException
{
out.writeShort(i);
}
private void writeUint8(DataOutputStream out, int i) throws IOException
{
out.writeByte(i);
}
private void writeLongDateTime(DataOutputStream out, Calendar calendar) throws IOException
{
// inverse operation of TTFDataStream.readInternationalDate()
Calendar cal = Calendar.getInstance((TimeZone) TIMEZONE_UTC.clone());
cal.set(1904, 0, 1, 0, 0, 0);
cal.set(Calendar.MILLISECOND, 0);
long millisFor1904 = cal.getTimeInMillis();
long secondsSince1904 = (calendar.getTimeInMillis() - millisFor1904) / 1000L;
out.writeLong(secondsSince1904);
}
private long toUInt32(int high, int low)
{
return (high & 0xffffL) << 16 | low & 0xffffL;
}
private long toUInt32(byte[] bytes)
{
return (bytes[0] & 0xffL) << 24
| (bytes[1] & 0xffL) << 16
| (bytes[2] & 0xffL) << 8
| bytes[3] & 0xffL;
}
private int log2(int num)
{
return (int) Math.floor(Math.log(num) / Math.log(2));
}
public void addGlyphIds(Set<Integer> allGlyphIds)
{
glyphIds.addAll(allGlyphIds);
}
}