blob: 37125f06eadcc1c0038ec0fe9894d5ad222c3565 [file] [log] [blame]
// 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[]{ };
}
}