| /**************************************************************************** |
| * |
| * sfwoff2.c |
| * |
| * WOFF2 format management (base). |
| * |
| * Copyright (C) 2019 by |
| * Nikhil Ramakrishnan, David Turner, Robert Wilhelm, and Werner Lemberg. |
| * |
| * This file is part of the FreeType project, and may only be used, |
| * modified, and distributed under the terms of the FreeType project |
| * license, LICENSE.TXT. By continuing to use, modify, or distribute |
| * this file you indicate that you have read the license and |
| * understand and accept it fully. |
| * |
| */ |
| |
| #include <ft2build.h> |
| #include "sfwoff2.h" |
| #include "woff2tags.h" |
| #include FT_TRUETYPE_TAGS_H |
| #include FT_INTERNAL_DEBUG_H |
| #include FT_INTERNAL_STREAM_H |
| |
| |
| #ifdef FT_CONFIG_OPTION_USE_BROTLI |
| |
| #include <brotli/decode.h> |
| |
| #endif |
| |
| |
| /************************************************************************** |
| * |
| * The macro FT_COMPONENT is used in trace mode. It is an implicit |
| * parameter of the FT_TRACE() and FT_ERROR() macros, used to print/log |
| * messages during execution. |
| */ |
| #undef FT_COMPONENT |
| #define FT_COMPONENT sfwoff2 |
| |
| |
| #define READ_255USHORT( var ) Read255UShort( stream, &var ) |
| |
| #define READ_BASE128( var ) ReadBase128( stream, &var ) |
| |
| #define ROUND4( var ) ( var + 3 ) & ~3 |
| |
| #define WRITE_USHORT( p, v ) \ |
| do \ |
| { \ |
| *(p)++ = (FT_Byte)( (v) >> 8 ); \ |
| *(p)++ = (FT_Byte)( (v) >> 0 ); \ |
| \ |
| } while ( 0 ) |
| |
| #define WRITE_ULONG( p, v ) \ |
| do \ |
| { \ |
| *(p)++ = (FT_Byte)( (v) >> 24 ); \ |
| *(p)++ = (FT_Byte)( (v) >> 16 ); \ |
| *(p)++ = (FT_Byte)( (v) >> 8 ); \ |
| *(p)++ = (FT_Byte)( (v) >> 0 ); \ |
| \ |
| } while ( 0 ) |
| |
| |
| FT_CALLBACK_DEF( int ) |
| compare_tags( const void* a, |
| const void* b ) |
| { |
| WOFF2_Table table1 = *(WOFF2_Table*)a; |
| WOFF2_Table table2 = *(WOFF2_Table*)b; |
| |
| FT_ULong tag1 = table1->Tag; |
| FT_ULong tag2 = table2->Tag; |
| |
| |
| if ( tag1 > tag2 ) |
| return 1; |
| else if ( tag1 < tag2 ) |
| return -1; |
| else |
| return 0; |
| } |
| |
| |
| static FT_Error |
| Read255UShort( FT_Stream stream, |
| FT_UShort* value ) |
| { |
| static const FT_Int oneMoreByteCode1 = 255; |
| static const FT_Int oneMoreByteCode2 = 254; |
| static const FT_Int wordCode = 253; |
| static const FT_Int lowestUCode = 253; |
| |
| FT_Error error; |
| FT_Byte code; |
| FT_Byte result_byte = 0; |
| FT_UShort result_short = 0; |
| |
| if( FT_READ_BYTE( code ) ) |
| return FT_THROW( Invalid_Table ); |
| if( code == wordCode ) |
| { |
| /* Read next two bytes and store FT_UShort value */ |
| if( FT_READ_USHORT( result_short ) ) |
| return FT_THROW( Invalid_Table ); |
| *value = result_short; |
| return FT_Err_Ok; |
| } |
| else if( code == oneMoreByteCode1 ) |
| { |
| if( FT_READ_BYTE( result_byte ) ) |
| return FT_THROW( Invalid_Table ); |
| *value = result_byte + lowestUCode; |
| return FT_Err_Ok; |
| } |
| else if( code == oneMoreByteCode2 ) |
| { |
| if( FT_READ_BYTE( result_byte ) ) |
| return FT_THROW( Invalid_Table ); |
| *value = result_byte + lowestUCode * 2; |
| return FT_Err_Ok; |
| } |
| else |
| { |
| *value = code; |
| return FT_Err_Ok; |
| } |
| } |
| |
| |
| static FT_Error |
| ReadBase128( FT_Stream stream, |
| FT_ULong* value ) |
| { |
| FT_ULong result = 0; |
| FT_Int i; |
| FT_Byte code; |
| FT_Error error; |
| |
| for ( i = 0; i < 5; ++i ) { |
| code = 0; |
| if( FT_READ_BYTE( code ) ) |
| return FT_THROW( Invalid_Table ); |
| |
| /* Leading zeros are invalid. */ |
| if ( i == 0 && code == 0x80 ) { |
| return FT_THROW( Invalid_Table ); |
| } |
| |
| /* If any of top seven bits are set then we're about to overflow. */ |
| if ( result & 0xfe000000 ){ |
| return FT_THROW( Invalid_Table ); |
| } |
| |
| result = ( result << 7 ) | ( code & 0x7f ); |
| |
| /* Spin until most significant bit of data byte is false. */ |
| if ( (code & 0x80) == 0 ) { |
| *value = result; |
| return FT_Err_Ok; |
| } |
| } |
| /* Make sure not to exceed the size bound. */ |
| return FT_THROW( Invalid_Table ); |
| } |
| |
| |
| static FT_Offset |
| CollectionHeaderSize( FT_ULong header_version, |
| FT_UShort num_fonts ) |
| { |
| FT_Offset size = 0; |
| if (header_version == 0x00020000) |
| size += 12; /* ulDsig{Tag,Length,Offset} */ |
| if (header_version == 0x00010000 || header_version == 0x00020000) { |
| size += 12 /* TTCTag, Version, numFonts */ |
| + ( 4 * num_fonts ); /* OffsetTable[numFonts] */ |
| } |
| return size; |
| } |
| |
| |
| static FT_UInt64 |
| compute_first_table_offset( const WOFF2_Header woff2 ) |
| { |
| FT_Int nn; |
| FT_Offset offset = WOFF2_SFNT_HEADER_SIZE + |
| ( WOFF2_SFNT_ENTRY_SIZE * |
| (FT_Offset)( woff2->num_tables ) ); |
| if(woff2->header_version) |
| { |
| offset = CollectionHeaderSize( woff2->header_version, |
| woff2->num_fonts ) |
| + WOFF2_SFNT_HEADER_SIZE * woff2->num_fonts; |
| for ( nn = 0; nn< woff2->num_fonts; nn++ ) { |
| offset += WOFF2_SFNT_ENTRY_SIZE * woff2->ttc_fonts[nn].num_tables; |
| } |
| } |
| return offset; |
| } |
| |
| |
| static FT_Error |
| woff2_uncompress( FT_Byte* dst, |
| FT_ULong dst_size, |
| const FT_Byte* src, |
| FT_ULong src_size ) |
| { |
| #ifdef FT_CONFIG_OPTION_USE_BROTLI |
| |
| FT_ULong uncompressed_size = dst_size; |
| BrotliDecoderResult result; |
| |
| result = BrotliDecoderDecompress( |
| src_size, src, &uncompressed_size, dst); |
| |
| if( result != BROTLI_DECODER_RESULT_SUCCESS || |
| uncompressed_size != dst_size ) |
| return FT_THROW( Invalid_Table ); |
| |
| return FT_Err_Ok; |
| |
| #else /* !FT_CONFIG_OPTION_USE_BROTLI */ |
| |
| FT_ERROR(( "woff2_uncompress: Brotli support not available.\n" )); |
| return FT_THROW( Unimplemented_Feature ); |
| |
| #endif /* !FT_CONFIG_OPTION_USE_BROTLI */ |
| } |
| |
| |
| /* Replace `face->root.stream' with a stream containing the extracted */ |
| /* SFNT of a WOFF2 font. */ |
| |
| FT_LOCAL_DEF( FT_Error ) |
| woff2_open_font( FT_Stream stream, |
| TT_Face face, |
| FT_Int face_index ) |
| { |
| FT_Memory memory = stream->memory; |
| FT_Error error = FT_Err_Ok; |
| |
| WOFF2_HeaderRec woff2; |
| WOFF2_Table tables = NULL; |
| WOFF2_Table* indices = NULL; |
| WOFF2_Table last_table; |
| |
| FT_Int nn; |
| FT_ULong j; |
| FT_ULong flags; |
| FT_UShort xform_version; |
| FT_ULong src_offset = 0; |
| |
| FT_UShort ttc_num_tables; |
| FT_UInt glyf_index; |
| FT_UInt loca_index; |
| FT_UInt64 first_table_offset; |
| FT_UInt64 file_offset; |
| |
| FT_Byte* sfnt = NULL; |
| FT_Stream sfnt_stream = NULL; |
| FT_Byte* sfnt_header; |
| |
| FT_Byte* uncompressed_buf = NULL; |
| |
| static const FT_Frame_Field woff2_header_fields[] = |
| { |
| #undef FT_STRUCTURE |
| #define FT_STRUCTURE WOFF2_HeaderRec |
| |
| FT_FRAME_START( 48 ), |
| FT_FRAME_ULONG ( signature ), |
| FT_FRAME_ULONG ( flavor ), |
| FT_FRAME_ULONG ( length ), |
| FT_FRAME_USHORT( num_tables ), |
| FT_FRAME_SKIP_BYTES( 2 + 4 ), |
| FT_FRAME_ULONG ( totalCompressedSize ), |
| FT_FRAME_SKIP_BYTES( 2 * 2 ), |
| FT_FRAME_ULONG ( metaOffset ), |
| FT_FRAME_ULONG ( metaLength ), |
| FT_FRAME_ULONG ( metaOrigLength ), |
| FT_FRAME_ULONG ( privOffset ), |
| FT_FRAME_ULONG ( privLength ), |
| FT_FRAME_END |
| }; |
| |
| |
| /* DEBUG - Remove later */ |
| FT_TRACE2(( "woff2_open_font: Received Data.\n" )); |
| |
| FT_ASSERT( stream == face->root.stream ); |
| FT_ASSERT( FT_STREAM_POS() == 0 ); |
| |
| /* DEBUG - Remove later. */ |
| FT_TRACE2(( "Face index = %ld\n", face_index )); |
| |
| /* Read WOFF2 Header. */ |
| if ( FT_STREAM_READ_FIELDS( woff2_header_fields, &woff2 ) ) |
| return error; |
| |
| /* DEBUG - Remove later. */ |
| FT_TRACE2(( "signature -> 0x%X\n", woff2.signature )); |
| FT_TRACE2(( "flavor -> 0x%X\n", woff2.flavor )); |
| FT_TRACE2(( "length -> %lu\n", woff2.length )); |
| FT_TRACE2(( "num_tables -> %hu\n", woff2.num_tables )); |
| FT_TRACE2(( "metaOffset -> %hu\n", woff2.metaOffset )); |
| FT_TRACE2(( "metaLength -> %hu\n", woff2.metaLength )); |
| FT_TRACE2(( "privOffset -> %hu\n", woff2.privOffset )); |
| FT_TRACE2(( "privLength -> %hu\n", woff2.privLength )); |
| |
| /* Make sure we don't recurse back here. */ |
| if ( woff2.flavor == TTAG_wOF2 ) |
| return FT_THROW( Invalid_Table ); |
| |
| /* Miscellaneous checks. */ |
| if ( woff2.length != stream->size || |
| woff2.num_tables == 0 || |
| 48 + woff2.num_tables * 20UL >= woff2.length || |
| ( woff2.metaOffset == 0 && ( woff2.metaLength != 0 || |
| woff2.metaOrigLength != 0 ) ) || |
| ( woff2.metaLength != 0 && woff2.metaOrigLength == 0 ) || |
| ( woff2.metaOffset >= woff2.length ) || |
| ( woff2.length - woff2.metaOffset < woff2.metaLength ) || |
| ( woff2.privOffset == 0 && woff2.privLength != 0 ) || |
| ( woff2.privOffset >= woff2.length ) || |
| ( woff2.length - woff2.privOffset < woff2.privLength ) ) |
| { |
| FT_ERROR(( "woff2_font_open: invalid WOFF2 header\n" )); |
| return FT_THROW( Invalid_Table ); |
| } |
| /* DEBUG - Remove later. */ |
| else{ |
| FT_TRACE2(( "WOFF2 Header is valid.\n" )); |
| } |
| |
| /* Read table directory. */ |
| if ( FT_NEW_ARRAY( tables, woff2.num_tables ) || |
| FT_NEW_ARRAY( indices, woff2.num_tables ) ) |
| goto Exit; |
| |
| FT_TRACE2(( "\n" |
| " tag flags transform origLen transformLen\n" |
| " --------------------------------------------------\n" )); |
| |
| for ( nn = 0; nn < woff2.num_tables; nn++ ) |
| { |
| WOFF2_Table table = tables + nn; |
| if( FT_READ_BYTE( table->FlagByte ) ) |
| goto Exit; |
| |
| if ( ( table->FlagByte & 0x3f ) == 0x3f ) |
| { |
| if( FT_READ_ULONG( table->Tag ) ) |
| goto Exit; |
| } |
| else |
| table->Tag = KnownTags[table->FlagByte & 0x3f]; |
| |
| flags = 0; |
| xform_version = ( table->FlagByte >> 6 ) & 0x03; |
| |
| /* 0 means xform for glyph/loca, non-0 for others. */ |
| if ( table->Tag == TTAG_glyf || table->Tag == TTAG_loca ) |
| { |
| if ( xform_version == 0 ) |
| flags |= WOFF2_FLAGS_TRANSFORM; |
| } |
| else if ( xform_version != 0 ) |
| flags |= WOFF2_FLAGS_TRANSFORM; |
| |
| flags |= xform_version; |
| |
| if( READ_BASE128( table->OrigLength ) ) |
| goto Exit; |
| |
| table->TransformLength = table->OrigLength; |
| |
| if ( ( flags & WOFF2_FLAGS_TRANSFORM ) != 0 ) |
| { |
| if( READ_BASE128( table->TransformLength ) ) |
| goto Exit; |
| if( table->Tag == TTAG_loca && table->TransformLength ) |
| { |
| FT_ERROR(( "woff_font_open: Invalid loca `transformLength'.\n" )); |
| return FT_THROW( Invalid_Table ); |
| } |
| } |
| |
| if ( src_offset + table->TransformLength < src_offset ) |
| { |
| FT_ERROR(( "woff_font_open: invalid WOFF2 table directory.\n" )); |
| return FT_THROW( Invalid_Table ); |
| } |
| |
| table->src_offset = src_offset; |
| table->src_length = table->TransformLength; |
| src_offset += table->TransformLength; |
| table->flags = flags; |
| |
| FT_TRACE2(( " %c%c%c%c %08d %08d %08ld %08ld\n", |
| (FT_Char)( table->Tag >> 24 ), |
| (FT_Char)( table->Tag >> 16 ), |
| (FT_Char)( table->Tag >> 8 ), |
| (FT_Char)( table->Tag ), |
| table->FlagByte & 0x3f, |
| ( table->FlagByte >> 6 ) & 0x03, |
| table->OrigLength, |
| table->TransformLength, |
| table->src_length, |
| table->src_offset )); |
| |
| indices[nn] = table; |
| } |
| |
| /* End of last table is uncompressed size. */ |
| last_table = indices[woff2.num_tables - 1]; |
| woff2.uncompressed_size = last_table->src_offset |
| + last_table->src_length; |
| if( woff2.uncompressed_size < last_table->src_offset ) |
| return FT_THROW( Invalid_Table ); |
| /* DEBUG - Remove later. */ |
| FT_TRACE2(( "Uncompressed size: %ld\n", woff2.uncompressed_size )); |
| |
| FT_TRACE2(( "Table directory successfully parsed.\n" )); |
| |
| /* Check for and read collection directory. */ |
| woff2.header_version = 0; |
| if( woff2.flavor == TTAG_ttcf ){ |
| FT_TRACE2(( "Font is a TTC, reading collection directory.\n" )); |
| if( FT_READ_ULONG( woff2.header_version ) ) |
| goto Exit; |
| /* DEBUG - Remove later */ |
| FT_TRACE2(( "Header version: %lx\n", woff2.header_version )); |
| if( woff2.header_version != 0x00010000 && |
| woff2.header_version != 0x00020000 ) |
| return FT_THROW( Invalid_Table ); |
| |
| if( READ_255USHORT( woff2.num_fonts ) ) |
| goto Exit; |
| /* DEBUG - Remove later */ |
| FT_TRACE2(( "Number of fonts in TTC: %ld\n", woff2.num_fonts )); |
| |
| if( FT_NEW_ARRAY( woff2.ttc_fonts, woff2.num_fonts ) ) |
| goto Exit; |
| |
| for ( nn = 0; nn < woff2.num_fonts; nn++ ) |
| { |
| WOFF2_TtcFont ttc_font = woff2.ttc_fonts + nn; |
| |
| if( READ_255USHORT( ttc_num_tables ) ) |
| goto Exit; |
| if( FT_READ_ULONG( ttc_font->flavor ) ) |
| goto Exit; |
| |
| if( FT_NEW_ARRAY( ttc_font->table_indices, ttc_num_tables ) ) |
| goto Exit; |
| /* DEBUG - Change to TRACE4 */ |
| FT_TRACE2(( "Number of tables in font %d: %ld\n", |
| nn, ttc_num_tables )); |
| /* DEBUG - Change to TRACE5 */ |
| FT_TRACE2(( " Indices: " )); |
| |
| glyf_index = 0; |
| loca_index = 0; |
| |
| for ( j = 0; j < ttc_num_tables; j++ ) |
| { |
| FT_UShort table_index; |
| WOFF2_Table table; |
| |
| if( READ_255USHORT( table_index ) ) |
| goto Exit; |
| /* DEBUG - Change to TRACE5 */ |
| FT_TRACE2(("%hu ", table_index)); |
| ttc_font->table_indices[j] = table_index; |
| |
| table = indices[table_index]; |
| if( table->Tag == TTAG_loca ) |
| loca_index = table_index; |
| if( table->Tag == TTAG_glyf ) |
| glyf_index = table_index; |
| } |
| /* DEBUG - Change to TRACE5 */ |
| FT_TRACE2(( "\n" )); |
| |
| /* glyf and loca must be consecutive */ |
| if( glyf_index > 0 || loca_index > 0 ) |
| { |
| if(glyf_index > loca_index || loca_index - glyf_index != 1) |
| return FT_THROW( Invalid_Table ); |
| /* DEBUG - Remove later */ |
| else |
| FT_TRACE2(( "glyf and loca indices are valid.\n" )); |
| } |
| } |
| /* Collection directory reading complete. */ |
| FT_TRACE2(( "WOFF2 collection dirtectory is valid.\n" )); |
| |
| } |
| |
| first_table_offset = compute_first_table_offset( &woff2 ); |
| FT_TRACE2(( "Offset to first table: %ld\n", first_table_offset )); |
| |
| woff2.compressed_offset = FT_STREAM_POS(); |
| file_offset = ROUND4( woff2.compressed_offset + |
| woff2.totalCompressedSize ); |
| |
| /* Few more checks before we start reading the tables. */ |
| if( file_offset > woff2.length ) |
| return FT_THROW( Invalid_Table ); |
| |
| if ( woff2.metaOffset ) |
| { |
| if ( file_offset != woff2.metaOffset ) |
| return FT_THROW( Invalid_Table ); |
| file_offset = ROUND4(woff2.metaOffset + woff2.metaLength); |
| } |
| |
| if( woff2.privOffset ) |
| { |
| if( file_offset != woff2.privOffset ) |
| return FT_THROW( Invalid_Table ); |
| file_offset = ROUND4(woff2.privOffset + woff2.privLength); |
| } |
| |
| if( file_offset != ( ROUND4( woff2.length ) ) ) |
| return FT_THROW( Invalid_Table ); |
| |
| /* TODO Add support for uncompression of TTC fonts. */ |
| /* Redirect a TTC to exit for now. */ |
| if( woff2.header_version ) |
| { |
| FT_TRACE2(( "Reading TTC fonts not supported yet.\n" )); |
| error = FT_THROW( Unimplemented_Feature ); |
| goto Exit; |
| } |
| |
| /* Write sfnt header. */ |
| if ( FT_ALLOC( sfnt, 12 + woff2.num_tables * 16UL ) || |
| FT_NEW( sfnt_stream ) ) |
| goto Exit; |
| |
| sfnt_header = sfnt; |
| |
| { |
| FT_UInt searchRange, entrySelector, rangeShift, x; |
| /* DEBUG - Remove later */ |
| FT_TRACE2(( "Writing SFNT offset table.\n" )); |
| |
| x = woff2.num_tables; |
| entrySelector = 0; |
| while ( x ) |
| { |
| x >>= 1; |
| entrySelector += 1; |
| } |
| entrySelector--; |
| |
| searchRange = ( 1 << entrySelector ) * 16; |
| rangeShift = ( woff2.num_tables * 16 ) - searchRange; |
| |
| WRITE_ULONG ( sfnt_header, woff2.flavor ); |
| WRITE_USHORT( sfnt_header, woff2.num_tables ); |
| WRITE_USHORT( sfnt_header, searchRange ); |
| WRITE_USHORT( sfnt_header, entrySelector ); |
| WRITE_USHORT( sfnt_header, rangeShift ); |
| |
| } |
| |
| /* Sort tables by tag. */ |
| ft_qsort( indices, |
| woff2.num_tables, |
| sizeof ( WOFF2_Table ), |
| compare_tags ); |
| |
| /* DEBUG - Remove later */ |
| FT_TRACE2(( "Sorted table indices: \n" )); |
| for( nn = 0; nn < woff2.num_tables; ++nn ) |
| { |
| WOFF2_Table table = indices[nn]; |
| /* DEBUG - Remove later */ |
| FT_TRACE2(( " Index %d", nn )); |
| FT_TRACE2(( " %c%c%c%c\n", |
| (FT_Char)( table->Tag >> 24 ), |
| (FT_Char)( table->Tag >> 16 ), |
| (FT_Char)( table->Tag >> 8 ), |
| (FT_Char)( table->Tag ))); |
| } |
| |
| if( woff2.uncompressed_size < 1 ) |
| { |
| error = FT_THROW( Invalid_Table ); |
| goto Exit; |
| } |
| |
| /* Allocate memory for uncompressed table data. */ |
| if ( FT_ALLOC( uncompressed_buf, woff2.uncompressed_size ) || |
| FT_FRAME_ENTER( woff2.totalCompressedSize ) ) |
| goto Exit; |
| |
| /* Uncompress the stream. */ |
| error = woff2_uncompress( uncompressed_buf, woff2.uncompressed_size, |
| stream->cursor, woff2.totalCompressedSize ); |
| if( error ) |
| goto Exit; |
| |
| FT_FRAME_EXIT(); |
| |
| /* TODO Write table entries. */ |
| |
| error = FT_THROW( Unimplemented_Feature ); |
| /* DEBUG - Remove later */ |
| FT_TRACE2(( "Reached end without errors.\n" )); |
| goto Exit; |
| |
| Exit: |
| FT_FREE( tables ); |
| FT_FREE( indices ); |
| |
| if( error ) |
| { |
| FT_FREE( sfnt ); |
| FT_Stream_Close( sfnt_stream ); |
| FT_FREE( sfnt_stream ); |
| } |
| |
| return error; |
| } |
| |
| |
| #undef READ_255USHORT |
| #undef READ_BASE128 |
| #undef ROUND4 |
| #undef WRITE_USHORT |
| #undef WRITE_ULONG |
| |
| |
| /* END */ |