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
+27
View File
@@ -0,0 +1,27 @@
// Copyright (c) 2011 Google Inc. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+155
View File
@@ -0,0 +1,155 @@
This is a README for the font compression reference code. Its very rough in
this snapshot, but will be cleaned up some for public release.
= How to run the compression test tool =
This document documents how to run the compression reference code. At this
writing, the code, while it is intended to produce a bytestream that can be
reconstructed into a working font, the reference decompression code is not
done, and the exact format of that bytestream is subject to change.
== Building the tool ==
On a standard Unix-style environment, it should be as simple as running “ant”.
A couple of paths to compression subprocesses are hardcoded in
CompressionRunner.java, namely “usr/bin/lzma” and “/bin/bzip2”. These are the
default locations in Ubuntu, but if theyre elsewhere on your system, youll
need to change that.
The tool depends on sfntly for much of the font work. The lib/ directory
contains a snapshot jar. If you want to use the latest sfntly sources, then cd
to the java subdirectory, run “ant”, then copy these files dist/lib/sfntly.jar
dist/tools/conversion/eot/eotconverter.jar and
dist.tools/conversion/woff/woffconverter.jar to $(thisproject)/lib:
dist/lib/sfntly.jar dist/tools/conversion/eot/eotconverter.jar
dist.tools/conversion/woff/woffconverter.jar
Theres also a dependency on guava (see references below).
The dependencies are subject to their own licenses.
== Setting up the test ==
A run of the tool evaluates a “base” configuration plus one or more test
configurations, for each font. It measures the file size of the test as a ratio
over the base file size, then graphs the value of that ratio sorted across all
files given on the command line.
The test parameters are set by command line options (an improvement from the
last snapshot). The base is set by the -b command line option, and the
additional tests are specified by repeated -x command line options (see below).
Each test is specified by a string description. It is a colon-separated list of
stages. The final stage is entropy compression and can be one of “gzip”,
“lzma”, “bzip2”, “woff”, “eot” (with actual wire-format MTX compression), or
“uncomp” (for raw, uncompressed TTFs). Also, the new wire-format draft
WOFF2 spec is available as "woff2", and takes an entropy coding as an
optional argument, as in "woff2/gzip" or "woff2/lzma".
Other stages may optionally include subparameters (following a slash, and
comma-separated). The stages are:
glyf: performs glyf-table preprocessing based on MTX. There are subparameters:
1. cbbox (composite bounding box). When specified, the bounding box for
composite glyphs is included, otherwise stripped 2. sbbox (simple bounding
box). When specified, the bounding box for simple glyphs is included 3. code:
the bytecode is separated out into a separate stream 4. triplet: triplet coding
(as in MTX) is used 5. push: push sequences are separated; if unset, pushes are
kept inline in the bytecode 6. reslice: components of the glyf table are
separated into individual streams, taking the MTX idea of separating the
bytecodes further.
hmtx: strips lsbs from the hmtx table. Based on the idea that lsbs can be
reconstructed from bbox.
hdmx: performs the delta coding on hdmx, essentially the same as MTX.
cmap: compresses cmap table: wire format representation is inverse of cmap
table plus exceptions (one glyph encoded by multiple character codes).
kern: compresses kern table (not robust, intended just for rough testing).
strip: the subparameters are a list of tables to be stripped entirely
(comma-separated).
The string roughly corresponding to MTX is:
glyf/cbbox,code,triplet,push,hop:hdmx:gzip
Meaning: glyph encoding is used, with simple glyph bboxes stripped (but
composite glyph bboxes included), triplet coding, push sequences, and hop
codes. The hdmx table is compressed. And finally, gzip is used as the entropy
coder.
This differs from MTX in a number of small ways: LZCOMP is not exactly the same
as gzip. MTX uses three separate compression streams (the base font including
triplet-coded glyph data), the bytecodes, and the push sequences, while this
test uses a single stream. MTX also compresses the CVT table (an upper bound on
the impact of this can be estimated by testing strip/cvt)
Lastly, as a point of methodology, the code by default strips the “dsig” table,
which would be invalidated by any non-bit-identical change to the font data. If
it is desired to keep this table, add the “keepdsig” stage.
The string representing the currently most aggressive optimization level is:
glyf/triplet,code,push,reslice:hdmx:hmtx:cmap:kern:lzma
In addition to the MTX one above, it strips the bboxes from composite glyphs,
reslices the glyf table, compresses the htmx, cmap, and kern tables, and uses
lzma as the entropy coding.
The string corresponding to the current WOFF Ultra Condensed draft spec
document is:
glyf/cbbox,triplet,code,reslice:woff2/lzma
The current C++ codebase can roundtrip compressed files as long as no per-table
entropy coding is specified, as below (this will be fixed soon).
glyf/cbbox,triplet,code,reslice:woff2
== Running the tool ==
java -jar build/jar/compression.jar *.ttf > chart.html
The tool takes a list of OpenType fonts on the commandline, and generates an
HTML chart, which it simply outputs to stdout. This chart uses the Google Chart
API for plotting.
Options:
-b <desc>
Sets the baseline experiment description.
[ -x <desc> ]...
Sets an experiment description. Can be used multiple times.
-o
Outputs the actual compressed file, substituting ".wof2" for ".ttf" in
the input file name. Only useful when a single -x parameter is specified.
= Decompressing the fonts =
See the cpp/ directory (including cpp/README) for the C++ implementation of
decompression. This code is based on OTS, and successfully roundtrips the
basic compression as described in the draft spec.
= References =
sfntly: http://code.google.com/p/sfntly/ Guava:
http://code.google.com/p/guava-libraries/ MTX:
http://www.w3.org/Submission/MTX/
Also please refer to documents (currently Google Docs):
WOFF Ultra Condensed file format: proposals and discussion of wire format
issues
WIFF Ultra Condensed: more discussion of results and compression techniques.
This tool was used to prepare the data in that document.
+31
View File
@@ -0,0 +1,31 @@
<project name="compression" default="jar">
<target name="clean">
<delete dir="build/classes" />
<delete dir="build/jar" />
</target>
<target name="compile">
<mkdir dir="build/classes" />
<javac srcdir="src" destdir="build/classes" includeantruntime="false">
<compilerarg value="-Xlint" />
<classpath>
<fileset dir="lib" includes="*.jar" />
</classpath>
</javac>
</target>
<target name="jar" depends="compile">
<mkdir dir="build/jar" />
<jar destfile="build/jar/compression.jar" basedir="build/classes">
<zipfileset src="lib/eotconverter.jar" />
<zipfileset src="lib/guava-11.0.1.jar" />
<zipfileset src="lib/sfntly.jar" />
<zipfileset src="lib/woffconverter.jar" />
<manifest>
<attribute name="Main-Class" value="com.google.typography.font.compression.CompressionRunner" />
</manifest>
</jar>
</target>
</project>
Binary file not shown.
Binary file not shown.
Binary file not shown.
+44
View File
@@ -0,0 +1,44 @@
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
@@ -0,0 +1,52 @@
# 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
@@ -0,0 +1,61 @@
// 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;
}
+948
View File
@@ -0,0 +1,948 @@
// 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.
// This is the implementation of decompression of the proposed WOFF Ultra
// Condensed file format.
// For now, use of LZMA is conditional, because the build is trickier. When
// that gets all sorted out, we can get rid of these ifdefs.
#define USE_LZMA
#ifdef USE_LZMA
#include "third_party/lzma_sdk/LzmaLib.h"
#endif
#include <zlib.h>
#include <vector>
#include "opentype-sanitiser.h"
#include "ots-memory-stream.h"
#include "ots.h"
#include "woff2.h"
namespace {
// simple glyph flags
const int kGlyfOnCurve = 1 << 0;
const int kGlyfXShort = 1 << 1;
const int kGlyfYShort = 1 << 2;
const int kGlyfRepeat = 1 << 3;
const int kGlyfThisXIsSame = 1 << 4;
const int kGlyfThisYIsSame = 1 << 5;
// composite glyph flags
const int FLAG_ARG_1_AND_2_ARE_WORDS = 1 << 0;
const int FLAG_ARGS_ARE_XY_VALUES = 1 << 1;
const int FLAG_ROUND_XY_TO_GRID = 1 << 2;
const int FLAG_WE_HAVE_A_SCALE = 1 << 3;
const int FLAG_RESERVED = 1 << 4;
const int FLAG_MORE_COMPONENTS = 1 << 5;
const int FLAG_WE_HAVE_AN_X_AND_Y_SCALE = 1 << 6;
const int FLAG_WE_HAVE_A_TWO_BY_TWO = 1 << 7;
const int FLAG_WE_HAVE_INSTRUCTIONS = 1 << 8;
const int FLAG_USE_MY_METRICS = 1 << 9;
const int FLAG_OVERLAP_COMPOUND = 1 << 10;
const int FLAG_SCALED_COMPONENT_OFFSET = 1 << 11;
const int FLAG_UNSCALED_COMPONENT_OFFSET = 1 << 12;
const size_t kSfntHeaderSize = 12;
const size_t kSfntEntrySize = 16;
const size_t kCheckSumAdjustmentOffset = 8;
const size_t kEndPtsOfContoursOffset = 10;
const size_t kCompositeGlyphBegin = 10;
// Note that the byte order is big-endian, not the same as ots.cc
#define TAG(a, b, c, d) ((a << 24) | (b << 16) | (c << 8) | d)
const unsigned int kWoff2FlagsContinueStream = 1 << 4;
const unsigned int kWoff2FlagsTransform = 1 << 5;
const size_t kWoff2HeaderSize = 44;
const size_t kWoff2EntrySize = 20;
const size_t kLzmaHeaderSize = 13;
const uint32_t kCompressionTypeMask = 0xf;
const uint32_t kCompressionTypeNone = 0;
const uint32_t kCompressionTypeGzip = 1;
const uint32_t kCompressionTypeLzma = 2;
struct Point {
int x;
int y;
bool on_curve;
};
struct Table {
uint32_t tag;
uint32_t flags;
uint32_t src_offset;
uint32_t src_length;
uint32_t transform_length;
uint32_t dst_offset;
uint32_t dst_length;
};
// Based on section 6.1.1 of MicroType Express draft spec
bool Read255UShort(ots::Buffer *buf, unsigned int *value) {
const int kWordCode = 253;
const int kOneMoreByteCode2 = 254;
const int kOneMoreByteCode1 = 255;
const int kLowestUCode = 253;
uint8_t code = 0;
if (!buf->ReadU8(&code)) {
return OTS_FAILURE();
}
if (code == kWordCode) {
uint16_t result = 0;
if (!buf->ReadU16(&result)) {
return OTS_FAILURE();
}
*value = result;
return true;
} else if (code == kOneMoreByteCode1) {
uint8_t result = 0;
if (!buf->ReadU8(&result)) {
return OTS_FAILURE();
}
*value = result + kLowestUCode;
return true;
} else if (code == kOneMoreByteCode2) {
uint8_t result = 0;
if (!buf->ReadU8(&result)) {
return OTS_FAILURE();
}
*value = result + kLowestUCode * 2;
return true;
} else {
*value = code;
return true;
}
}
bool ReadBase128(ots::Buffer *buf, uint32_t *value) {
uint32_t result = 0;
for (size_t i = 0; i < 5; ++i) {
uint8_t code = 0;
if (!buf->ReadU8(&code)) {
return OTS_FAILURE();
}
result = (result << 7) | (code & 0x7f);
if ((code & 0x80) == 0) {
*value = result;
return true;
}
}
// Make sure not to exceed the size bound
return OTS_FAILURE();
}
size_t StoreU32(uint8_t *dst, size_t offset, uint32_t x) {
dst[offset] = x >> 24;
dst[offset + 1] = (x >> 16) & 0xff;
dst[offset + 2] = (x >> 8) & 0xff;
dst[offset + 3] = x & 0xff;
return offset + 4;
}
size_t Store16(uint8_t *dst, size_t offset, int x) {
dst[offset] = x >> 8;
dst[offset + 1] = x & 0xff;
return offset + 2;
}
int WithSign(int flag, int baseval) {
return (flag & 1) ? baseval : -baseval;
}
bool TripletDecode(const uint8_t *flags_in, const uint8_t *in, size_t in_size,
unsigned int n_points, std::vector<Point> *result,
size_t *in_bytes_consumed) {
int x = 0;
int y = 0;
if (n_points > in_size) {
return false;
}
unsigned int triplet_index = 0;
for (unsigned int i = 0; i < n_points; ++i) {
uint8_t flag = flags_in[i];
bool on_curve = !(flag >> 7);
flag &= 0x7f;
int n_data_bytes;
if (flag < 84) {
n_data_bytes = 1;
} else if (flag < 120) {
n_data_bytes = 2;
} else if (flag < 124) {
n_data_bytes = 3;
} else {
n_data_bytes = 4;
}
#if 0
fprintf(stderr, "flag = %d:", flag);
for (int j = 0; j < n_data_bytes; ++j) {
fprintf(stderr, " %d", in[triplet_index + j]);
}
fprintf(stderr, "\n");
#endif
if (triplet_index + n_data_bytes > in_size ||
triplet_index + n_data_bytes < triplet_index) {
return OTS_FAILURE();
}
int dx, dy;
if (flag < 10) {
dx = 0;
dy = WithSign(flag, ((flag & 14) << 7) + in[triplet_index]);
} else if (flag < 20) {
dx = WithSign(flag, (((flag - 10) & 14) << 7) + in[triplet_index]);
dy = 0;
} else if (flag < 84) {
int b0 = flag - 20;
int b1 = in[triplet_index];
dx = WithSign(flag, 1 + (b0 & 0x30) + (b1 >> 4));
dy = WithSign(flag >> 1, 1 + ((b0 & 0x0c) << 2) + (b1 & 0x0f));
} else if (flag < 120) {
int b0 = flag - 84;
dx = WithSign(flag, 1 + ((b0 / 12) << 8) + in[triplet_index]);
dy = WithSign(flag >> 1,
1 + (((b0 % 12) >> 2) << 8) + in[triplet_index + 1]);
} else if (flag < 124) {
int b2 = in[triplet_index + 1];
dx = WithSign(flag, (in[triplet_index] << 4) + (b2 >> 4));
dy = WithSign(flag >> 1, ((b2 & 0x0f) << 8) + in[triplet_index + 2]);
} else {
dx = WithSign(flag, (in[triplet_index] << 8) + in[triplet_index + 1]);
dy = WithSign(flag >> 1,
(in[triplet_index + 2] << 8) + in[triplet_index + 3]);
}
triplet_index += n_data_bytes;
x += dx;
y += dy;
result->push_back(Point());
Point &back = result->back();
back.x = x;
back.y = y;
back.on_curve = on_curve;
// fprintf(stderr, "point %d: %d %d %s (delta %d %d)\n",
// i, x, y, on_curve ? "on" : "off", dx, dy);
}
*in_bytes_consumed = triplet_index;
return true;
}
// This function stores just the point data. On entry, dst points to the
// beginning of a simple glyph. Returns true on success.
bool StorePoints(const std::vector<Point> &points,
unsigned int n_contours, unsigned int instruction_length,
uint8_t *dst, size_t dst_size, size_t *glyph_size) {
unsigned int flag_offset = kEndPtsOfContoursOffset + 2 * n_contours + 2 +
instruction_length;
int last_flag = -1;
int repeat_count = 0;
int last_x = 0;
int last_y = 0;
int x_bytes = 0;
int y_bytes = 0;
for (unsigned int i = 0; i < points.size(); ++i) {
const Point &point = points[i];
int flag = point.on_curve ? kGlyfOnCurve : 0;
int dx = point.x - last_x;
int dy = point.y - last_y;
if (dx == 0) {
flag |= kGlyfThisXIsSame;
} else if (dx > -256 && dx < 256) {
flag |= kGlyfXShort | (dx > 0 ? kGlyfThisXIsSame : 0);
x_bytes += 1;
} else {
x_bytes += 2;
}
if (dy == 0) {
flag |= kGlyfThisYIsSame;
} else if (dy > -256 && dy < 256) {
flag |= kGlyfYShort | (dy > 0 ? kGlyfThisYIsSame : 0);
y_bytes += 1;
} else {
y_bytes += 2;
}
// fprintf(stderr, "nominal flag = %d\n", flag);
if (flag == last_flag && repeat_count != 255) {
dst[flag_offset - 1] |= kGlyfRepeat;
repeat_count++;
} else {
if (repeat_count != 0) {
if (flag_offset >= dst_size) return OTS_FAILURE();
dst[flag_offset++] = repeat_count;
}
if (flag_offset >= dst_size) return OTS_FAILURE();
dst[flag_offset++] = flag;
repeat_count = 0;
}
last_x = point.x;
last_y = point.y;
last_flag = flag;
}
if (repeat_count != 0) {
if (flag_offset >= dst_size) return OTS_FAILURE();
dst[flag_offset++] = repeat_count;
}
if (flag_offset + x_bytes + y_bytes > dst_size ||
flag_offset + x_bytes + y_bytes < flag_offset) {
return OTS_FAILURE();
}
int x_offset = flag_offset;
int y_offset = flag_offset + x_bytes;
last_x = 0;
last_y = 0;
for (unsigned int i = 0; i < points.size(); ++i) {
int dx = points[i].x - last_x;
if (dx == 0) {
// pass
} else if (dx > -256 && dx < 256) {
dst[x_offset++] = std::abs(dx);
} else {
x_offset = Store16(dst, x_offset, dx);
}
last_x += dx;
int dy = points[i].y - last_y;
if (dy == 0) {
// pass
} else if (dy > -256 && dy < 256) {
dst[y_offset++] = std::abs(dy);
} else {
y_offset = Store16(dst, y_offset, dy);
}
last_y += dy;
}
*glyph_size = y_offset;
return true;
}
// Compute the bounding box of the coordinates, and store into a glyf buffer.
// A precondition is that there are at least 10 bytes available.
void ComputeBbox(const std::vector<Point> &points, uint8_t *dst) {
int x_min = 0;
int y_min = 0;
int x_max = 0;
int y_max = 0;
for (unsigned int i = 0; i < points.size(); ++i) {
int x = points[i].x;
int y = points[i].y;
if (i == 0 || x < x_min) x_min = x;
if (i == 0 || x > x_max) x_max = x;
if (i == 0 || y < y_min) y_min = y;
if (i == 0 || y > y_max) y_max = y;
}
size_t offset = 2;
offset = Store16(dst, offset, x_min);
offset = Store16(dst, offset, y_min);
offset = Store16(dst, offset, x_max);
offset = Store16(dst, offset, y_max);
}
// Process entire bbox stream. This is done as a separate pass to allow for
// composite bbox computations (an optional more aggressive transform).
bool ProcessBboxStream(ots::Buffer *bbox_stream, unsigned int n_glyphs,
const std::vector<uint32_t> &loca_values, uint8_t *glyf_buf) {
const uint8_t *buf = bbox_stream->buffer();
unsigned int bitmap_length = ((n_glyphs + 31) >> 5) << 2;
if (bbox_stream->length() < bitmap_length) {
return OTS_FAILURE();
}
bbox_stream->Skip(bitmap_length);
for (unsigned int i = 0; i < n_glyphs; ++i) {
if (buf[i >> 3] & (0x80 >> (i & 7))) {
uint32_t loca_offset = loca_values[i];
if (loca_values[i + 1] - loca_offset < kEndPtsOfContoursOffset) {
return OTS_FAILURE();
}
bbox_stream->Read(glyf_buf + loca_offset + 2, 8);
}
}
return true;
}
bool ProcessComposite(ots::Buffer *composite_stream, uint8_t *dst,
size_t dst_size, size_t *glyph_size, bool *have_instructions) {
size_t start_offset = composite_stream->offset();
bool we_have_instructions = false;
uint16_t flags = FLAG_MORE_COMPONENTS;
while (flags & FLAG_MORE_COMPONENTS) {
if (!composite_stream->ReadU16(&flags)) {
return OTS_FAILURE();
}
we_have_instructions |= (flags & FLAG_WE_HAVE_INSTRUCTIONS) != 0;
size_t arg_size = 2; // glyph index
if (flags & FLAG_ARG_1_AND_2_ARE_WORDS) {
arg_size += 4;
} else {
arg_size += 2;
}
if (flags & FLAG_WE_HAVE_A_SCALE) {
arg_size += 2;
} else if (flags & FLAG_WE_HAVE_AN_X_AND_Y_SCALE) {
arg_size += 4;
} else if (flags & FLAG_WE_HAVE_A_TWO_BY_TWO) {
arg_size += 8;
}
if (!composite_stream->Skip(arg_size)) {
return OTS_FAILURE();
}
//fprintf(stderr, "flags = %04x, arg_size = %d\n", flags, arg_size);
}
size_t composite_glyph_size = composite_stream->offset() - start_offset;
if (composite_glyph_size + kCompositeGlyphBegin > dst_size) {
return OTS_FAILURE();
}
Store16(dst, 0, 0xffff); // nContours = -1 for composite glyph
std::memcpy(dst + kCompositeGlyphBegin,
composite_stream->buffer() + start_offset,
composite_glyph_size);
*glyph_size = kCompositeGlyphBegin + composite_glyph_size;
*have_instructions = we_have_instructions;
return true;
}
// Build TrueType loca table
bool StoreLoca(const std::vector<uint32_t> &loca_values, int index_format,
uint8_t *dst, size_t dst_size) {
size_t loca_size = loca_values.size();
size_t offset_size = index_format ? 4 : 2;
if (offset_size * loca_size > dst_size) {
return OTS_FAILURE();
}
size_t offset = 0;
for (size_t i = 0; i < loca_values.size(); ++i) {
int value = loca_values[i];
if (index_format) {
offset = StoreU32(dst, offset, value);
} else {
offset = Store16(dst, offset, value >> 1);
}
}
return true;
}
// Reconstruct entire glyf table based on transformed original
bool ReconstructGlyf(const uint8_t *data, size_t data_size,
uint8_t *dst, size_t dst_size,
uint8_t *loca_buf, size_t loca_size) {
ots::Buffer file(data, data_size);
uint32_t version;
const int kNumSubStreams = 7;
std::vector<std::pair<const uint8_t*, size_t> > substreams(kNumSubStreams);
if (!file.ReadU32(&version)) {
return OTS_FAILURE();
}
uint16_t num_glyphs = 0;
uint16_t index_format = 0;
if (!file.ReadU16(&num_glyphs) ||
!file.ReadU16(&index_format)) {
return OTS_FAILURE();
}
// fprintf(stderr, "num_glyphs = %d\n", num_glyphs);
unsigned int offset = (2 + kNumSubStreams) * 4;
for (int i = 0; i < kNumSubStreams; ++i) {
uint32_t substream_size = 0;
if (!file.ReadU32(&substream_size)) {
return OTS_FAILURE();
}
if (substream_size > data_size - offset) {
return OTS_FAILURE();
}
// fprintf(stderr, "substream size = %d\n", substream_size);
substreams[i] = std::make_pair(data + offset, substream_size);
offset += substream_size;
}
ots::Buffer n_contour_stream(substreams[0].first, substreams[0].second);
ots::Buffer n_points_stream(substreams[1].first, substreams[1].second);
ots::Buffer flag_stream(substreams[2].first, substreams[2].second);
ots::Buffer glyph_stream(substreams[3].first, substreams[3].second);
ots::Buffer composite_stream(substreams[4].first, substreams[4].second);
ots::Buffer bbox_stream(substreams[5].first, substreams[5].second);
ots::Buffer instruction_stream(substreams[6].first, substreams[6].second);
std::vector<uint32_t> loca_values(num_glyphs + 1);
std::vector<unsigned int> n_points_vec;
std::vector<Point> points;
uint32_t loca_offset = 0;
for (unsigned int i = 0; i < num_glyphs; ++i) {
size_t glyph_size = 0;
uint16_t n_contours = 0;
if (!n_contour_stream.ReadU16(&n_contours)) {
return OTS_FAILURE();
}
// fprintf(stderr, "n_contours[%d] = %d\n", i, n_contours);
uint8_t *glyf_dst = dst + loca_offset;
size_t glyf_dst_size = dst_size - loca_offset;
if (n_contours == 0xffff) {
// composite glyph
bool have_instructions = false;
unsigned int instruction_size = 0;
if (!ProcessComposite(&composite_stream, glyf_dst, glyf_dst_size,
&glyph_size, &have_instructions)) {
return OTS_FAILURE();
}
if (have_instructions) {
if (!Read255UShort(&glyph_stream, &instruction_size)) {
return OTS_FAILURE();
}
if (instruction_size + 2 > glyf_dst_size - glyph_size) {
return OTS_FAILURE();
}
Store16(glyf_dst, glyph_size, instruction_size);
if (!instruction_stream.Read(glyf_dst + glyph_size + 2,
instruction_size)) {
return OTS_FAILURE();
}
glyph_size += instruction_size + 2;
}
} else if (n_contours > 0) {
// simple glyph
n_points_vec.clear();
points.clear();
unsigned int total_n_points = 0;
unsigned int n_points_contour;
for (unsigned int j = 0; j < n_contours; ++j) {
if (!Read255UShort(&n_points_stream, &n_points_contour)) {
return OTS_FAILURE();
}
n_points_vec.push_back(n_points_contour);
total_n_points += n_points_contour;
}
unsigned int flag_size = total_n_points;
if (flag_size > flag_stream.length() - flag_stream.offset()) {
return OTS_FAILURE();
}
const uint8_t *flags_buf = flag_stream.buffer() + flag_stream.offset();
const uint8_t *triplet_buf = glyph_stream.buffer() +
glyph_stream.offset();
size_t triplet_size = glyph_stream.length() - glyph_stream.offset();
size_t triplet_bytes_consumed = 0;
if (!TripletDecode(flags_buf, triplet_buf, triplet_size, total_n_points,
&points, &triplet_bytes_consumed)) {
return OTS_FAILURE();
}
if (glyf_dst_size < kEndPtsOfContoursOffset + 2 * n_contours) {
return OTS_FAILURE();
}
Store16(glyf_dst, 0, n_contours);
ComputeBbox(points, glyf_dst);
size_t offset = kEndPtsOfContoursOffset;
int end_point = -1;
for (unsigned int contour_ix = 0; contour_ix < n_contours; ++contour_ix) {
end_point += n_points_vec[contour_ix];
offset = Store16(glyf_dst, offset, end_point);
}
flag_stream.Skip(flag_size);
glyph_stream.Skip(triplet_bytes_consumed);
unsigned int instruction_size;
if (!Read255UShort(&glyph_stream, &instruction_size)) {
return OTS_FAILURE();
}
// fprintf(stderr, "%d: instruction size = %d\n", i, instruction_size);
uint8_t *instruction_dst = glyf_dst + kEndPtsOfContoursOffset +
2 * n_contours;
Store16(instruction_dst, 0, instruction_size);
if (!instruction_stream.Read(instruction_dst + 2, instruction_size)) {
return OTS_FAILURE();
}
if (!StorePoints(points, n_contours, instruction_size,
glyf_dst, glyf_dst_size, &glyph_size)) {
return OTS_FAILURE();
}
} else {
glyph_size = 0;
}
loca_values[i] = loca_offset;
if (glyph_size + 3 < glyph_size) {
return OTS_FAILURE();
}
// Round up to 4-byte alignment
glyph_size = (glyph_size + 3) & -4;
if (glyph_size > dst_size - loca_offset) {
// This shouldn't happen, but this test defensively maintains the
// invariant that loca_offset <= dst_size.
return OTS_FAILURE();
}
loca_offset += glyph_size;
}
loca_values[num_glyphs] = loca_offset;
if (!ProcessBboxStream(&bbox_stream, num_glyphs, loca_values, dst)) {
return OTS_FAILURE();
}
return StoreLoca(loca_values, index_format, loca_buf, loca_size);
}
// This is linear search, but could be changed to binary because we
// do have a guarantee that the tables are sorted by tag. But the total
// cpu time is expected to be very small in any case.
const Table *FindTable(const std::vector<Table> &tables, uint32_t tag) {
size_t n_tables = tables.size();
for (size_t i = 0; i < n_tables; ++i) {
if (tables[i].tag == tag) {
return &tables[i];
}
}
return NULL;
}
bool ReconstructTransformed(const std::vector<Table> &tables, uint32_t tag,
const uint8_t *transformed_buf, size_t transformed_size,
uint8_t *dst) {
if (tag == TAG('g', 'l', 'y', 'f')) {
const Table *glyf_table = FindTable(tables, tag);
const Table *loca_table = FindTable(tables, TAG('l', 'o', 'c', 'a'));
if (glyf_table == NULL || loca_table == NULL) {
return OTS_FAILURE();
}
return ReconstructGlyf(transformed_buf, transformed_size,
dst + glyf_table->dst_offset, glyf_table->dst_length,
dst + loca_table->dst_offset, loca_table->dst_offset);
} else if (tag == TAG('l', 'o', 'c', 'a')) {
// processing was already done by glyf table, but validate
if (!FindTable(tables, TAG('g', 'l', 'y', 'f'))) {
return OTS_FAILURE();
}
} else {
// transform for the tag is not known
return OTS_FAILURE();
}
return true;
}
// TODO: copied from ots.cc, probably shouldn't be duplicated.
// Round a value up to the nearest multiple of 4. Don't round the value in the
// case that rounding up overflows.
template<typename T> T Round4(T value) {
if (std::numeric_limits<T>::max() - value < 3) {
return value;
}
return (value + 3) & ~3;
}
uint32_t ComputeChecksum(const uint8_t *buf, size_t size) {
uint32_t checksum = 0;
for (size_t i = 0; i < size; i += 4) {
// We assume the addition is mod 2^32. This is a pretty safe assumption,
// but technically it's undefined behavior.
checksum += (buf[i] << 24) | (buf[i + 1] << 16) |
(buf[i + 2] << 8) | buf[i + 3];
}
return checksum;
}
bool FixChecksums(const std::vector<Table> &tables, uint8_t *dst) {
const Table *head_table = FindTable(tables, TAG('h', 'e', 'a', 'd'));
if (head_table == NULL ||
head_table->dst_length < kCheckSumAdjustmentOffset + 4) {
return OTS_FAILURE();
}
size_t adjustment_offset = head_table->dst_offset + kCheckSumAdjustmentOffset;
StoreU32(dst, adjustment_offset, 0);
size_t n_tables = tables.size();
uint32_t file_checksum = 0;
for (size_t i = 0; i < n_tables; ++i) {
const Table *table = &tables[i];
size_t table_length = table->dst_length;
uint8_t *table_data = dst + table->dst_offset;
uint32_t checksum = ComputeChecksum(table_data, table_length);
StoreU32(dst, kSfntHeaderSize + i * kSfntEntrySize + 4, checksum);
file_checksum += checksum;
}
file_checksum += ComputeChecksum(dst,
kSfntHeaderSize + kSfntEntrySize * n_tables);
uint32_t checksum_adjustment = 0xb1b0afba - file_checksum;
StoreU32(dst, adjustment_offset, checksum_adjustment);
return true;
}
bool Woff2Uncompress(uint8_t *dst_buf, size_t dst_size,
const uint8_t *src_buf, size_t src_size, uint32_t compression_type) {
if (compression_type == kCompressionTypeGzip) {
uLongf uncompressed_length = dst_size;
int r = uncompress((Bytef *)dst_buf, &uncompressed_length,
src_buf, src_size);
if (r != Z_OK || uncompressed_length != src_size) {
return OTS_FAILURE();
}
return true;
#ifdef USE_LZMA
} else if (compression_type == kCompressionTypeLzma) {
if (src_size < kLzmaHeaderSize) {
// Make sure we have at least a full Lzma header
return OTS_FAILURE();
}
// TODO: check that size matches (or elide size?)
size_t uncompressed_size = dst_size;
size_t compressed_size = src_size;
int result = LzmaUncompress(dst_buf, &dst_size,
src_buf + kLzmaHeaderSize, &compressed_size,
src_buf, LZMA_PROPS_SIZE);
if (result != SZ_OK || uncompressed_size != dst_size) {
return OTS_FAILURE();
}
return true;
#endif
}
// Unknown compression type
return OTS_FAILURE();
}
bool ReadLongDirectory(ots::Buffer *file, std::vector<Table> *tables,
size_t num_tables) {
for (size_t i = 0; i < num_tables; ++i) {
Table *table = &(*tables)[i];
if (!file->ReadU32(&table->tag) ||
!file->ReadU32(&table->flags) ||
!file->ReadU32(&table->src_length) ||
!file->ReadU32(&table->transform_length) ||
!file->ReadU32(&table->dst_length)) {
return OTS_FAILURE();
}
}
return true;
}
const uint32_t known_tags[29] = {
TAG('c', 'm', 'a', 'p'), // 0
TAG('h', 'e', 'a', 'd'), // 1
TAG('h', 'h', 'e', 'a'), // 2
TAG('h', 'm', 't', 'x'), // 3
TAG('m', 'a', 'x', 'p'), // 4
TAG('n', 'a', 'm', 'e'), // 5
TAG('O', 'S', '/', '2'), // 6
TAG('p', 'o', 's', 't'), // 7
TAG('c', 'v', 't', ' '), // 8
TAG('f', 'p', 'g', 'm'), // 9
TAG('g', 'l', 'y', 'f'), // 10
TAG('l', 'o', 'c', 'a'), // 11
TAG('p', 'r', 'e', 'p'), // 12
TAG('C', 'F', 'F', ' '), // 13
TAG('V', 'O', 'R', 'G'), // 14
TAG('E', 'B', 'D', 'T'), // 15
TAG('E', 'B', 'L', 'C'), // 16
TAG('g', 'a', 's', 'p'), // 17
TAG('h', 'd', 'm', 'x'), // 18
TAG('k', 'e', 'r', 'n'), // 19
TAG('L', 'T', 'S', 'H'), // 20
TAG('P', 'C', 'L', 'T'), // 21
TAG('V', 'D', 'M', 'X'), // 22
TAG('v', 'h', 'e', 'a'), // 23
TAG('v', 'm', 't', 'x'), // 24
TAG('B', 'A', 'S', 'E'), // 25
TAG('G', 'D', 'E', 'F'), // 26
TAG('G', 'P', 'O', 'S'), // 27
TAG('G', 'S', 'U', 'B'), // 28
};
bool ReadShortDirectory(ots::Buffer *file, std::vector<Table> *tables,
size_t num_tables) {
uint32_t last_compression_type = 0;
for (size_t i = 0; i < num_tables; ++i) {
Table *table = &(*tables)[i];
uint8_t flag_byte = 0;
if (!file->ReadU8(&flag_byte)) {
return OTS_FAILURE();
}
uint32_t tag = 0;
if ((flag_byte & 0x1f) == 0x1f) {
if (!file->ReadU32(&tag)) {
return OTS_FAILURE();
}
} else {
tag = known_tags[flag_byte & 0x1f];
}
uint32_t flags = flag_byte >> 6;
if (flags == 3) {
flags = last_compression_type | kWoff2FlagsContinueStream;
} else {
last_compression_type = flags;
}
if ((flag_byte & 0x20) != 0) {
flags |= kWoff2FlagsTransform;
}
uint32_t dst_length = 0;
if (!ReadBase128(file, &dst_length)) {
return OTS_FAILURE();
}
uint32_t transform_length = dst_length;
if ((flags & kWoff2FlagsTransform) != 0) {
if (!ReadBase128(file, &transform_length)) {
return OTS_FAILURE();
}
}
uint32_t src_length = transform_length;
if ((flag_byte >> 6) == 1 | (flag_byte >> 6) == 2) {
if (!ReadBase128(file, &src_length)) {
return OTS_FAILURE();
}
}
table->tag = tag;
table->flags = flags;
table->src_length = src_length;
table->transform_length = transform_length;
table->dst_length = dst_length;
}
return true;
}
} // namespace
namespace ots {
size_t ComputeWOFF2FinalSize(const uint8_t *data, size_t length) {
ots::Buffer file(data, length);
file.Skip(16);
uint32_t total_length = 0;
if (!file.ReadU32(&total_length)) {
return OTS_FAILURE();
}
return total_length;
}
bool ConvertWOFF2ToTTF(uint8_t *result, size_t result_length,
const uint8_t *data, size_t length) {
ots::Buffer file(data, length);
uint32_t signature = 0;
uint32_t flavor = 0;
if (!file.ReadU32(&signature) || signature != 0x774f4632 ||
!file.ReadU32(&flavor)) {
return OTS_FAILURE();
}
file.Skip(4);
uint16_t num_tables = 0;
if (!file.ReadU16(&num_tables)) {
return OTS_FAILURE();
}
file.Skip(30);
std::vector<Table> tables(num_tables);
// Note: change below to ReadLongDirectory to enable long format.
if (!ReadShortDirectory(&file, &tables, num_tables)) {
return OTS_FAILURE();
}
size_t src_offset = file.offset();
size_t dst_offset = kSfntHeaderSize + kSfntEntrySize * num_tables;
size_t uncompressed_sum = 0;
for (int i = 0; i < num_tables; ++i) {
Table *table = &tables[i];
table->src_offset = src_offset;
if (src_offset + table->src_length < src_offset) {
return OTS_FAILURE();
}
src_offset += table->src_length;
src_offset = Round4(src_offset); // TODO: reconsider
table->dst_offset = dst_offset;
if (dst_offset + table->dst_length < dst_offset) {
return OTS_FAILURE();
}
dst_offset += table->dst_length;
dst_offset = Round4(dst_offset);
if ((table->flags & kCompressionTypeMask) != kCompressionTypeNone) {
if (uncompressed_sum + table->src_length < uncompressed_sum) {
return OTS_FAILURE();
}
uncompressed_sum += table->src_length;
}
}
// Enforce same 30M limit on uncompressed tables as OTS
if (uncompressed_sum > 30 * 1024 * 1024) {
return OTS_FAILURE();
}
if (dst_offset > result_length) {
return OTS_FAILURE();
}
// Start building the font
size_t offset = 0;
offset = StoreU32(result, offset, flavor);
offset = Store16(result, offset, num_tables);
unsigned max_pow2 = 0;
while (1u << (max_pow2 + 1) <= num_tables) {
max_pow2++;
}
const uint16_t output_search_range = (1u << max_pow2) << 4;
offset = Store16(result, offset, output_search_range);
offset = Store16(result, offset, max_pow2);
offset = Store16(result, offset, (num_tables << 4) - output_search_range);
for (int i = 0; i < num_tables; ++i) {
const Table *table = &tables[i];
offset = StoreU32(result, offset, table->tag);
offset = StoreU32(result, offset, 0); // checksum, to fill in later
offset = StoreU32(result, offset, table->dst_offset);
offset = StoreU32(result, offset, table->dst_length);
}
std::vector<uint8_t> uncompressed_buf;
bool continue_valid = false;
for (int i = 0; i < num_tables; ++i) {
const Table *table = &tables[i];
uint32_t flags = table->flags;
const uint8_t *src_buf = data + table->src_offset;
uint32_t compression_type = flags & kCompressionTypeMask;
const uint8_t *transform_buf = NULL;
size_t transform_length = table->transform_length;
if ((flags & kWoff2FlagsContinueStream) != 0) {
if (!continue_valid) {
return OTS_FAILURE();
}
} else if (compression_type == kCompressionTypeNone) {
if (transform_length != table->src_length) {
return OTS_FAILURE();
}
transform_buf = src_buf;
continue_valid = false;
} else if ((flags & kWoff2FlagsContinueStream) == 0) {
size_t total_size = transform_length;
for (int j = i + 1; j < num_tables; ++j) {
if ((tables[j].flags & kWoff2FlagsContinueStream) == 0) {
break;
}
if (total_size + tables[j].transform_length < total_size) {
return OTS_FAILURE();
}
total_size += tables[j].transform_length;
}
uncompressed_buf.resize(total_size);
if (!Woff2Uncompress(&uncompressed_buf[0], total_size,
src_buf, table->src_length, compression_type)) {
return OTS_FAILURE();
}
transform_buf = &uncompressed_buf[0];
continue_valid = true;
}
if ((flags & kWoff2FlagsTransform) == 0) {
if (transform_length != table->dst_length) {
return OTS_FAILURE();
}
std::memcpy(result + table->dst_offset, transform_buf,
transform_length);
} else {
if (!ReconstructTransformed(tables, table->tag,
transform_buf, transform_length, result)) {
return OTS_FAILURE();
}
}
if (continue_valid) {
transform_buf += transform_length;
}
}
return FixChecksums(tables, result);
}
} // namespace ots
+43
View File
@@ -0,0 +1,43 @@
{
'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
@@ -0,0 +1,21 @@
// 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.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+5500
View File
File diff suppressed because it is too large Load Diff
@@ -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;
}
}
}
+38
View File
@@ -0,0 +1,38 @@
# 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.
# This is a simple utility for dumping out the header of a compressed file, and
# is suitable for doing spot checks of compressed. files. However, this only
# implements the "long" form of the table directory.
import struct
import sys
def dump_woff2_header(header):
header_values = struct.unpack('>IIIHHIHHIIIII', header[:44])
for i, key in enumerate([
'signature',
'flavor',
'length',
'numTables',
'reserved',
'totalSfntSize',
'majorVersion',
'minorVersion',
'metaOffset',
'metaOrigLength',
'privOffset',
'privLength']):
print key, header_values[i]
numTables = header_values[3]
for i in range(numTables):
entry = struct.unpack('>IIIII', header[44+20*i:44+20*(i+1)])
print '%08x %d %d %d %d' % entry
def main():
header = file(sys.argv[1]).read()
dump_woff2_header(header)
main()