Remove old files; use the ones in woff2/ instead

This commit is contained in:
Roderick Sheeter
2013-11-20 15:11:17 -08:00
parent 437bbad370
commit 4147ca2646
27 changed files with 0 additions and 2935 deletions
Binary file not shown.
Binary file not shown.
-44
View File
@@ -1,44 +0,0 @@
To build:
Check out a recent version of OTS, so that ots-read-only/ is alongside cpp/
Cd into cpp, and run "scons".
Then, decompress a font using ./woff2-decompress font.wof2 > font.ttf .
An example font is provided (Inconsolata-Regular.wof2). In this snapshot,
it contains no actual compression, but does apply the glyf table transform
and has the file format and structure as described in the current draft
of the "WOFF Ultra Condensed file format" doc.
That said, it is possible to get reliable estimates, and at the very least,
bounds, on the compression efficiency, with the confidence that the
compression is reversible. Running the compressor and doing a whole-file
compression with a standard entropy coder such as gzip or lzma will yield
a file size within a few dozen bytes or so of using a single entropy coded
stream in the final file format.
Another limitation of the current implementation snapshot is that it
doesn't implement continue streams. These will follow shortly.
= Building with lzma =
The lzma-enabled build is made with gyp instead of scons. Right now, the build
requires patching a clean copy of OTS. (And thanks to Bashi for the patch!)
- Download GYP from http://code.google.com/p/gyp/
- Download clean OTS sources:
% svn checkout http://ots.googlecode.com/svn/trunk/ ots-read-only
- Apply patch
% cd ots-read-only; patch -p0 < ../ots-lzma.patch
- Run gyp to generate Makefile
% cd ../cpp; gyp --depth=. -f make woff2.gyp
- Build
% make
Now run:
out/Default/woff2-decompress Inconsolata-Regular-lzma.wof2 > i.ttf
We expect the build recipes to be cleaned up before the code is ready for
production, but this should be good enough for testing.
-52
View File
@@ -1,52 +0,0 @@
# Build script for Linux
# Note: this script DOESN'T include LZMA. Use the gyp-based build instead.
#
# Usage:
# $ cd ots/test/
# $ scons -c # clean
# $ scons # build
#
# Since the validator-checker tool might handle malicious font files, all hardening options for recent g++/ld are enabled just in case.
# See http://wiki.debian.org/Hardening for details.
env = Environment(CCFLAGS = ['-O2', '-I../ots-read-only/include', '-I../ots-read-only/src', '-I/usr/include/freetype2', '-ggdb', '-Wall', '-W', '-Wno-unused-parameter', '-fno-strict-aliasing', '-fPIE', '-fstack-protector', '-D_FORTIFY_SOURCE=2', '-DOTS_DEBUG'], LINKFLAGS = ['-ggdb', '-Wl,-z,relro', '-Wl,-z,now', '-pie', '-lz'])
env.Library('libwoff2.a',
[
'woff2.cc',
# Just build all of OTS to keep things simple for now. We could
# refactor so that we only compile the few support routines from
# ots.cc that we need.
'../ots-read-only/src/cff.cc',
'../ots-read-only/src/cff_type2_charstring.cc',
'../ots-read-only/src/cmap.cc',
'../ots-read-only/src/cvt.cc',
'../ots-read-only/src/fpgm.cc',
'../ots-read-only/src/gasp.cc',
'../ots-read-only/src/gdef.cc',
'../ots-read-only/src/glyf.cc',
'../ots-read-only/src/gpos.cc',
'../ots-read-only/src/gsub.cc',
'../ots-read-only/src/hdmx.cc',
'../ots-read-only/src/head.cc',
'../ots-read-only/src/hhea.cc',
'../ots-read-only/src/hmtx.cc',
'../ots-read-only/src/kern.cc',
'../ots-read-only/src/layout.cc',
'../ots-read-only/src/loca.cc',
'../ots-read-only/src/ltsh.cc',
'../ots-read-only/src/maxp.cc',
'../ots-read-only/src/metrics.cc',
'../ots-read-only/src/name.cc',
'../ots-read-only/src/os2.cc',
'../ots-read-only/src/ots.cc',
'../ots-read-only/src/post.cc',
'../ots-read-only/src/prep.cc',
'../ots-read-only/src/vdmx.cc',
'../ots-read-only/src/vhea.cc',
'../ots-read-only/src/vmtx.cc',
'../ots-read-only/src/vorg.cc'
])
env.Program('woff2-decompress.cc', LIBS = ['woff2'], LIBPATH='.')
-61
View File
@@ -1,61 +0,0 @@
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// A very simple commandline tool for decompressing woff2 format files
// (given as argc[1]), writing the decompressed version to stdout.
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include "opentype-sanitiser.h"
#include "woff2.h"
namespace {
int Usage(const char *argv0) {
std::fprintf(stderr, "Usage: %s woff2_file > dest_ttf_file\n", argv0);
return 1;
}
} // namespace
int main(int argc, char **argv) {
if (argc != 2) return Usage(argv[0]);
if (::isatty(1)) return Usage(argv[0]);
const int fd = ::open(argv[1], O_RDONLY);
if (fd < 0) {
::perror("open");
return 1;
}
struct stat st;
::fstat(fd, &st);
uint8_t *data = new uint8_t[st.st_size];
if (::read(fd, data, st.st_size) != st.st_size) {
::perror("read");
return 1;
}
::close(fd);
size_t decompressed_size = ots::ComputeWOFF2FinalSize(data, st.st_size);
if (decompressed_size == 0) {
std::fprintf(stderr, "Error computing decompressed file size!\n");
return 1;
}
uint8_t *buf = new uint8_t[decompressed_size];
const bool result = ots::ConvertWOFF2ToTTF(buf, decompressed_size,
data, st.st_size);
if (!result) {
std::fprintf(stderr, "Failed to decompress file!\n");
}
fwrite(buf, 1, decompressed_size, stdout);
return !result;
}
-1035
View File
File diff suppressed because it is too large Load Diff
-43
View File
@@ -1,43 +0,0 @@
{
'variables': {
'ots_include_dirs': [
# This isn't particularly elegant, but it works
'../ots-read-only/include',
'../ots-read-only/src',
],
},
'target_defaults': {
'defines': [
'OTS_DEBUG',
],
},
'targets': [
{
'target_name': 'woff2',
'type': 'static_library',
'sources': [
'woff2.cc',
],
'include_dirs': [
'<@(ots_include_dirs)',
],
'dependencies': [
'../ots-read-only/ots-standalone.gyp:ots',
],
},
{
'target_name': 'woff2-decompress',
'type': 'executable',
'sources': [
'woff2-decompress.cc',
],
'include_dirs': [
'<@(ots_include_dirs)',
],
'dependencies': [
'woff2',
],
},
],
}
-21
View File
@@ -1,21 +0,0 @@
// Copyright (c) 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.
#ifndef OTS_WOFF2_H_
#define OTS_WOFF2_H_
namespace ots {
// Compute the size of the final uncompressed font, or 0 on error.
size_t ComputeWOFF2FinalSize(const uint8_t *data, size_t length);
// Decompresses the font into the target buffer. The result_length should
// be the same as determined by ComputeFinalSize(). Returns true on successful
// decompression.
bool ConvertWOFF2ToTTF(uint8_t *result, size_t result_length,
const uint8_t *data, size_t length);
}
#endif // OTS_WOFF2_H_
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
@@ -1,28 +0,0 @@
// 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 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;
}
}
@@ -1,83 +0,0 @@
// 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;
/**
* Experimental CMap encoder, based primarily on writing the _inverse_ encoding.
*
* @author Raph Levien
*/
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));
}
}
@@ -1,43 +0,0 @@
// Copyright 2012 Google Inc. All Rights Reserved.
package com.google.typography.font.compression;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import SevenZip.Compression.LZMA.Encoder;
/**
* @author Raph Levien
* @author David Kuettel
*/
public class CompressLzma {
public static byte[] compress(byte[] input) {
try {
ByteArrayInputStream in = new ByteArrayInputStream(input);
ByteArrayOutputStream out = new ByteArrayOutputStream();
Encoder encoder = new Encoder();
encoder.SetAlgorithm(2);
encoder.SetDictionarySize(1 << 23);
encoder.SetNumFastBytes(128);
encoder.SetMatchFinder(1);
encoder.SetLcLpPb(3, 0, 2);
encoder.SetEndMarkerMode(true);
encoder.WriteCoderProperties(out);
for (int i = 0; i < 8; i++) {
out.write((int) ((long) -1 >>> (8 * i)) & 0xFF);
}
encoder.Code(in, out, -1, -1, null);
out.flush();
out.close();
return out.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@@ -1,103 +0,0 @@
// 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.io.Files;
import com.google.typography.font.sfntly.Font;
import com.google.typography.font.sfntly.FontFactory;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
/**
* 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.
*
* @author Raph Levien
*/
public class CompressionRunner {
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/lzma");
}
run(filenames, baseline, descs, generateOutput);
}
private static void run(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 = Experiment.run(font, baseline);
o.printf("<!-- %s: baseline %d bytes", new File(filename).getName(), baselineResult.length);
for (int i = 0; i < descs.size(); i++) {
byte[] expResult = Experiment.run(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();
}
}
@@ -1,105 +0,0 @@
package com.google.typography.font.compression;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Compression stats, both aggregate and per font.
*
* @author David Kuettel
*/
public class CompressionStats {
public enum Size { ORIGINAL, GZIP, WOFF, WOFF2 }
private final List<Stats> values = Lists.newArrayList();
public void add(Stats stat) {
values.add(stat);
}
public List<Stats> values() {
return values;
}
public double mean(Size size) {
double sum = 0;
for (Long value : values(size)) {
sum += value;
}
return sum / values.size();
}
public double median(Size size) {
List<Long> list = values(size);
Collections.sort(list);
int length = list.size();
if (length % 2 == 1) {
return list.get((length - 1) / 2);
} else {
return 0.5 * (list.get(length / 2 - 1) + list.get(length / 2));
}
}
private List<Long> values(Size size) {
List<Long> list = Lists.newArrayList();
for (Stats stats : values) {
list.add(stats.getSize(size));
}
return list;
}
public static class Stats {
private final String filename;
private final Map<Size, Long> sizes;
private Stats(String filename, Map<Size, Long> sizes) {
this.filename = filename;
this.sizes = sizes;
}
public String getFilename() {
return filename;
}
public long getSize(Size size) {
return sizes.get(size);
}
public double getPercent(Size s1, Size s2) {
long v1 = sizes.get(s1);
long v2 = sizes.get(s2);
return 100.0 * (v1 - v2) / v1;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String filename;
private Map<Size, Long> sizes = Maps.newHashMap();
public Builder setFilename(String filename) {
this.filename = filename;
return this;
}
public Builder setSize(Size key, long value) {
this.sizes.put(key, value);
return this;
}
public Stats build() {
return new Stats(filename, ImmutableMap.copyOf(sizes));
}
}
}
}
@@ -1,33 +0,0 @@
package com.google.typography.font.compression;
import com.google.common.io.Closeables;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
/**
* Generates a CSV report containing the compression stats.
*
* @author David Kuettel
*/
public class CsvReport {
public static void create(CompressionStats stats, String filename) throws IOException {
PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(filename)));
try {
writer.printf("Font, Original (bytes), WOFF 1.0 (bytes), WOFF 2.0 (bytes), %% Improvement\n");
for (CompressionStats.Stats stat : stats.values()) {
writer.printf("%s, %d, %d, %d, %.2f%%\n",
stat.getFilename(),
stat.getSize(CompressionStats.Size.ORIGINAL),
stat.getSize(CompressionStats.Size.WOFF),
stat.getSize(CompressionStats.Size.WOFF2),
stat.getPercent(CompressionStats.Size.WOFF, CompressionStats.Size.WOFF2));
}
} finally {
Closeables.closeQuietly(writer);
}
}
}
@@ -1,93 +0,0 @@
package com.google.typography.font.compression;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
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.tools.conversion.eot.EOTWriter;
import com.google.typography.font.tools.conversion.woff.WoffWriter;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
/**
* @author Raph Levien
*/
public class Experiment {
/**
* 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
*/
public static byte[] run(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 = FontUtil.preprocessMtxGlyf(font, piece.length > 1 ? piece[1] : "").build();
} else if (cmd.equals("hmtx")) {
font = FontUtil.preprocessHmtx(font).build();
} else if (cmd.equals("hdmx")) {
font = FontUtil.preprocessHdmx(font).build();
} else if (cmd.equals("cmap")) {
font = FontUtil.preprocessCmap(font).build();
} else if (cmd.equals("kern")) {
font = FontUtil.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 = FontUtil.stripTags(font, removeTags).build();
}
}
if (!keepDsig) {
font = FontUtil.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 (Map.Entry<Integer, Integer> mapping : Woff2Writer.getTransformMap().entrySet()) {
if (font.hasTable(mapping.getValue())) {
tagsToStrip.add(mapping.getKey());
}
}
font = FontUtil.stripTags(font, tagsToStrip).build();
}
byte[] result = null;
if (lastBase.equals("gzip")) {
result = GzipUtil.deflate(FontUtil.toBytes(fontFactory, font));
} else if (lastBase.equals("lzma")) {
result = CompressLzma.compress(FontUtil.toBytes(fontFactory, font));
} else if (lastBase.equals("woff")) {
result = FontUtil.toBytes(new WoffWriter().convert(font));
} else if (lastBase.equals("woff2")) {
result = FontUtil.toBytes(new Woff2Writer(lastArgs).convert(font));
} else if (lastBase.equals("eot")) {
result = FontUtil.toBytes(new EOTWriter(true).convert(font));
} else if (lastBase.equals("uncomp")) {
result = FontUtil.toBytes(fontFactory, font);
}
return result;
}
}
@@ -1,100 +0,0 @@
package com.google.typography.font.compression;
import com.google.common.collect.ImmutableSet;
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.HdmxEncoder;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import java.util.Set;
/**
* Font utility methods
*
* @author Raph Levien
*/
public class FontUtil {
public 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;
}
public 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;
}
public 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;
}
public 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;
}
public 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;
}
public 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;
}
public static void addTableBytes(Font.Builder fontBuilder, int tag, byte[] contents) {
fontBuilder.newTableBuilder(tag, ReadableFontData.createReadableFontData(contents));
}
public static byte[] toBytes(FontFactory fontFactory, Font font) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
fontFactory.serializeFont(font, baos);
return baos.toByteArray();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static byte[] toBytes(ReadableFontData rfd) {
byte[] result = new byte[rfd.length()];
rfd.readBytes(0, result, 0, rfd.length());
return result;
}
}
@@ -1,481 +0,0 @@
// 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;
/**
* 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.
*
* @author Raph Levien
*/
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[]{ };
}
}
@@ -1,31 +0,0 @@
// 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
*
* @author Raph Levien
*/
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);
}
}
}
@@ -1,38 +0,0 @@
// 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;
/**
* 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.
*
* @author Raph Levien
*/
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;
}
}
@@ -1,92 +0,0 @@
package com.google.typography.font.compression;
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.table.truetype.GlyphTable;
import com.google.typography.font.sfntly.table.truetype.LocaTable;
import java.io.File;
import java.io.IOException;
/**
* Simple WOFF 2.0 compression report runner.
*
* @author David Kuettel
*/
public class SimpleRunner {
private static final FontFactory FONT_FACTORY = FontFactory.getInstance();
private static final String WOFF = "woff";
private static final String WOFF2 = "woff2/lzma";
private static final String TRUETYPE = "glyf/cbbox,triplet,code,reslice";
private static final String CFF = "";
private static final String REPORT = "report.csv";
public static void main(String[] args) throws IOException {
if (args.length == 0) {
usage();
}
CompressionStats stats = new CompressionStats();
System.out.printf("Analyzing (%d) fonts\n", args.length);
run(stats, args);
System.out.printf("Creating report: %s\n", REPORT);
CsvReport.create(stats, REPORT);
}
private static void run(CompressionStats stats, String[] filenames) throws IOException {
for (String filename : filenames) {
try {
File file = new File(filename);
byte[] bytes = Files.toByteArray(file);
Font font = FONT_FACTORY.loadFonts(bytes)[0];
byte[] woff = Experiment.run(font, WOFF);
byte[] woff2 = Experiment.run(font, getOptions(font));
CompressionStats.Stats stat = CompressionStats.Stats.builder()
.setFilename(file.getName())
.setSize(CompressionStats.Size.ORIGINAL, bytes.length)
.setSize(CompressionStats.Size.WOFF, woff.length)
.setSize(CompressionStats.Size.WOFF2, woff2.length)
.build();
stats.add(stat);
System.out.printf("> %s, %d, %d, %d, %.2f%%\n",
stat.getFilename(),
stat.getSize(CompressionStats.Size.ORIGINAL),
stat.getSize(CompressionStats.Size.WOFF),
stat.getSize(CompressionStats.Size.WOFF2),
stat.getPercent(CompressionStats.Size.WOFF, CompressionStats.Size.WOFF2));
String target = filename.replaceAll("[.](ttf|otf)", ".woff2");
Files.write(woff2, new File(target));
} catch (Throwable t) {
System.err.printf("WARNING: failed to compress: %s\n", filename);
t.printStackTrace();
}
}
}
private static boolean isTrueType(Font font) {
LocaTable loca = font.getTable(Tag.loca);
GlyphTable glyf = font.getTable(Tag.glyf);
return (loca != null && glyf != null);
}
private static String getOptions(Font font) {
return String.format("%s:%s", (isTrueType(font)) ? TRUETYPE : CFF, WOFF2);
}
private static void usage() {
System.err.println("Usage: SimpleRunner font...");
System.exit(-1);
}
}
@@ -1,93 +0,0 @@
// 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 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>");
}
}
@@ -1,356 +0,0 @@
// 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;
/**
* @author Raph Levien
*/
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;
public 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 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;
}
}
}