| // 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@google.com (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; |
| } |
| } |
| } |