Remove old files; use the ones in woff2/ instead
This commit is contained in:
Binary file not shown.
Binary file not shown.
-44
@@ -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.
|
|
||||||
@@ -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='.')
|
|
||||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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.
Binary file not shown.
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user