Initial commit of font compression code into public project.

This commit is contained in:
Raph Levien
2012-03-23 11:21:16 -07:00
commit dcecdd883a
49 changed files with 8454 additions and 0 deletions
@@ -0,0 +1,27 @@
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package com.google.typography.font.compression;
import com.google.typography.font.sfntly.Font;
import com.google.typography.font.sfntly.Tag;
import com.google.typography.font.sfntly.data.WritableFontData;
import com.google.typography.font.sfntly.table.core.HorizontalMetricsTable;
/**
* Extract just advance widths from hmtx table.
*
* @author raph@google.com (Raph Levien)
*/
public class AdvWidth {
public static WritableFontData encode(Font font) {
HorizontalMetricsTable hmtx = font.getTable(Tag.hmtx);
int nMetrics = hmtx.numberOfHMetrics();
WritableFontData result = WritableFontData.createWritableFontData(nMetrics * 2);
for (int i = 0; i < nMetrics; i++) {
result.writeShort(i * 2, hmtx.hMetricAdvanceWidth(i));
}
return result;
}
}
@@ -0,0 +1,82 @@
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package com.google.typography.font.compression;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.typography.font.sfntly.Font;
import com.google.typography.font.sfntly.Tag;
import com.google.typography.font.sfntly.table.core.CMap;
import com.google.typography.font.sfntly.table.core.CMapTable;
import com.google.typography.font.sfntly.table.core.MaximumProfileTable;
import java.io.ByteArrayOutputStream;
import java.util.List;
import java.util.Map;
/**
* @author raph@google.com (Raph Levien)
*
* Experimental CMap encoder, based primarily on writing the _inverse_ encoding.
*/
public class CmapEncoder {
public static byte[] encode(Font font) {
int nGlyphs = font.<MaximumProfileTable>getTable(Tag.maxp).numGlyphs();
CMapTable cmapTable = font.getTable(Tag.cmap);
CMap cmap = getBestCMap(cmapTable);
Map<Integer, Integer> invEncoding = Maps.newHashMap();
List<Integer> exceptions = Lists.newArrayList();
for (Integer i : cmap) {
int glyphId = cmap.glyphId(i);
if (invEncoding.containsKey(glyphId)) {
exceptions.add(i);
exceptions.add(glyphId);
} else {
invEncoding.put(glyphId, i);
}
}
ByteArrayOutputStream os = new ByteArrayOutputStream();
int last = -1;
for (int i = 0; i < nGlyphs; i++) {
if (invEncoding.containsKey(i)) {
int value = invEncoding.get(i);
int delta = value - last;
writeVShort(os, delta);
last = value;
} else {
writeVShort(os, 0);
}
}
for (int i : exceptions) {
writeVShort(os, i);
}
return os.toByteArray();
}
private static CMap getBestCMap(CMapTable cmapTable) {
for (CMap cmap : cmapTable) {
if (cmap.format() == CMap.CMapFormat.Format12.value()) {
return cmap;
}
}
for (CMap cmap : cmapTable) {
if (cmap.format() == CMap.CMapFormat.Format4.value()) {
return cmap;
}
}
return null;
}
// A simple signed varint encoding
static void writeVShort(ByteArrayOutputStream os, int value) {
if (value >= 0x2000 || value < -0x2000) {
os.write((byte)(0x80 | ((value >> 14) & 0x7f)));
}
if (value >= 0x40 || value < -0x40) {
os.write((byte)(0x80 | ((value >> 7) & 0x7f)));
}
os.write((byte)(value & 0x7f));
}
}
@@ -0,0 +1,72 @@
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package com.google.typography.font.compression;
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* This is a simple wrapper to run a commandline as a pipe. It's not
* particularly efficient, robust, or featureful.
*/
public class Command {
private class StreamConsumer extends Thread {
private final InputStream is;
private byte[] result = null;
public StreamConsumer(InputStream is) {
this.is = is;
}
@Override
public void run() {
try {
result = ByteStreams.toByteArray(is);
} catch (IOException e) {
// TODO: handle this well
}
}
public byte[] getResult() {
return result;
}
}
private final ProcessBuilder processBuilder;
public Command(String[] args) {
processBuilder = new ProcessBuilder(args);
}
public CommandResult execute(byte[] input) throws CommandException {
Process process;
try {
process = processBuilder.start();
} catch (IOException e) {
throw new CommandException("exec failed");
}
StreamConsumer sc = new StreamConsumer(process.getInputStream());
sc.start();
OutputStream stdin = process.getOutputStream();
try {
stdin.write(input);
stdin.close();
} catch (IOException e) {
throw new CommandException("error writing input");
}
try {
process.waitFor();
sc.join();
} catch (InterruptedException e) {
throw new CommandException("interrupted");
}
return new CommandResult(sc.getResult());
}
}
@@ -0,0 +1,12 @@
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package com.google.typography.font.compression;
public class CommandException extends Exception {
public CommandException(String message) {
super(message);
}
}
@@ -0,0 +1,18 @@
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package com.google.typography.font.compression;
public final class CommandResult {
private final byte[] stdout;
public CommandResult(byte[] stdout) {
this.stdout = stdout;
}
public byte[] getStdout() {
return stdout;
}
}
@@ -0,0 +1,22 @@
// Copyright 2012 Google Inc. All Rights Reserved.
package com.google.typography.font.compression;
/**
* @author raph@google.com (Raph Levien)
*/
public class CompressLzma {
// This is currently implemented by shelling out to a command line helper,
// which is fine for research purposes, but obviously problematic for
// production.
public static byte[] compress(byte[] input) {
try {
String[] args = {"/usr/bin/lzma"};
CommandResult result = new Command(args).execute(input);
return result.getStdout();
} catch (CommandException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,283 @@
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package com.google.typography.font.compression;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import com.google.typography.font.sfntly.Font;
import com.google.typography.font.sfntly.FontFactory;
import com.google.typography.font.sfntly.Tag;
import com.google.typography.font.sfntly.data.ReadableFontData;
import com.google.typography.font.tools.conversion.eot.EOTWriter;
import com.google.typography.font.tools.conversion.eot.HdmxEncoder;
import com.google.typography.font.tools.conversion.woff.WoffWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
/**
* A command-line tool for running different experimental compression code over
* a corpus of fonts, and gathering statistics, particularly compression
* efficiency.
*
* This is not intended to be production code, and for certain codecs it will
* shell out to helper binaries, which is fine for the purposes of gathering
* statistics, but obviously not much else.
*
* @author raph@google.com (Raph Levien)
*/
public class CompressionRunner {
// private static final boolean DEBUG = false;
public static void main(String[] args) throws IOException {
boolean generateOutput = false;
List<String> descs = Lists.newArrayList();
String baseline = "gzip";
List<String> filenames = Lists.newArrayList();
for (int i = 0; i < args.length; i++) {
if (args[i].charAt(0) == '-') {
if (args[i].equals("-o")) {
generateOutput = true;
} else if (args[i].equals("-x")) {
descs.add(args[i + 1]);
i++;
} else if (args[i].equals("-b")) {
baseline = args[i + 1];
i++;
}
} else {
filenames.add(args[i]);
}
}
// String baseline = "glyf/triplet,code,push:lzma";
// String baseline = "glyf/cbbox,triplet,code,push:hdmx:lzma";
// descs.add("woff2");
if (descs.isEmpty()) {
descs.add("glyf/cbbox,triplet,code,reslice:woff2");
}
runTest(filenames, baseline, descs, generateOutput);
}
private static void runTest(List<String> filenames, String baseline, List<String> descs,
boolean generateOutput) throws IOException {
PrintWriter o = new PrintWriter(System.out);
List<StatsCollector> stats = Lists.newArrayList();
for (int i = 0; i < descs.size(); i++) {
stats.add(new StatsCollector());
}
FontFactory fontFactory = FontFactory.getInstance();
o.println("<html>");
for (String filename : filenames) {
byte[] bytes = Files.toByteArray(new File(filename));
Font font = fontFactory.loadFonts(bytes)[0];
byte[] baselineResult = runExperiment(font, baseline);
o.printf("<!-- %s: baseline %d bytes", new File(filename).getName(), baselineResult.length);
for (int i = 0; i < descs.size(); i++) {
byte[] expResult = runExperiment(font, descs.get(i));
if (generateOutput) {
String newFilename = filename;
if (newFilename.endsWith(".ttf")) {
newFilename = newFilename.substring(0, newFilename.length() - 4);
}
newFilename += ".woff2";
Files.write(expResult, new File(newFilename));
}
double percent = 100. * expResult.length / baselineResult.length;
stats.get(i).addStat(percent);
o.printf(", %c %.2f%%", 'A' + i, percent);
}
o.printf(" -->\n");
}
stats.get(0).chartHeader(o, descs.size());
for (int i = 0; i < descs.size(); i++) {
stats.get(i).chartData(o, i + 1);
}
stats.get(0).chartEnd(o);
o.printf("<p>baseline: %s</p>\n", baseline);
for (int i = 0; i < descs.size(); i++) {
StatsCollector sc = stats.get(i);
o.printf("<p>%c: %s: median %f, mean %f</p>\n",
'A' + i, descs.get(i), sc.median(), sc.mean());
}
stats.get(0).chartFooter(o);
o.close();
}
private static Font.Builder stripTags(Font srcFont, Set<Integer> removeTags) {
FontFactory fontFactory = FontFactory.getInstance();
Font.Builder fontBuilder = fontFactory.newFontBuilder();
for (Integer tag : srcFont.tableMap().keySet()) {
if (!removeTags.contains(tag)) {
fontBuilder.newTableBuilder(tag, srcFont.getTable(tag).readFontData());
}
}
return fontBuilder;
}
private static Font.Builder preprocessMtxGlyf(Font srcFont, String options) {
Font.Builder fontBuilder = stripTags(srcFont, ImmutableSet.<Integer>of());
GlyfEncoder glyfEncoder = new GlyfEncoder(options);
glyfEncoder.encode(srcFont);
addTableBytes(fontBuilder, Tag.intValue(new byte[] {'g', 'l', 'z', '1'}),
glyfEncoder.getGlyfBytes());
addTableBytes(fontBuilder, Tag.intValue(new byte[] {'l', 'o', 'c', 'z'}),
glyfEncoder.getLocaBytes());
if (!Arrays.asList(options.split(",")).contains("reslice")) {
addTableBytes(fontBuilder, Tag.intValue(new byte[] {'g', 'l', 'z', '2'}),
glyfEncoder.getCodeBytes());
addTableBytes(fontBuilder, Tag.intValue(new byte[] {'g', 'l', 'z', '3'}),
glyfEncoder.getPushBytes());
}
return fontBuilder;
}
private static Font.Builder preprocessHmtx(Font srcFont) {
Font.Builder fontBuilder = stripTags(srcFont, ImmutableSet.of(Tag.hmtx));
addTableBytes(fontBuilder, Tag.intValue(new byte[] {'h', 'm', 't', 'z'}),
toBytes(AdvWidth.encode(srcFont)));
return fontBuilder;
}
private static Font.Builder preprocessHdmx(Font srcFont) {
Font.Builder fontBuilder = stripTags(srcFont, ImmutableSet.of(Tag.hdmx));
if (srcFont.hasTable(Tag.hdmx)) {
addTableBytes(fontBuilder, Tag.hdmx, toBytes(new HdmxEncoder().encode(srcFont)));
}
return fontBuilder;
}
private static Font.Builder preprocessCmap(Font srcFont) {
Font.Builder fontBuilder = stripTags(srcFont, ImmutableSet.of(Tag.cmap));
addTableBytes(fontBuilder, Tag.intValue(new byte[] {'c', 'm', 'a', 'z'}),
CmapEncoder.encode(srcFont));
return fontBuilder;
}
private static Font.Builder preprocessKern(Font srcFont) {
Font.Builder fontBuilder = stripTags(srcFont, ImmutableSet.of(Tag.kern));
if (srcFont.hasTable(Tag.kern)) {
addTableBytes(fontBuilder, Tag.intValue(new byte[] {'k', 'e', 'r', 'z'}),
toBytes(KernEncoder.encode(srcFont)));
}
return fontBuilder;
}
private static void addTableBytes(Font.Builder fontBuilder, int tag, byte[] contents) {
fontBuilder.newTableBuilder(tag, ReadableFontData.createReadableFontData(contents));
}
private static byte[] fontToBytes(FontFactory fontFactory, Font font) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
fontFactory.serializeFont(font, baos);
return baos.toByteArray();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static byte[] toBytes(ReadableFontData rfd) {
byte[] result = new byte[rfd.length()];
rfd.readBytes(0, result, 0, rfd.length());
return result;
}
// This is currently implemented by shelling out to a command line helper,
// which is fine for research purposes, but obviously problematic for
// production.
private static byte[] compressBzip2(byte[] input) {
try {
String[] args = {"/bin/bzip2"};
CommandResult result = new Command(args).execute(input);
return result.getStdout();
} catch (CommandException e) {
throw new RuntimeException(e);
}
}
/**
* Does one experimental compression on a font, using the string to guide what
* gets done.
*
* @param srcFont Source font
* @param desc experiment description string; the exact format is probably
* still evolving
* @return serialization of compressed font
* @throws IOException
*/
private static byte[] runExperiment(Font srcFont, String desc) throws IOException {
Font font = srcFont;
FontFactory fontFactory = FontFactory.getInstance();
String[] pieces = desc.split(":");
boolean keepDsig = false;
for (int i = 0; i < pieces.length - 1; i++) {
String[] piece = pieces[i].split("/");
String cmd = piece[0];
if (cmd.equals("glyf")) {
font = preprocessMtxGlyf(font, piece.length > 1 ? piece[1] : "").build();
} else if (cmd.equals("hmtx")) {
font = preprocessHmtx(font).build();
} else if (cmd.equals("hdmx")) {
font = preprocessHdmx(font).build();
} else if (cmd.equals("cmap")) {
font = preprocessCmap(font).build();
} else if (cmd.equals("kern")) {
font = preprocessKern(font).build();
} else if (cmd.equals("keepdsig")) {
keepDsig = true;
} else if (cmd.equals("strip")) {
Set<Integer> removeTags = Sets.newTreeSet();
for (String tag : piece[1].split(",")) {
removeTags.add(Tag.intValue(tag.getBytes()));
}
font = stripTags(font, removeTags).build();
}
}
if (!keepDsig) {
font = stripTags(font, ImmutableSet.of(Tag.DSIG)).build();
}
String last = pieces[pieces.length - 1];
String[] lastPieces = last.split("/");
String lastBase = lastPieces[0];
String lastArgs = lastPieces.length > 1 ? lastPieces[1] : "";
if (!lastBase.equals("woff2")) {
Set<Integer> tagsToStrip = Sets.newHashSet();
for (Entry<Integer, Integer> mapping : Woff2Writer.getTransformMap().entrySet()) {
if (font.hasTable(mapping.getValue())) {
tagsToStrip.add(mapping.getKey());
}
}
font = stripTags(font, tagsToStrip).build();
}
byte[] result = null;
if (lastBase.equals("gzip")) {
result = GzipUtil.deflate(fontToBytes(fontFactory, font));
} else if (lastBase.equals("lzma")) {
result = CompressLzma.compress(fontToBytes(fontFactory, font));
} else if (lastBase.equals("bzip2")) {
result = compressBzip2(fontToBytes(fontFactory, font));
} else if (lastBase.equals("woff")) {
result = toBytes(new WoffWriter().convert(font));
} else if (lastBase.equals("woff2")) {
result = toBytes(new Woff2Writer(lastArgs).convert(srcFont, font));
} else if (lastBase.equals("eot")) {
result = toBytes(new EOTWriter(true).convert(font));
} else if (lastBase.equals("uncomp")) {
result = fontToBytes(fontFactory, font);
}
return result;
}
}
@@ -0,0 +1,481 @@
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package com.google.typography.font.compression;
import com.google.typography.font.sfntly.Font;
import com.google.typography.font.sfntly.Tag;
import com.google.typography.font.sfntly.data.ReadableFontData;
import com.google.typography.font.sfntly.table.core.FontHeaderTable;
import com.google.typography.font.sfntly.table.truetype.CompositeGlyph;
import com.google.typography.font.sfntly.table.truetype.Glyph;
import com.google.typography.font.sfntly.table.truetype.GlyphTable;
import com.google.typography.font.sfntly.table.truetype.LocaTable;
import com.google.typography.font.sfntly.table.truetype.SimpleGlyph;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
/**
* @author Raph Levien
*
* Implementation of compression of CTF glyph data, as per sections 5.6-5.10 and 6 of the spec.
* This is a hacked-up version with a number of options, for experimenting.
*/
public class GlyfEncoder {
private final ByteArrayOutputStream nContourStream;
private final ByteArrayOutputStream nPointsStream;
private final ByteArrayOutputStream flagBytesStream;
private final ByteArrayOutputStream compositeStream;
private final ByteArrayOutputStream bboxStream;
private final ByteArrayOutputStream glyfStream;
private final ByteArrayOutputStream pushStream;
private final ByteArrayOutputStream codeStream;
private final boolean sbbox;
private final boolean cbbox;
private final boolean code;
private final boolean triplet;
private final boolean doPush;
private final boolean doHop;
private final boolean push2byte;
private final boolean reslice;
private int nGlyphs;
private byte[] bboxBitmap;
private FontHeaderTable.IndexToLocFormat indexFmt;
public GlyfEncoder(String options) {
glyfStream = new ByteArrayOutputStream();
pushStream = new ByteArrayOutputStream();
codeStream = new ByteArrayOutputStream();
nContourStream = new ByteArrayOutputStream();
nPointsStream = new ByteArrayOutputStream();
flagBytesStream = new ByteArrayOutputStream();
compositeStream = new ByteArrayOutputStream();
bboxStream = new ByteArrayOutputStream();
boolean sbbox = false;
boolean cbbox = false;
boolean code = false;
boolean triplet = false;
boolean doPush = false;
boolean reslice = false;
boolean doHop = false;
boolean push2byte = false;
for (String option : options.split(",")) {
if (option.equals("sbbox")) {
sbbox = true;
} else if (option.equals("cbbox")) {
cbbox = true;
} else if (option.equals("code")) {
code = true;
} else if (option.equals("triplet")) {
triplet = true;
} else if (option.equals("push")) {
doPush = true;
} else if (option.equals("hop")) {
doHop = true;
} else if (option.equals("push2byte")) {
push2byte = true;
} else if (option.equals("reslice")) {
reslice = true;
}
}
this.sbbox = sbbox;
this.cbbox = cbbox;
this.code = code;
this.triplet = triplet;
this.doPush = doPush;
this.doHop = doHop;
this.push2byte = push2byte;
this.reslice = reslice;
}
public void encode(Font sourceFont) {
FontHeaderTable head = sourceFont.getTable(Tag.head);
indexFmt = head.indexToLocFormat();
LocaTable loca = sourceFont.getTable(Tag.loca);
nGlyphs = loca.numGlyphs();
GlyphTable glyf = sourceFont.getTable(Tag.glyf);
bboxBitmap = new byte[((nGlyphs + 31) >> 5) << 2];
for (int glyphId = 0; glyphId < nGlyphs; glyphId++) {
int sourceOffset = loca.glyphOffset(glyphId);
int length = loca.glyphLength(glyphId);
Glyph glyph = glyf.glyph(sourceOffset, length);
writeGlyph(glyphId, glyph);
}
}
private void writeGlyph(int glyphId, Glyph glyph) {
try {
if (glyph == null || glyph.dataLength() == 0) {
writeNContours(0);
} else if (glyph instanceof SimpleGlyph) {
writeSimpleGlyph(glyphId, (SimpleGlyph)glyph);
} else if (glyph instanceof CompositeGlyph) {
writeCompositeGlyph(glyphId, (CompositeGlyph)glyph);
}
} catch (IOException e) {
throw new RuntimeException("unexpected IOException writing glyph data", e);
}
}
private void writeInstructions(Glyph glyph) throws IOException{
if (doPush) {
splitPush(glyph);
} else {
int pushCount = 0;
int codeSize = glyph.instructionSize();
if (!reslice) {
write255UShort(glyfStream, pushCount);
}
write255UShort(glyfStream, codeSize);
if (codeSize > 0) {
if (code) {
glyph.instructions().copyTo(codeStream);
} else {
glyph.instructions().copyTo(glyfStream);
}
}
}
}
private void writeSimpleGlyph(int glyphId, SimpleGlyph glyph) throws IOException {
int numContours = glyph.numberOfContours();
writeNContours(numContours);
if (sbbox) {
writeBbox(glyphId, glyph);
}
// TODO: check that bbox matches, write bbox if not
for (int i = 0; i < numContours; i++) {
if (reslice) {
write255UShort(nPointsStream, glyph.numberOfPoints(i));
} else {
write255UShort(glyfStream, glyph.numberOfPoints(i) - (i == 0 ? 1 : 0));
}
}
ByteArrayOutputStream os = new ByteArrayOutputStream();
int lastX = 0;
int lastY = 0;
for (int i = 0; i < numContours; i++) {
int numPoints = glyph.numberOfPoints(i);
for (int j = 0; j < numPoints; j++) {
int x = glyph.xCoordinate(i, j);
int y = glyph.yCoordinate(i, j);
int dx = x - lastX;
int dy = y - lastY;
if (triplet) {
writeTriplet(os, glyph.onCurve(i, j), dx, dy);
} else {
writeVShort(os, dx * 2 + (glyph.onCurve(i, j) ? 1 : 0));
writeVShort(os, dy);
}
lastX = x;
lastY = y;
}
}
os.writeTo(glyfStream);
if (numContours > 0) {
writeInstructions(glyph);
}
}
private void writeCompositeGlyph(int glyphId, CompositeGlyph glyph) throws IOException {
boolean haveInstructions = false;
writeNContours(-1);
if (cbbox) {
writeBbox(glyphId, glyph);
}
ByteArrayOutputStream outStream = reslice ? compositeStream : glyfStream;
for (int i = 0; i < glyph.numGlyphs(); i++) {
int flags = glyph.flags(i);
writeUShort(outStream, flags);
haveInstructions = (flags & CompositeGlyph.FLAG_WE_HAVE_INSTRUCTIONS) != 0;
writeUShort(outStream, glyph.glyphIndex(i));
if ((flags & CompositeGlyph.FLAG_ARG_1_AND_2_ARE_WORDS) == 0) {
outStream.write(glyph.argument1(i));
outStream.write(glyph.argument2(i));
} else {
writeUShort(outStream, glyph.argument1(i));
writeUShort(outStream, glyph.argument2(i));
}
if (glyph.transformationSize(i) != 0) {
try {
outStream.write(glyph.transformation(i));
} catch (IOException e) {
}
}
}
if (haveInstructions) {
writeInstructions(glyph);
}
}
private void writeNContours(int nContours) {
if (reslice) {
writeUShort(nContourStream, nContours);
} else {
writeUShort(nContours);
}
}
private void writeBbox(int glyphId, Glyph glyph) {
if (reslice) {
bboxBitmap[glyphId >> 3] |= 0x80 >> (glyphId & 7);
}
ByteArrayOutputStream outStream = reslice ? bboxStream : glyfStream;
writeUShort(outStream, glyph.xMin());
writeUShort(outStream, glyph.yMin());
writeUShort(outStream, glyph.xMax());
writeUShort(outStream, glyph.yMax());
}
private void writeUShort(ByteArrayOutputStream os, int value) {
os.write(value >> 8);
os.write(value & 255);
}
private void writeUShort(int value) {
writeUShort(glyfStream, value);
}
private void writeLong(OutputStream os, int value) throws IOException {
os.write((value >> 24) & 255);
os.write((value >> 16) & 255);
os.write((value >> 8) & 255);
os.write(value & 255);
}
// As per 6.1.1 of spec
// visible for testing
static void write255UShort(ByteArrayOutputStream os, int value) {
if (value < 0) {
throw new IllegalArgumentException();
}
if (value < 253) {
os.write((byte)value);
} else if (value < 506) {
os.write(255);
os.write((byte)(value - 253));
} else if (value < 762) {
os.write(254);
os.write((byte)(value - 506));
} else {
os.write(253);
os.write((byte)(value >> 8));
os.write((byte)(value & 0xff));
}
}
// As per 6.1.1 of spec
// visible for testing
static void write255Short(OutputStream os, int value) throws IOException {
int absValue = Math.abs(value);
if (value < 0) {
// spec is unclear about whether words should be signed. This code is conservative, but we
// can test once the implementation is working.
os.write(250);
}
if (absValue < 250) {
os.write((byte)absValue);
} else if (absValue < 500) {
os.write(255);
os.write((byte)(absValue - 250));
} else if (absValue < 756) {
os.write(254);
os.write((byte)(absValue - 500));
} else {
os.write(253);
os.write((byte)(absValue >> 8));
os.write((byte)(absValue & 0xff));
}
}
// A simple signed varint encoding
static void writeVShort(ByteArrayOutputStream os, int value) {
if (value >= 0x2000 || value < -0x2000) {
os.write((byte)(0x80 | ((value >> 14) & 0x7f)));
}
if (value >= 0x40 || value < -0x40) {
os.write((byte)(0x80 | ((value >> 7) & 0x7f)));
}
os.write((byte)(value & 0x7f));
}
// As in section 5.11 of the spec
// visible for testing
void writeTriplet(OutputStream os, boolean onCurve, int x, int y) throws IOException {
int absX = Math.abs(x);
int absY = Math.abs(y);
int onCurveBit = onCurve ? 0 : 128;
int xSignBit = (x < 0) ? 0 : 1;
int ySignBit = (y < 0) ? 0 : 1;
int xySignBits = xSignBit + 2 * ySignBit;
ByteArrayOutputStream flagStream = reslice ? flagBytesStream : glyfStream;
if (x == 0 && absY < 1280) {
flagStream.write(onCurveBit + ((absY & 0xf00) >> 7) + ySignBit);
os.write(absY & 0xff);
} else if (y == 0 && absX < 1280) {
flagStream.write(onCurveBit + 10 + ((absX & 0xf00) >> 7) + xSignBit);
os.write(absX & 0xff);
} else if (absX < 65 && absY < 65) {
flagStream.write(onCurveBit + 20 + ((absX - 1) & 0x30) + (((absY - 1) & 0x30) >> 2) +
xySignBits);
os.write((((absX - 1) & 0xf) << 4) | ((absY - 1) & 0xf));
} else if (absX < 769 && absY < 769) {
flagStream.write(onCurveBit + 84 + 12 * (((absX - 1) & 0x300) >> 8) +
(((absY - 1) & 0x300) >> 6) + xySignBits);
os.write((absX - 1) & 0xff);
os.write((absY - 1) & 0xff);
} else if (absX < 4096 && absY < 4096) {
flagStream.write(onCurveBit + 120 + xySignBits);
os.write(absX >> 4);
os.write(((absX & 0xf) << 4) | (absY >> 8));
os.write(absY & 0xff);
} else {
flagStream.write(onCurveBit + 124 + xySignBits);
os.write(absX >> 8);
os.write(absX & 0xff);
os.write(absY >> 8);
os.write(absY & 0xff);
}
}
/**
* Split the instructions into a push sequence and the remainder of the instructions.
* Writes both streams, and the counts to the glyfStream.
*
* @param glyph
*/
private void splitPush(Glyph glyph) throws IOException {
int instrSize = glyph.instructionSize();
ReadableFontData data = glyph.instructions();
int i = 0;
List<Integer> result = new ArrayList<Integer>();
// All push sequences are at least two bytes, make sure there's enough room
while (i + 1 < instrSize) {
int ix = i;
int instr = data.readUByte(ix++);
int n = 0;
int size = 0;
if (instr == 0x40 || instr == 0x41) {
// NPUSHB, NPUSHW
n = data.readUByte(ix++);
size = (instr & 1) + 1;
} else if (instr >= 0xB0 && instr < 0xC0) {
// PUSHB, PUSHW
n = 1 + (instr & 7);
size = ((instr & 8) >> 3) + 1;
} else {
break;
}
if (i + size * n > instrSize) {
// This is a broken font, and a potential buffer overflow, but in the interest
// of preserving the original, we just put the broken instruction sequence in
// the stream.
break;
}
for (int j = 0; j < n; j++) {
if (size == 1) {
result.add(data.readUByte(ix));
} else {
result.add(data.readShort(ix));
}
ix += size;
}
i = ix;
}
int pushCount = result.size();
int codeSize = instrSize - i;
write255UShort(glyfStream, pushCount);
write255UShort(glyfStream, codeSize);
encodePushSequence(pushStream, result);
if (codeSize > 0) {
data.slice(i).copyTo(codeStream);
}
}
// As per section 6.2.2 of the spec
private void encodePushSequence(ByteArrayOutputStream os, List<Integer> data) throws IOException {
int n = data.size();
int hopSkip = 0;
for (int i = 0; i < n; i++) {
if ((hopSkip & 1) == 0) {
int val = data.get(i);
if (doHop && hopSkip == 0 && i >= 2 &&
i + 2 < n && val == data.get(i - 2) && val == data.get(i + 2)) {
if (i + 4 < n && val == data.get(i + 4)) {
// Hop4 code
os.write(252);
hopSkip = 0x14;
} else {
// Hop3 code
os.write(251);
hopSkip = 4;
}
} else {
if (push2byte) {
// Measure relative effectiveness of 255Short literal encoding vs 2-byte ushort.
writeUShort(os, data.get(i));
} else {
write255Short(os, data.get(i));
}
}
}
hopSkip >>= 1;
}
}
public byte[] getGlyfBytes() {
if (reslice) {
ByteArrayOutputStream newStream = new ByteArrayOutputStream();
try {
// Pack all the glyf streams in a sensible way
writeLong(newStream, 0); // version
writeUShort(newStream, nGlyphs);
writeUShort(newStream, indexFmt.value());
writeLong(newStream, nContourStream.size());
writeLong(newStream, nPointsStream.size());
writeLong(newStream, flagBytesStream.size());
writeLong(newStream, glyfStream.size());
writeLong(newStream, compositeStream.size());
writeLong(newStream, bboxBitmap.length + bboxStream.size());
writeLong(newStream, codeStream.size());
System.out.printf("stream sizes = %d %d %d %d %d %d %d\n",
nContourStream.size(), nPointsStream.size(), flagBytesStream.size(), glyfStream.size(),
compositeStream.size(), bboxStream.size(), codeStream.size());
nContourStream.writeTo(newStream);
nPointsStream.writeTo(newStream);
flagBytesStream.writeTo(newStream);
glyfStream.writeTo(newStream);
compositeStream.writeTo(newStream);
newStream.write(bboxBitmap);
bboxStream.writeTo(newStream);
codeStream.writeTo(newStream);
} catch (IOException e) {
throw new RuntimeException("Can't happen, world must have come to end", e);
}
return newStream.toByteArray();
} else {
return glyfStream.toByteArray();
}
}
public byte[] getPushBytes() {
return pushStream.toByteArray();
}
public byte[] getCodeBytes() {
return codeStream.toByteArray();
}
public byte[] getLocaBytes() {
return new byte[]{ };
}
}
@@ -0,0 +1,28 @@
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package com.google.typography.font.compression;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
/**
* Simple utility for GZIP compression
*/
public class GzipUtil {
public static byte[] deflate(byte[] bytes) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DeflaterOutputStream dos = new DeflaterOutputStream(baos, new Deflater());
dos.write(bytes, 0, bytes.length);
dos.close();
return baos.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,37 @@
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package com.google.typography.font.compression;
import com.google.typography.font.sfntly.Font;
import com.google.typography.font.sfntly.Tag;
import com.google.typography.font.sfntly.data.ReadableFontData;
import com.google.typography.font.sfntly.data.WritableFontData;
import com.google.typography.font.sfntly.table.Table;
/**
* @author raph@google.com (Raph Levien)
*
* Encoder for "kern" table. This probably won't go in the spec because an even more
* effective technique would be to do class kerning in the GDEF tables, but, even so, I wanted
* to capture the stats.
*/
public class KernEncoder {
public static WritableFontData encode(Font font) {
Table kernTable = font.getTable(Tag.kern);
ReadableFontData data = kernTable.readFontData();
WritableFontData newData = WritableFontData.createWritableFontData(data.size());
data.copyTo(newData);
if (data.readUShort(0) == 0 && data.readUShort(4) == 0) {
int base = 18;
int nPairs = data.readUShort(10);
for (int i = 0; i < nPairs; i++) {
newData.writeUShort(base + i * 2, data.readUShort(base + i * 6));
newData.writeUShort(base + nPairs * 2 + i * 2, data.readUShort(base + i * 6 + 2));
newData.writeUShort(base + nPairs * 4 + i * 2, data.readUShort(base + i * 6 + 4));
}
}
return newData;
}
}
@@ -0,0 +1,92 @@
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package com.google.typography.font.compression;
import com.google.common.collect.Lists;
import java.io.PrintWriter;
import java.util.Collections;
import java.util.List;
/**
* Class for gathering up stats, for summarizing and graphing.
*
* @author raph@google.com (Raph Levien)
*/
public class StatsCollector {
private final List<Double> values;
public StatsCollector() {
values = Lists.newArrayList();
}
public void addStat(double value) {
values.add(value);
}
public double mean() {
double sum = 0;
for (Double value : values) {
sum += value;
}
return sum / values.size();
}
public double median() {
Collections.sort(values);
int length = values.size();
if (length % 2 == 1) {
return values.get((length - 1) / 2);
} else {
return 0.5 * (values.get(length / 2 - 1) + values.get(length / 2));
}
}
// Need to print <html> before calling this method
public void chartHeader(PrintWriter o, int n) {
o.println("<head>");
o.println("<script type='text/javascript' src='https://www.google.com/jsapi'></script>");
o.println("<script type='text/javascript'>");
o.println("google.load('visualization', '1', {packages:['corechart']});");
o.println("google.setOnLoadCallback(drawChart);");
o.println("function drawChart() {");
o.println(" var data = new google.visualization.DataTable()");
o.println(" data.addColumn('string', 'Font');");
if (n == 1) {
o.println(" data.addColumn('number', 'Ratio');");
} else {
for (int i = 0; i < n; i++) {
o.printf(" data.addColumn('number', 'Ratio %c');\n", 'A' + i);
}
}
o.printf(" data.addRows(%d);\n", values.size());
}
public void chartData(PrintWriter o, int ix) {
Collections.sort(values);
int length = values.size();
for (int i = 0; i < length; i++) {
o.printf(" data.setValue(%d, %d, %f);\n", i, ix, values.get(i));
}
}
public void chartEnd(PrintWriter o) {
o.println(" var chart = new google.visualization.LineChart(document.getElementById("
+ "'chart_div'));");
o.println(" chart.draw(data, {width:700, height:400, title: 'Compression ratio'});");
o.println("}");
o.println("</script>");
o.println("</head>");
o.println();
o.println("<body>");
o.println("<div id='chart_div'></div>");
// TODO: split so we can get content into the HTML
}
public void chartFooter(PrintWriter o) {
o.println("</body>");
o.println("</html>");
}
}
@@ -0,0 +1,28 @@
// Copyright 2011 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package com.google.typography.font.compression;
import java.io.IOException;
/**
* A simple test for the command mechanism. Quick and dirty run with:
* java -cp 'build/classes:lib/guava-11.0.1.jar' com/google/typography/font/compression/TestCommand
*/
public class TestCommand {
public static void main(String[] args) throws IOException {
String[] commandArgs = {"/usr/bin/lzma"};
byte[] input = new byte[16384];
try {
CommandResult result = new Command(commandArgs).execute(input);
byte[] output = result.getStdout();
for (int i = 0; i < output.length; i++) {
System.out.printf("%02x\n", output[i] & 0xff);
}
} catch (CommandException e) {
e.printStackTrace();
};
}
}
@@ -0,0 +1,352 @@
// Copyright 2012 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package com.google.typography.font.compression;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.Lists;
import com.google.typography.font.sfntly.Font;
import com.google.typography.font.sfntly.Tag;
import com.google.typography.font.sfntly.data.WritableFontData;
import com.google.typography.font.sfntly.table.Table;
import com.google.typography.font.sfntly.table.core.FontHeaderTable;
import java.util.List;
import java.util.TreeSet;
public class Woff2Writer {
private static final long SIGNATURE = 0x774f4632;
private static final int WOFF2_HEADER_SIZE = 44;
private static final int TABLE_ENTRY_SIZE = 5 * 4;
private static final int FLAG_CONTINUE_STREAM = 1 << 4;
private static final int FLAG_APPLY_TRANSFORM = 1 << 5;
private final CompressionType compressionType;
private final boolean longForm;
Woff2Writer(String args) {
CompressionType compressionType = CompressionType.NONE;
boolean longForm = false;
for (String arg : args.split(",")) {
if ("lzma".equals(arg)) {
compressionType = CompressionType.LZMA;
} else if ("gzip".equals(arg)) {
compressionType = CompressionType.GZIP;
} else if ("short".equals(arg)) {
longForm = false;
} else if ("long".equals(arg)) {
longForm = true;
}
}
this.compressionType = compressionType;
this.longForm = longForm;
}
private static ImmutableBiMap<Integer, Integer> TRANSFORM_MAP = ImmutableBiMap.of(
Tag.glyf, Tag.intValue(new byte[] {'g', 'l', 'z', '1'}),
Tag.loca, Tag.intValue(new byte[] {'l', 'o', 'c', 'z'})
);
public static ImmutableBiMap<Integer, Integer> getTransformMap() {
return TRANSFORM_MAP;
}
private static ImmutableBiMap<Integer, Integer> KNOWN_TABLES =
new ImmutableBiMap.Builder<Integer, Integer>()
.put(Tag.intValue(new byte[] {'c', 'm', 'a', 'p'}), 0)
.put(Tag.intValue(new byte[] {'h', 'e', 'a', 'd'}), 1)
.put(Tag.intValue(new byte[] {'h', 'h', 'e', 'a'}), 2)
.put(Tag.intValue(new byte[] {'h', 'm', 't', 'x'}), 3)
.put(Tag.intValue(new byte[] {'m', 'a', 'x', 'p'}), 4)
.put(Tag.intValue(new byte[] {'n', 'a', 'm', 'e'}), 5)
.put(Tag.intValue(new byte[] {'O', 'S', '/', '2'}), 6)
.put(Tag.intValue(new byte[] {'p', 'o', 's', 't'}), 7)
.put(Tag.intValue(new byte[] {'c', 'v', 't', ' '}), 8)
.put(Tag.intValue(new byte[] {'f', 'p', 'g', 'm'}), 9)
.put(Tag.intValue(new byte[] {'g', 'l', 'y', 'f'}), 10)
.put(Tag.intValue(new byte[] {'l', 'o', 'c', 'a'}), 11)
.put(Tag.intValue(new byte[] {'p', 'r', 'e', 'p'}), 12)
.put(Tag.intValue(new byte[] {'C', 'F', 'F', ' '}), 13)
.put(Tag.intValue(new byte[] {'V', 'O', 'R', 'G'}), 14)
.put(Tag.intValue(new byte[] {'E', 'B', 'D', 'T'}), 15)
.put(Tag.intValue(new byte[] {'E', 'B', 'L', 'C'}), 16)
.put(Tag.intValue(new byte[] {'g', 'a', 's', 'p'}), 17)
.put(Tag.intValue(new byte[] {'h', 'd', 'm', 'x'}), 18)
.put(Tag.intValue(new byte[] {'k', 'e', 'r', 'n'}), 19)
.put(Tag.intValue(new byte[] {'L', 'T', 'S', 'H'}), 20)
.put(Tag.intValue(new byte[] {'P', 'C', 'L', 'T'}), 21)
.put(Tag.intValue(new byte[] {'V', 'D', 'M', 'X'}), 22)
.put(Tag.intValue(new byte[] {'v', 'h', 'e', 'a'}), 23)
.put(Tag.intValue(new byte[] {'v', 'm', 't', 'x'}), 24)
.put(Tag.intValue(new byte[] {'B', 'A', 'S', 'E'}), 25)
.put(Tag.intValue(new byte[] {'G', 'D', 'E', 'F'}), 26)
.put(Tag.intValue(new byte[] {'G', 'P', 'O', 'S'}), 27)
.put(Tag.intValue(new byte[] {'G', 'S', 'U', 'B'}), 28)
.build();
public WritableFontData convert(Font srcFont, Font font) {
List<TableDirectoryEntry> entries = createTableDirectoryEntries(font);
int size = computeCompressedFontSize(entries);
WritableFontData writableFontData = WritableFontData.createWritableFontData(size);
int index = 0;
FontHeaderTable head = font.getTable(Tag.head);
index += writeWoff2Header(writableFontData, entries, font.sfntVersion(), size,
head.fontRevision());
System.out.printf("Wrote header, index = %d\n", index);
index += writeDirectory(writableFontData, index, entries);
System.out.printf("Wrote directory, index = %d\n", index);
index += writeTables(writableFontData, index, entries);
System.out.printf("Wrote tables, index = %d\n", index);
return writableFontData;
}
private List<TableDirectoryEntry> createTableDirectoryEntries(Font font) {
List<TableDirectoryEntry> entries = Lists.newArrayList();
TreeSet<Integer> tags = new TreeSet<Integer>(font.tableMap().keySet());
for (int tag : tags) {
Table table = font.getTable(tag);
byte[] uncompressedBytes = bytesFromTable(table);
byte[] transformedBytes = null;
if (TRANSFORM_MAP.containsValue(tag)) {
// Don't store the intermediate transformed tables under the nonstandard tags.
continue;
}
if (TRANSFORM_MAP.containsKey(tag)) {
int transformedTag = TRANSFORM_MAP.get(tag);
Table transformedTable = font.getTable(transformedTag);
if (transformedTable != null) {
transformedBytes = bytesFromTable(transformedTable);
}
}
if (transformedBytes == null) {
entries.add(new TableDirectoryEntry(tag, uncompressedBytes, compressionType));
} else {
entries.add(new TableDirectoryEntry(tag, uncompressedBytes, transformedBytes,
FLAG_APPLY_TRANSFORM, compressionType));
}
}
return entries;
}
private byte[] bytesFromTable(Table table) {
int length = table.dataLength();
byte[] bytes = new byte[length];
table.readFontData().readBytes(0, bytes, 0, length);
return bytes;
}
private int writeWoff2Header(WritableFontData writableFontData,
List<TableDirectoryEntry> entries,
int flavor,
int length,
int version) {
int index = 0;
index += writableFontData.writeULong(index, SIGNATURE);
index += writableFontData.writeULong(index, flavor);
index += writableFontData.writeULong(index, length);
index += writableFontData.writeUShort(index, entries.size()); // numTables
index += writableFontData.writeUShort(index, 0); // reserved
int uncompressedFontSize = computeUncompressedSize(entries);
index += writableFontData.writeULong(index, uncompressedFontSize);
index += writableFontData.writeFixed(index, version);
index += writableFontData.writeULong(index, 0); // metaOffset
index += writableFontData.writeULong(index, 0); // metaLength
index += writableFontData.writeULong(index, 0); // metaOrigLength
index += writableFontData.writeULong(index, 0); // privOffset
index += writableFontData.writeULong(index, 0); // privLength
return index;
}
private int writeDirectory(WritableFontData writableFontData, int offset,
List<TableDirectoryEntry> entries) {
int directorySize = computeDirectoryLength(entries);
for (TableDirectoryEntry entry : entries) {
offset += entry.writeEntry(writableFontData, offset);
}
return directorySize;
}
private int writeTables(WritableFontData writableFontData, int offset,
List<TableDirectoryEntry> entries) {
int start = offset;
for (TableDirectoryEntry entry : entries) {
offset += entry.writeData(writableFontData, offset);
offset = align4(offset);
}
return offset - start;
}
private int computeDirectoryLength(List<TableDirectoryEntry> entries) {
if (longForm) {
return TABLE_ENTRY_SIZE * entries.size();
} else {
int size = 0;
for (TableDirectoryEntry entry : entries) {
size += entry.writeEntry(null, size);
}
return size;
}
}
private int align4(int value) {
return (value + 3) & -4;
}
private int computeUncompressedSize(List<TableDirectoryEntry> entries) {
int size = 20 + 16 * entries.size(); // sfnt header length
for (TableDirectoryEntry entry : entries) {
size += entry.getOrigLength();
size = align4(size);
}
return size;
}
private int computeCompressedFontSize(List<TableDirectoryEntry> entries) {
int fontSize = WOFF2_HEADER_SIZE;
fontSize += computeDirectoryLength(entries);
for (TableDirectoryEntry entry : entries) {
fontSize += entry.getCompLength();
fontSize = align4(fontSize);
}
return fontSize;
}
private enum CompressionType {
NONE, GZIP, LZMA
}
private static long flagsForCompression(CompressionType compressionType) {
switch (compressionType) {
case NONE:
return 0;
case GZIP:
return 1;
case LZMA:
return 2;
}
return 0;
}
private static byte[] compress(byte[] input, CompressionType compressionType) {
switch (compressionType) {
case NONE:
return input;
case GZIP:
return GzipUtil.deflate(input);
case LZMA:
return CompressLzma.compress(input);
}
return null;
}
// Note: if writableFontData is null, just return the size
private static int writeBase128(WritableFontData writableFontData, long value, int offset) {
int size = 1;
long tmpValue = value;
while (tmpValue >= 128) {
size += 1;
tmpValue = tmpValue >> 7;
}
for (int i = 0; i < size; i++) {
int b = (int)(value >> (7 * (size - i - 1))) & 0x7f;
if (i < size - 1) {
b |= 0x80;
}
if (writableFontData != null) {
writableFontData.writeByte(offset, (byte)b);
}
offset += 1;
}
return size;
}
private class TableDirectoryEntry {
private final long tag;
private final long flags;
private final long origLength;
private final long transformLength;
private final byte[] bytes;
// This is the constructor for tables that don't have transforms
public TableDirectoryEntry(long tag, byte[] uncompressedBytes,
CompressionType compressionType) {
this(tag, uncompressedBytes, uncompressedBytes, 0, compressionType);
}
public TableDirectoryEntry(long tag, byte[] uncompressedBytes, byte[] transformedBytes,
long transformFlags, CompressionType compressionType) {
byte[] compressedBytes = compress(transformedBytes, compressionType);
if (compressedBytes.length >= transformedBytes.length) {
compressedBytes = transformedBytes;
compressionType = CompressionType.NONE;
}
this.tag = tag;
this.flags = transformFlags | flagsForCompression(compressionType);
this.origLength = uncompressedBytes.length;
this.transformLength = transformedBytes.length;
this.bytes = compressedBytes;
}
public long getOrigLength() {
return origLength;
}
public long getCompLength() {
return bytes.length;
}
// Note: if writableFontData is null, just return the size
public int writeEntry(WritableFontData writableFontData, int offset) {
if (longForm) {
if (writableFontData != null) {
offset += writableFontData.writeULong(offset, tag);
offset += writableFontData.writeULong(offset, flags);
offset += writableFontData.writeULong(offset, getCompLength());
offset += writableFontData.writeULong(offset, transformLength);
offset += writableFontData.writeULong(offset, getOrigLength());
}
return TABLE_ENTRY_SIZE;
} else {
int start = offset;
int flag_byte = 0x1f;
if (KNOWN_TABLES.containsKey((int)tag)) {
flag_byte = KNOWN_TABLES.get((int)tag);
}
if ((flags & FLAG_APPLY_TRANSFORM) != 0) {
flag_byte |= 0x20;
}
if ((flags & FLAG_CONTINUE_STREAM) != 0) {
flag_byte |= 0xc0;
} else {
flag_byte |= (flags & 3) << 6;
}
if (writableFontData != null) {
System.out.printf("%d: tag = %08x, flag = %02x\n", offset, tag, flag_byte);
writableFontData.writeByte(offset, (byte)flag_byte);
}
offset += 1;
if ((flag_byte & 0x1f) == 0x1f) {
if (writableFontData != null) {
writableFontData.writeULong(offset, tag);
}
offset += 4;
}
offset += writeBase128(writableFontData, getOrigLength(), offset);
if ((flag_byte & 0x20) != 0) {
offset += writeBase128(writableFontData, transformLength, offset);
}
if ((flag_byte & 0xc0) == 0x40 || (flag_byte & 0xc0) == 0x80) {
offset += writeBase128(writableFontData, getCompLength(), offset);
}
return offset - start;
}
}
public int writeData(WritableFontData writableFontData, int offset) {
writableFontData.writeBytes(offset, bytes);
return bytes.length;
}
}
}