| /* Copyright 2014 Google Inc. All Rights Reserved. |
| |
| Distributed under MIT license. |
| See file LICENSE for detail or copy at https://opensource.org/licenses/MIT |
| */ |
| |
| /* Library for converting TTF format font files to their WOFF2 versions. */ |
| |
| #include <woff2/encode.h> |
| |
| #include <stdlib.h> |
| #include <complex> |
| #include <limits> |
| #include <string> |
| #include <vector> |
| |
| #if !defined(STARBOARD) |
| #include <cstring> |
| #else |
| #include "starboard/client_porting/poem/string_poem.h" |
| #endif |
| |
| #include <brotli/encode.h> |
| #include "./buffer.h" |
| #include "./font.h" |
| #include "./normalize.h" |
| #include "./round.h" |
| #include "./store_bytes.h" |
| #include "./table_tags.h" |
| #include "./transform.h" |
| #include "./variable_length.h" |
| #include "./woff2_common.h" |
| |
| namespace woff2 { |
| |
| namespace { |
| |
| |
| using std::string; |
| using std::vector; |
| |
| |
| const size_t kWoff2HeaderSize = 48; |
| const size_t kWoff2EntrySize = 20; |
| |
| bool Compress(const uint8_t* data, const size_t len, uint8_t* result, |
| uint32_t* result_len, BrotliEncoderMode mode, int quality) { |
| size_t compressed_len = *result_len; |
| if (BrotliEncoderCompress(quality, BROTLI_DEFAULT_WINDOW, mode, len, data, |
| &compressed_len, result) == 0) { |
| return false; |
| } |
| *result_len = compressed_len; |
| return true; |
| } |
| |
| bool Woff2Compress(const uint8_t* data, const size_t len, |
| uint8_t* result, uint32_t* result_len, |
| int quality) { |
| return Compress(data, len, result, result_len, |
| BROTLI_MODE_FONT, quality); |
| } |
| |
| bool TextCompress(const uint8_t* data, const size_t len, |
| uint8_t* result, uint32_t* result_len, |
| int quality) { |
| return Compress(data, len, result, result_len, |
| BROTLI_MODE_TEXT, quality); |
| } |
| |
| int KnownTableIndex(uint32_t tag) { |
| for (int i = 0; i < 63; ++i) { |
| if (tag == kKnownTags[i]) return i; |
| } |
| return 63; |
| } |
| |
| void StoreTableEntry(const Table& table, size_t* offset, uint8_t* dst) { |
| uint8_t flag_byte = (table.flags & 0xC0) | KnownTableIndex(table.tag); |
| dst[(*offset)++] = flag_byte; |
| // The index here is treated as a set of flag bytes because |
| // bits 6 and 7 of the byte are reserved for future use as flags. |
| // 0x3f or 63 means an arbitrary table tag. |
| if ((flag_byte & 0x3f) == 0x3f) { |
| StoreU32(table.tag, offset, dst); |
| } |
| StoreBase128(table.src_length, offset, dst); |
| if ((table.flags & kWoff2FlagsTransform) != 0) { |
| StoreBase128(table.transform_length, offset, dst); |
| } |
| } |
| |
| size_t TableEntrySize(const Table& table) { |
| uint8_t flag_byte = KnownTableIndex(table.tag); |
| size_t size = ((flag_byte & 0x3f) != 0x3f) ? 1 : 5; |
| size += Base128Size(table.src_length); |
| if ((table.flags & kWoff2FlagsTransform) != 0) { |
| size += Base128Size(table.transform_length); |
| } |
| return size; |
| } |
| |
| size_t ComputeWoff2Length(const FontCollection& font_collection, |
| const std::vector<Table>& tables, |
| std::map<std::pair<uint32_t, uint32_t>, uint16_t> |
| index_by_tag_offset, |
| size_t compressed_data_length, |
| size_t extended_metadata_length) { |
| size_t size = kWoff2HeaderSize; |
| |
| for (const auto& table : tables) { |
| size += TableEntrySize(table); |
| } |
| |
| // for collections only, collection tables |
| if (font_collection.flavor == kTtcFontFlavor) { |
| size += 4; // UInt32 Version of TTC Header |
| size += Size255UShort(font_collection.fonts.size()); // 255UInt16 numFonts |
| |
| size += 4 * font_collection.fonts.size(); // UInt32 flavor for each |
| |
| for (const auto& font : font_collection.fonts) { |
| size += Size255UShort(font.tables.size()); // 255UInt16 numTables |
| for (const auto& entry : font.tables) { |
| const Font::Table& table = entry.second; |
| // no collection entry for xform table |
| if (table.tag & 0x80808080) continue; |
| |
| std::pair<uint32_t, uint32_t> tag_offset(table.tag, table.offset); |
| uint16_t table_index = index_by_tag_offset[tag_offset]; |
| size += Size255UShort(table_index); // 255UInt16 index entry |
| } |
| } |
| } |
| |
| // compressed data |
| size += compressed_data_length; |
| size = Round4(size); |
| |
| size += extended_metadata_length; |
| return size; |
| } |
| |
| size_t ComputeUncompressedLength(const Font& font) { |
| // sfnt header + offset table |
| size_t size = 12 + 16 * font.num_tables; |
| for (const auto& entry : font.tables) { |
| const Font::Table& table = entry.second; |
| if (table.tag & 0x80808080) continue; // xform tables don't stay |
| if (table.IsReused()) continue; // don't have to pay twice |
| size += Round4(table.length); |
| } |
| return size; |
| } |
| |
| size_t ComputeUncompressedLength(const FontCollection& font_collection) { |
| if (font_collection.flavor != kTtcFontFlavor) { |
| return ComputeUncompressedLength(font_collection.fonts[0]); |
| } |
| size_t size = CollectionHeaderSize(font_collection.header_version, |
| font_collection.fonts.size()); |
| for (const auto& font : font_collection.fonts) { |
| size += ComputeUncompressedLength(font); |
| } |
| return size; |
| } |
| |
| size_t ComputeTotalTransformLength(const Font& font) { |
| size_t total = 0; |
| for (const auto& i : font.tables) { |
| const Font::Table& table = i.second; |
| if (table.IsReused()) { |
| continue; |
| } |
| if (table.tag & 0x80808080 || !font.FindTable(table.tag ^ 0x80808080)) { |
| // Count transformed tables and non-transformed tables that do not have |
| // transformed versions. |
| total += table.length; |
| } |
| } |
| return total; |
| } |
| |
| } // namespace |
| |
| size_t MaxWOFF2CompressedSize(const uint8_t* data, size_t length) { |
| return MaxWOFF2CompressedSize(data, length, ""); |
| } |
| |
| size_t MaxWOFF2CompressedSize(const uint8_t* data, size_t length, |
| const string& extended_metadata) { |
| // Except for the header size, which is 32 bytes larger in woff2 format, |
| // all other parts should be smaller (table header in short format, |
| // transformations and compression). Just to be sure, we will give some |
| // headroom anyway. |
| return length + 1024 + extended_metadata.length(); |
| } |
| |
| uint32_t CompressedBufferSize(uint32_t original_size) { |
| return 1.2 * original_size + 10240; |
| } |
| |
| bool TransformFontCollection(FontCollection* font_collection) { |
| for (auto& font : font_collection->fonts) { |
| if (!TransformGlyfAndLocaTables(&font)) { |
| #ifdef FONT_COMPRESSION_BIN |
| fprintf(stderr, "glyf/loca transformation failed.\n"); |
| #endif |
| return FONT_COMPRESSION_FAILURE(); |
| } |
| } |
| |
| return true; |
| } |
| |
| bool ConvertTTFToWOFF2(const uint8_t *data, size_t length, |
| uint8_t *result, size_t *result_length) { |
| WOFF2Params params; |
| return ConvertTTFToWOFF2(data, length, result, result_length, |
| params); |
| } |
| |
| bool ConvertTTFToWOFF2(const uint8_t *data, size_t length, |
| uint8_t *result, size_t *result_length, |
| const WOFF2Params& params) { |
| FontCollection font_collection; |
| if (!ReadFontCollection(data, length, &font_collection)) { |
| #ifdef FONT_COMPRESSION_BIN |
| fprintf(stderr, "Parsing of the input font failed.\n"); |
| #endif |
| return FONT_COMPRESSION_FAILURE(); |
| } |
| |
| if (!NormalizeFontCollection(&font_collection)) { |
| return FONT_COMPRESSION_FAILURE(); |
| } |
| |
| if (params.allow_transforms && !TransformFontCollection(&font_collection)) { |
| return FONT_COMPRESSION_FAILURE(); |
| } else { |
| // glyf/loca use 11 to flag "not transformed" |
| for (auto& font : font_collection.fonts) { |
| Font::Table* glyf_table = font.FindTable(kGlyfTableTag); |
| Font::Table* loca_table = font.FindTable(kLocaTableTag); |
| if (glyf_table) { |
| glyf_table->flag_byte |= 0xc0; |
| } |
| if (loca_table) { |
| loca_table->flag_byte |= 0xc0; |
| } |
| } |
| } |
| |
| // Although the compressed size of each table in the final woff2 file won't |
| // be larger than its transform_length, we have to allocate a large enough |
| // buffer for the compressor, since the compressor can potentially increase |
| // the size. If the compressor overflows this, it should return false and |
| // then this function will also return false. |
| |
| size_t total_transform_length = 0; |
| for (const auto& font : font_collection.fonts) { |
| total_transform_length += ComputeTotalTransformLength(font); |
| } |
| size_t compression_buffer_size = CompressedBufferSize(total_transform_length); |
| std::vector<uint8_t> compression_buf(compression_buffer_size); |
| uint32_t total_compressed_length = compression_buffer_size; |
| |
| // Collect all transformed data into one place in output order. |
| std::vector<uint8_t> transform_buf(total_transform_length); |
| size_t transform_offset = 0; |
| for (const auto& font : font_collection.fonts) { |
| for (const auto tag : font.OutputOrderedTags()) { |
| const Font::Table& original = font.tables.at(tag); |
| if (original.IsReused()) continue; |
| if (tag & 0x80808080) continue; |
| const Font::Table* table_to_store = font.FindTable(tag ^ 0x80808080); |
| if (table_to_store == NULL) table_to_store = &original; |
| |
| StoreBytes(table_to_store->data, table_to_store->length, |
| &transform_offset, &transform_buf[0]); |
| } |
| } |
| |
| // Compress all transformed data in one stream. |
| if (!Woff2Compress(transform_buf.data(), total_transform_length, |
| &compression_buf[0], |
| &total_compressed_length, |
| params.brotli_quality)) { |
| #ifdef FONT_COMPRESSION_BIN |
| fprintf(stderr, "Compression of combined table failed.\n"); |
| #endif |
| return FONT_COMPRESSION_FAILURE(); |
| } |
| |
| #ifdef FONT_COMPRESSION_BIN |
| fprintf(stderr, "Compressed %zu to %u.\n", total_transform_length, |
| total_compressed_length); |
| #endif |
| |
| // Compress the extended metadata |
| // TODO(user): how does this apply to collections |
| uint32_t compressed_metadata_buf_length = |
| CompressedBufferSize(params.extended_metadata.length()); |
| std::vector<uint8_t> compressed_metadata_buf(compressed_metadata_buf_length); |
| |
| if (params.extended_metadata.length() > 0) { |
| if (!TextCompress((const uint8_t*)params.extended_metadata.data(), |
| params.extended_metadata.length(), |
| compressed_metadata_buf.data(), |
| &compressed_metadata_buf_length, |
| params.brotli_quality)) { |
| #ifdef FONT_COMPRESSION_BIN |
| fprintf(stderr, "Compression of extended metadata failed.\n"); |
| #endif |
| return FONT_COMPRESSION_FAILURE(); |
| } |
| } else { |
| compressed_metadata_buf_length = 0; |
| } |
| |
| std::vector<Table> tables; |
| std::map<std::pair<uint32_t, uint32_t>, uint16_t> index_by_tag_offset; |
| |
| for (const auto& font : font_collection.fonts) { |
| |
| for (const auto tag : font.OutputOrderedTags()) { |
| const Font::Table& src_table = font.tables.at(tag); |
| if (src_table.IsReused()) { |
| continue; |
| } |
| |
| std::pair<uint32_t, uint32_t> tag_offset(src_table.tag, src_table.offset); |
| if (index_by_tag_offset.find(tag_offset) == index_by_tag_offset.end()) { |
| index_by_tag_offset[tag_offset] = tables.size(); |
| } else { |
| return false; |
| } |
| |
| Table table; |
| table.tag = src_table.tag; |
| table.flags = src_table.flag_byte; |
| table.src_length = src_table.length; |
| table.transform_length = src_table.length; |
| const uint8_t* transformed_data = src_table.data; |
| const Font::Table* transformed_table = |
| font.FindTable(src_table.tag ^ 0x80808080); |
| if (transformed_table != NULL) { |
| table.flags = transformed_table->flag_byte; |
| table.flags |= kWoff2FlagsTransform; |
| table.transform_length = transformed_table->length; |
| transformed_data = transformed_table->data; |
| |
| } |
| tables.push_back(table); |
| } |
| } |
| |
| size_t woff2_length = ComputeWoff2Length(font_collection, tables, |
| index_by_tag_offset, total_compressed_length, |
| compressed_metadata_buf_length); |
| if (woff2_length > *result_length) { |
| #ifdef FONT_COMPRESSION_BIN |
| fprintf(stderr, "Result allocation was too small (%zd vs %zd bytes).\n", |
| *result_length, woff2_length); |
| #endif |
| return FONT_COMPRESSION_FAILURE(); |
| } |
| *result_length = woff2_length; |
| |
| size_t offset = 0; |
| |
| // start of woff2 header (http://www.w3.org/TR/WOFF2/#woff20Header) |
| StoreU32(kWoff2Signature, &offset, result); |
| if (font_collection.flavor != kTtcFontFlavor) { |
| StoreU32(font_collection.fonts[0].flavor, &offset, result); |
| } else { |
| StoreU32(kTtcFontFlavor, &offset, result); |
| } |
| StoreU32(woff2_length, &offset, result); |
| Store16(tables.size(), &offset, result); |
| Store16(0, &offset, result); // reserved |
| // totalSfntSize |
| StoreU32(ComputeUncompressedLength(font_collection), &offset, result); |
| StoreU32(total_compressed_length, &offset, result); // totalCompressedSize |
| |
| // Let's just all be v1.0 |
| Store16(1, &offset, result); // majorVersion |
| Store16(0, &offset, result); // minorVersion |
| if (compressed_metadata_buf_length > 0) { |
| StoreU32(woff2_length - compressed_metadata_buf_length, |
| &offset, result); // metaOffset |
| StoreU32(compressed_metadata_buf_length, &offset, result); // metaLength |
| StoreU32(params.extended_metadata.length(), |
| &offset, result); // metaOrigLength |
| } else { |
| StoreU32(0, &offset, result); // metaOffset |
| StoreU32(0, &offset, result); // metaLength |
| StoreU32(0, &offset, result); // metaOrigLength |
| } |
| StoreU32(0, &offset, result); // privOffset |
| StoreU32(0, &offset, result); // privLength |
| // end of woff2 header |
| |
| // table directory (http://www.w3.org/TR/WOFF2/#table_dir_format) |
| for (const auto& table : tables) { |
| StoreTableEntry(table, &offset, result); |
| } |
| |
| // for collections only, collection table directory |
| if (font_collection.flavor == kTtcFontFlavor) { |
| StoreU32(font_collection.header_version, &offset, result); |
| Store255UShort(font_collection.fonts.size(), &offset, result); |
| for (const Font& font : font_collection.fonts) { |
| |
| uint16_t num_tables = 0; |
| for (const auto& entry : font.tables) { |
| const Font::Table& table = entry.second; |
| if (table.tag & 0x80808080) continue; // don't write xform tables |
| num_tables++; |
| } |
| Store255UShort(num_tables, &offset, result); |
| |
| StoreU32(font.flavor, &offset, result); |
| for (const auto& entry : font.tables) { |
| const Font::Table& table = entry.second; |
| if (table.tag & 0x80808080) continue; // don't write xform tables |
| |
| // for reused tables, only the original has an updated offset |
| uint32_t table_offset = |
| table.IsReused() ? table.reuse_of->offset : table.offset; |
| uint32_t table_length = |
| table.IsReused() ? table.reuse_of->length : table.length; |
| std::pair<uint32_t, uint32_t> tag_offset(table.tag, table_offset); |
| if (index_by_tag_offset.find(tag_offset) == index_by_tag_offset.end()) { |
| #ifdef FONT_COMPRESSION_BIN |
| fprintf(stderr, "Missing table index for offset 0x%08x\n", |
| table_offset); |
| #endif |
| return FONT_COMPRESSION_FAILURE(); |
| } |
| uint16_t index = index_by_tag_offset[tag_offset]; |
| Store255UShort(index, &offset, result); |
| |
| } |
| |
| } |
| } |
| |
| // compressed data format (http://www.w3.org/TR/WOFF2/#table_format) |
| |
| StoreBytes(&compression_buf[0], total_compressed_length, &offset, result); |
| offset = Round4(offset); |
| |
| StoreBytes(compressed_metadata_buf.data(), compressed_metadata_buf_length, |
| &offset, result); |
| |
| if (*result_length != offset) { |
| #ifdef FONT_COMPRESSION_BIN |
| fprintf(stderr, "Mismatch between computed and actual length " |
| "(%zd vs %zd)\n", *result_length, offset); |
| #endif |
| return FONT_COMPRESSION_FAILURE(); |
| } |
| return true; |
| } |
| |
| } // namespace woff2 |