/**************************************************************************** * * ttvarc.c * * TrueType and OpenType VARC (Variable Composites) support (body). * * Copyright (C) 2025-2026 by * Behdad Esfahbod, 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. * */ /************************************************************************** * * VARC table specification: * * https://github.com/harfbuzz/boring-expansion-spec/blob/main/VARC.md * */ #include #include #include #include #include #include #include #include #include #ifdef TT_CONFIG_OPTION_VARC #include "ttvarc.h" #ifdef TT_CONFIG_OPTION_GX_VAR_SUPPORT #include "ttgxvar.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 ttvarc FT_LOCAL_DEF( FT_Error ) tt_face_init_varc_axes( FT_Face face, FT_UInt axis_count ) { #ifdef TT_CONFIG_OPTION_GX_VAR_SUPPORT return tt_var_init_blend( face, axis_count ); #else FT_UNUSED( face ); FT_UNUSED( axis_count ); return FT_Err_Ok; #endif } /* Macro for bounds checking when reading from table */ #define CHECK_TABLE_BOUNDS( p, size ) \ ( (FT_Byte*)(p) >= \ (FT_Byte*)varc->table && \ (FT_Byte*)(p) + (size) <= \ (FT_Byte*)varc->table + varc->table_size ) /* Stack allocation limits for performance. */ #define VARC_STACK_AXIS_COUNT 64 #define VARC_STACK_DELTA_COUNT 128 #define VARC_STACK_COORD_COUNT 64 #define VARC_STACK_INDICES_COUNT 64 /************************************************************************** * * Utility Functions * */ /************************************************************************** * * @Function: * read_uint32var * * @Description: * Read a variable-length uint32 value (1-5 bytes). * Similar to UTF-8 encoding but for integers. * * @Input: * p :: * Pointer to the byte stream (will be advanced). * * limit :: * Maximum valid address in the stream. * * @Output: * value :: * The decoded uint32 value. * * @Return: * FreeType error code. 0 means success. */ static FT_Error read_uint32var( FT_Byte** p, FT_Byte* limit, FT_UInt32* value ) { FT_Byte b0; FT_UInt32 result; if ( *p >= limit ) return FT_THROW( Invalid_Table ); b0 = *(*p)++; /* single byte: 0xxxxxxx */ if ( b0 < 0x80 ) { *value = b0; return FT_Err_Ok; } /* two bytes: 10xxxxxx xxxxxxxx */ if ( b0 < 0xC0 ) { if ( *p >= limit ) return FT_THROW( Invalid_Table ); result = ( ( b0 - 0x80 ) << 8 ) | *(*p)++; *value = result; return FT_Err_Ok; } /* three bytes: 110xxxxx xxxxxxxx xxxxxxxx */ if ( b0 < 0xE0 ) { if ( *p + 1 >= limit ) return FT_THROW( Invalid_Table ); result = ( ( b0 - 0xC0 ) << 16 ) | ( (*p)[0] << 8 ) | (*p)[1]; *p += 2; *value = result; return FT_Err_Ok; } /* four bytes: 1110xxxx xxxxxxxx xxxxxxxx xxxxxxxx */ if ( b0 < 0xF0 ) { if ( *p + 2 >= limit ) return FT_THROW( Invalid_Table ); result = ( ( b0 - 0xE0 ) << 24 ) | ( (*p)[0] << 16 ) | ( (*p)[1] << 8 ) | (*p)[2]; *p += 3; *value = result; return FT_Err_Ok; } /* five bytes: 11110000 xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx */ if ( b0 == 0xF0 ) { if ( *p + 3 >= limit ) return FT_THROW( Invalid_Table ); result = ( (*p)[0] << 24 ) | ( (*p)[1] << 16 ) | ( (*p)[2] << 8 ) | (*p)[3]; *p += 4; *value = result; return FT_Err_Ok; } /* invalid encoding */ return FT_THROW( Invalid_Table ); } /************************************************************************** * * @Function: * read_fword * * @Description: * Read a signed 16-bit FWORD value. * * @Input: * p :: * Pointer to the byte stream (will be advanced). * * limit :: * Maximum valid address in the stream. * * @Output: * value :: * The decoded FWORD value as 26.6 fixed-point. * * @Return: * FreeType error code. 0 means success. */ static FT_Error read_fword( FT_Byte** p, FT_Byte* limit, FT_Pos* value ) { FT_Short val; if ( *p + 1 >= limit ) return FT_THROW( Invalid_Table ); val = (FT_Short)( ( (FT_UShort)(*p)[0] << 8 ) | (FT_UShort)(*p)[1] ); *p += 2; /* FWORD is a signed 16-bit integer, */ /* shift left by 6 for FT_Pos (26.6 format). */ *value = (FT_Pos)val * 64; return FT_Err_Ok; } /************************************************************************** * * @Function: * read_f4dot12 * * @Description: * Read a signed F4DOT12 value (4.12 fixed-point). * * @Input: * p :: * Pointer to the byte stream (will be advanced). * * limit :: * Maximum valid address in the stream. * * @Output: * value :: * The decoded value as 16.16 fixed-point. * * @Return: * FreeType error code. 0 means success. */ static FT_Error read_f4dot12( FT_Byte** p, FT_Byte* limit, FT_Fixed* value ) { FT_Short val; if ( *p + 1 >= limit ) return FT_THROW( Invalid_Table ); val = (FT_Short)( ( (FT_UShort)(*p)[0] << 8 ) | (FT_UShort)(*p)[1] ); *p += 2; /* Convert F4DOT12 (4.12) to F16DOT16 (16.16) by shifting left 4. */ *value = (FT_Fixed)val * 16; return FT_Err_Ok; } /************************************************************************** * * @Function: * read_f6dot10 * * @Description: * Read a signed F6DOT10 value (6.10 fixed-point). * * @Input: * p :: * Pointer to the byte stream (will be advanced). * * limit :: * Maximum valid address in the stream. * * @Output: * value :: * The decoded value as 16.16 fixed-point. * * @Return: * FreeType error code. 0 means success. */ static FT_Error read_f6dot10( FT_Byte** p, FT_Byte* limit, FT_Fixed* value ) { FT_Short val; if ( *p + 1 >= limit ) return FT_THROW( Invalid_Table ); val = (FT_Short)( ( (FT_UShort)(*p)[0] << 8 ) | (FT_UShort)(*p)[1] ); *p += 2; /* Simply shift left by 6. */ *value = (FT_Fixed)val * 64; return FT_Err_Ok; } /************************************************************************** * * Coverage Table Functions * */ /************************************************************************** * * @Function: * tt_varc_get_coverage * * @Description: * Check whether a glyph is covered by the 'VARC' table using binary * search. * * @Input: * varc :: * The 'VARC' table structure. * * glyph_index :: * The glyph ID to check. * * @Return: * Coverage index if found, -1 otherwise. */ static FT_Int tt_varc_get_coverage( TT_Varc varc, FT_UInt glyph_index ) { FT_Byte* p; FT_UInt format; FT_UInt count; if ( !varc || !varc->coverage ) return -1; p = varc->coverage; /* Check bounds for format and count. */ if ( !CHECK_TABLE_BOUNDS( p, 4 ) ) return -1; format = ( p[0] << 8 ) | p[1]; p += 2; /* formats 1 and 3: list of 16-bit or 24-bit glyph IDs */ if ( format == 1 || format == 3 ) { FT_UInt glyph_size = format == 1 ? 2 : 3; if ( !CHECK_TABLE_BOUNDS( p, glyph_size ) ) return -1; count = format == 1 ? FT_NEXT_USHORT( p ) : FT_NEXT_UOFF3( p ); if ( !CHECK_TABLE_BOUNDS( p, count * glyph_size ) ) return -1; /* binary search */ { FT_UInt min = 0; FT_UInt max = count; while ( min < max ) { FT_UInt mid = ( min + max ) >> 1; FT_Byte* gid_p = p + mid * glyph_size; FT_UInt gid = format == 1 ? FT_PEEK_USHORT( gid_p ) : FT_PEEK_UOFF3( gid_p ); if ( gid == glyph_index ) return (FT_Int)mid; else if ( gid < glyph_index ) min = mid + 1; else max = mid; } } } /* formats 2 and 4: range list with 16-bit or 24-bit values */ else if ( format == 2 || format == 4 ) { FT_UInt value_size = format == 2 ? 2 : 3; FT_UInt record_size = value_size * 3; FT_UInt min, max; if ( !CHECK_TABLE_BOUNDS( p, value_size ) ) return -1; count = format == 2 ? FT_NEXT_USHORT( p ) : FT_NEXT_UOFF3( p ); if ( !CHECK_TABLE_BOUNDS( p, count * record_size ) ) return -1; /* binary search the sorted ranges */ min = 0; max = count; while ( min < max ) { FT_UInt mid = ( min + max ) >> 1; FT_Byte* rec = p + mid * record_size; FT_UInt start = format == 2 ? FT_PEEK_USHORT( rec ) : FT_PEEK_UOFF3( rec ); FT_UInt end = format == 2 ? FT_PEEK_USHORT( rec + value_size ) : FT_PEEK_UOFF3( rec + value_size ); if ( glyph_index < start ) max = mid; else if ( glyph_index > end ) min = mid + 1; else { FT_Byte* index_p = rec + value_size * 2; FT_UInt index = format == 2 ? FT_PEEK_USHORT( index_p ) : FT_PEEK_UOFF3( index_p ); return (FT_Int)( index + glyph_index - start ); } } } return -1; } /************************************************************************** * * CFF2IndexOf Functions * */ /************************************************************************** * * @Function: * tt_varc_read_cff2_index_entry * * @Description: * Read entry `index` from a CFF2-style INDEX: * * count (uint32) | offSize (uint8) | offset[count + 1] | data * * Each offset is `offSize` bytes and 1-based. On success `*data` * points to the entry's bytes and `*size` is its length. Every read * and the returned range are validated against `table_limit`; the * caller must pass an `index_data` pointer that lies within the table. * * @Return: * FreeType error code. 0 means success. */ static FT_Error tt_varc_read_cff2_index_entry( FT_Byte* index_data, FT_Byte* table_limit, FT_UInt index, FT_Byte** data, FT_UInt* size ) { FT_Byte* p = index_data; FT_UInt32 count; FT_UInt offSize; FT_ULong offset1 = 0; FT_ULong offset2 = 0; FT_Byte* offset_base; FT_UInt i; if ( !p || p + 4 > table_limit ) return FT_THROW( Invalid_Table ); count = ( (FT_UInt32)p[0] << 24 ) | ( (FT_UInt32)p[1] << 16 ) | ( (FT_UInt32)p[2] << 8 ) | (FT_UInt32)p[3]; p += 4; if ( p >= table_limit ) return FT_THROW( Invalid_Table ); offSize = *p++; if ( offSize < 1 || offSize > 4 || index >= count ) return FT_THROW( Invalid_Table ); /* division avoids overflow in the offset-array bounds check */ if ( count >= (FT_UInt32)( ( table_limit - p ) / offSize ) ) return FT_THROW( Invalid_Table ); offset_base = p + ( count + 1 ) * offSize; p += index * offSize; for ( i = 0; i < offSize; i++ ) offset1 = ( offset1 << 8 ) | p[i]; p += offSize; for ( i = 0; i < offSize; i++ ) offset2 = ( offset2 << 8 ) | p[i]; /* offsets are 1-based and must stay within the table */ if ( offset2 < offset1 || offset1 < 1 || offset2 - 1 > (FT_ULong)( table_limit - offset_base ) ) return FT_THROW( Invalid_Table ); *data = offset_base + offset1 - 1; *size = (FT_UInt)( offset2 - offset1 ); return FT_Err_Ok; } /************************************************************************** * * @Function: * tt_varc_get_glyph_record * * @Description: * Get the glyph record from the `CFF2IndexOf` structure. * * @Input: * varc :: * The 'VARC' table structure. * * glyph_index :: * The glyph ID. * * @Output: * record_data :: * Pointer to the glyph record data. * * record_size :: * Size of the glyph record in bytes. * * @Return: * FreeType error code. 0 means success. */ static FT_Error tt_varc_get_glyph_record( TT_Varc varc, FT_UInt glyph_index, FT_Byte** record_data, FT_UInt* record_size ) { FT_Error error; FT_Int coverage_index; if ( !varc || !varc->var_composite_glyphs ) return FT_THROW( Invalid_Table ); coverage_index = tt_varc_get_coverage( varc, glyph_index ); if ( coverage_index < 0 ) return FT_THROW( Invalid_Glyph_Index ); error = tt_varc_read_cff2_index_entry( varc->var_composite_glyphs, (FT_Byte*)varc->table + varc->table_size, (FT_UInt)coverage_index, record_data, record_size ); if ( error ) return error; /* a component record is at most 64KB */ if ( *record_size > 0xFFFFU ) return FT_THROW( Invalid_Table ); return FT_Err_Ok; } /************************************************************************** * * Component Parsing Functions * */ /************************************************************************** * * @Function: * tt_varc_parse_component * * @Description: * Parse a single VARC component record. * * @Input: * face :: * The font face. * * varc :: * The 'VARC' table structure. * * p :: * Pointer to component data (will be advanced). * * limit :: * End of component data. * * axis_values_buffer :: * A stack buffer to be used for small arrays. * * axis_values_buffer_size :: * The size of `axis_values_buffer`. If the number of parsed axis * values exceeds this size, an array gets allocated on the heap. * * @Output: * component :: * The parsed component structure. * * @Return: * FreeType error code. 0 means success. */ static FT_Error tt_varc_parse_component( TT_Face face, TT_Varc varc, FT_Byte** p, FT_Byte* limit, TT_VarcComponent component, FT_Fixed* axis_values_buffer, FT_UInt axis_values_buffer_size ) { FT_Error error; FT_Memory memory = face->root.memory; FT_UInt32 flags32; FT_ZERO( component ); /* read flags (uint32var) */ error = read_uint32var( p, limit, &flags32 ); if ( error ) return error; component->flags = flags32; /* read GID */ if ( component->flags & VARC_GID_IS_24BIT ) { if ( *p + 2 >= limit ) return FT_THROW( Invalid_Table ); component->gid = ( (*p)[0] << 16 ) | ( (*p)[1] << 8 ) | (*p)[2]; *p += 3; } else { if ( *p + 1 >= limit ) return FT_THROW( Invalid_Table ); component->gid = ( (*p)[0] << 8 ) | (*p)[1]; *p += 2; } /* validate GID */ if ( component->gid >= (FT_UInt)face->root.num_glyphs ) { /* For now, don't fail - the font might be using extended GID space. */ /* return FT_THROW( Invalid_Glyph_Index ); */ } /* read condition index */ if ( component->flags & VARC_HAVE_CONDITION ) { error = read_uint32var( p, limit, &component->condition_index ); if ( error ) return error; } /* read axis indices index */ if ( component->flags & VARC_HAVE_AXES ) { error = read_uint32var( p, limit, &component->axis_indices_index ); if ( error ) return error; } /* read axis values (TupleValues) */ if ( component->flags & VARC_HAVE_AXES ) { FT_UInt axis_count = 0; FT_Byte* table_limit = (FT_Byte*)varc->table + varc->table_size; FT_Byte* tuple_data; FT_UInt tuple_size; if ( !varc->axis_indices_list ) return FT_THROW( Invalid_Table ); error = tt_varc_read_cff2_index_entry( varc->axis_indices_list, table_limit, component->axis_indices_index, &tuple_data, &tuple_size ); if ( error ) return error; /* decode the TupleValues just to count the axis values */ { FT_Byte* tuple_limit = tuple_data + tuple_size; FT_Byte* tp = tuple_data; while ( tp < tuple_limit ) { FT_Byte control = *tp++; FT_UInt cnt = ( control & 0x3F ) + 1; if ( ( control & 0xC0 ) == 0xC0 ) { /* 32-bit values */ if ( tp + 4 * cnt > tuple_limit ) break; tp += 4 * cnt; } else if ( control & 0x80 ) ; /* zeros - no data bytes */ else if ( control & 0x40 ) { /* 2-byte values */ if ( tp + 2 * cnt > tuple_limit ) break; tp += 2 * cnt; } else { /* 1-byte values */ if ( tp + cnt > tuple_limit ) break; tp += cnt; } axis_count += cnt; } } component->num_axis_values = axis_count; if ( axis_count > 0 ) { FT_UInt i; FT_UInt j; /* use caller's stack buffer for small arrays, heap for large */ if ( axis_count <= axis_values_buffer_size ) { component->axis_values = axis_values_buffer; component->axis_values_on_heap = FALSE; } else { if ( FT_NEW_ARRAY( component->axis_values, axis_count ) ) return error; component->axis_values_on_heap = TRUE; } /* read axis values from TupleValues (packed deltas format) */ i = 0; while ( i < axis_count ) { FT_Byte control; FT_UInt cnt; /* read control byte */ if ( *p >= limit ) goto Fail; control = *(*p)++; /* extract run count (lower 6 bits), add 1 */ cnt = ( control & 0x3F ) + 1; if ( cnt > axis_count - i ) cnt = axis_count - i; /* check top 2 bits for encoding type */ if ( ( control & 0xC0 ) == 0xC0 ) { /* Both bits set = 32-bit signed integers */ if ( *p + 4 * cnt > limit ) goto Fail; for ( j = 0; j < cnt; j++ ) { /* read as signed 32-bit integer, */ /* reinterpret as F2DOT14 -> F16DOT16 */ FT_Int32 val = (FT_Int32)( ( (FT_UInt32)(*p)[0] << 24 ) | ( (FT_UInt32)(*p)[1] << 16 ) | ( (FT_UInt32)(*p)[2] << 8 ) | (FT_UInt32)(*p)[3] ); component->axis_values[i++] = (FT_Fixed)MUL_INT32( val, 4 ); *p += 4; } } else if ( control & 0x80 ) { /* bit 7 set = zeros */ for ( j = 0; j < cnt; j++ ) component->axis_values[i++] = 0; } else if ( control & 0x40 ) { /* bit 6 set = 2-byte signed integers */ if ( *p + 2 * cnt > limit ) goto Fail; for ( j = 0; j < cnt; j++ ) { /* read as signed 16-bit integer, */ /* reinterpret as F2DOT14 -> F16DOT16 */ FT_Short val = (FT_Short)( ( (FT_UShort)(*p)[0] << 8 ) | (FT_UShort)(*p)[1] ); component->axis_values[i++] = (FT_Fixed)( val * 4 ); *p += 2; } } else { /* neither bit set = 1-byte signed integers */ if ( *p + cnt > limit ) goto Fail; for ( j = 0; j < cnt; j++ ) { /* read signed byte integer, */ /* reinterpret as F2DOT14 -> F16DOT16 */ FT_Char val = (FT_Char)(*(*p)++); component->axis_values[i++] = (FT_Fixed)( val * 4 ); } } } } } /* read axis values variation index */ if ( component->flags & VARC_AXES_HAVE_VARIATION ) { error = read_uint32var( p, limit, &component->axis_values_var_index ); if ( error ) goto Fail; } /* read transform variation index */ if ( component->flags & VARC_TRANSFORM_HAS_VARIATION ) { error = read_uint32var( p, limit, &component->transform_var_index ); if ( error ) goto Fail; } /* initialize transform to identity */ component->translate_x = 0; component->translate_y = 0; component->rotation = 0; component->scale_x = 0x10000; /* 1.0 in 16.16 */ component->scale_y = 0x10000; /* 1.0 in 16.16 */ component->skew_x = 0; component->skew_y = 0; component->tcenter_x = 0; component->tcenter_y = 0; /* read transform components - each field is optional based on flags */ if ( component->flags & VARC_HAVE_TRANSLATE_X ) { error = read_fword( p, limit, &component->translate_x ); if ( error ) goto Fail; } if ( component->flags & VARC_HAVE_TRANSLATE_Y ) { error = read_fword( p, limit, &component->translate_y ); if ( error ) goto Fail; } if ( component->flags & VARC_HAVE_ROTATION ) { error = read_f4dot12( p, limit, &component->rotation ); if ( error ) goto Fail; } if ( component->flags & VARC_HAVE_SCALE_X ) { error = read_f6dot10( p, limit, &component->scale_x ); if ( error ) goto Fail; } if ( component->flags & VARC_HAVE_SCALE_Y ) { error = read_f6dot10( p, limit, &component->scale_y ); if ( error ) goto Fail; } else if ( component->flags & VARC_HAVE_SCALE_X ) { /* ScaleY defaults to ScaleX if ScaleX is present */ component->scale_y = component->scale_x; } if ( component->flags & VARC_HAVE_SKEW_X ) { error = read_f4dot12( p, limit, &component->skew_x ); if ( error ) goto Fail; } if ( component->flags & VARC_HAVE_SKEW_Y ) { error = read_f4dot12( p, limit, &component->skew_y ); if ( error ) goto Fail; } if ( component->flags & VARC_HAVE_TCENTER_X ) { error = read_fword( p, limit, &component->tcenter_x ); if ( error ) goto Fail; } if ( component->flags & VARC_HAVE_TCENTER_Y ) { error = read_fword( p, limit, &component->tcenter_y ); if ( error ) goto Fail; } /* Skip one uint32var for each set reserved flag bit (bits 15-31), */ /* keeping the byte stream aligned for forward-compatible */ /* extensions, as HarfBuzz, skrifa, and fontTools do. */ { FT_UInt32 reserved = component->flags & VARC_RESERVED_MASK; while ( reserved ) { FT_UInt32 ignore; error = read_uint32var( p, limit, &ignore ); if ( error ) goto Fail; reserved &= reserved - 1; } } return FT_Err_Ok; Fail: /* the axis-value reading loop jumps here on truncated data */ /* without setting `error`; never report success on failure */ if ( !error ) error = FT_THROW( Invalid_Table ); if ( component->axis_values && component->axis_values_on_heap ) FT_FREE( component->axis_values ); return error; } /************************************************************************** * * @Function: * tt_varc_free_component * * @Description: * Free resources allocated for a component. * * @Input: * face :: * The font face. * * component :: * The component to free. */ static void tt_varc_free_component( TT_Face face, TT_VarcComponent component ) { FT_Memory memory = face->root.memory; if ( component->axis_values && component->axis_values_on_heap ) FT_FREE( component->axis_values ); } /************************************************************************** * * MultiItemVariationStore Functions * */ /************************************************************************** * * @Function: * tt_varc_evaluate_region * * @Description: * Evaluate a variation region against current coordinates. * Return a scalar value (0.0 to 1.0) indicating how much this * region contributes at the current variation location. * * @Input: * region_data :: * Pointer to region data (axis ranges). * * limit :: * End of valid data. * * coords :: * Current normalized coordinates (F2DOT14). * * coord_count :: * Number of coordinates. * * @Return: * Scalar value as FT_Fixed (16.16 format), 0 to 0x10000 (1.0). */ static FT_Fixed tt_varc_evaluate_region( FT_Byte* region_data, FT_Byte* limit, FT_Fixed* coords, FT_UInt coord_count ) { FT_Byte* p = region_data; FT_Fixed scalar = 0x10000; /* start with 1.0 in 16.16 */ FT_UInt axis_count; FT_UInt i; if ( !coords || coord_count == 0 ) return scalar; /* no coords available, return 1.0 */ /* `SparseVariationRegion` table: * * regionAxisCount (u16) - number of non-default axes * axisOffsets (u32[regionAxisCount]) - relative to this region * for each sparse axis: * axisIndex (u16) * startCoord (F2DOT14) * peakCoord (F2DOT14) * endCoord (F2DOT14) */ if ( limit - p < 2 ) return 0; /* read sparse axis count (u16) */ axis_count = ( (FT_UInt)p[0] << 8 ) | p[1]; p += 2; if ( axis_count > (FT_ULong)( limit - p ) / 4 ) return 0; /* process each axis range */ for ( i = 0; i < axis_count; i++ ) { FT_UInt16 axis_index; FT_Short min_val, peak_val, max_val; FT_Fixed coord; FT_Fixed axis_scalar; FT_UInt32 axis_offset; FT_Byte* axis; FT_Fixed min_16; FT_Fixed peak_16; FT_Fixed max_16; axis_offset = ( (FT_UInt32)p[0] << 24 ) | ( (FT_UInt32)p[1] << 16 ) | ( (FT_UInt32)p[2] << 8 ) | (FT_UInt32)p[3]; p += 4; if ( axis_offset > (FT_ULong)( limit - region_data ) || (FT_ULong)( limit - region_data ) - axis_offset < 8 ) return 0; axis = region_data + axis_offset; axis_index = ( (FT_UInt16)axis[0] << 8 ) | axis[1]; axis += 2; /* read min, peak, max (F2DOT14) - each is 2 bytes, */ /* advance after each read */ min_val = (FT_Short)( ( (FT_UShort)axis[0] << 8 ) | (FT_UShort)axis[1] ); axis += 2; peak_val = (FT_Short)( ( (FT_UShort)axis[0] << 8 ) | (FT_UShort)axis[1] ); axis += 2; max_val = (FT_Short)( ( (FT_UShort)axis[0] << 8 ) | (FT_UShort)axis[1] ); /* get coordinate for this axis */ if ( axis_index >= coord_count ) continue; /* skip unknown axes */ coord = coords[axis_index]; /* convert F2DOT14 to F16DOT16 for comparison */ min_16 = (FT_Fixed)min_val * 4; peak_16 = (FT_Fixed)peak_val * 4; max_16 = (FT_Fixed)max_val * 4; /* Evaluate this axis with the sparse-region tent function, */ /* matching HarfBuzz (`VarRegionAxis::evaluate`) and skrifa */ /* (`SparseVariationRegion::compute_scalar`). */ if ( peak_16 == 0 ) continue; /* axis not part of region -> factor 1 */ if ( coord == peak_16 ) continue; /* at peak -> factor 1 */ if ( coord == 0 ) return 0; /* default location -> region gives 0 */ if ( min_16 > peak_16 || peak_16 > max_16 || ( min_16 < 0 && max_16 > 0 ) ) continue; /* ill-formed axis -> factor 1 */ if ( coord < min_16 || coord > max_16 ) return 0; /* outside range -> region gives 0 */ if ( coord < peak_16 ) axis_scalar = FT_DivFix( coord - min_16, peak_16 - min_16 ); else axis_scalar = FT_DivFix( max_16 - coord, max_16 - peak_16 ); /* multiply into overall scalar */ scalar = FT_MulFix( scalar, axis_scalar ); /* early exit if scalar becomes 0 */ if ( scalar == 0 ) return 0; } return scalar; } /************************************************************************** * * MultiItemVariationStore Functions * */ /************************************************************************** * * @Function: * tt_varc_read_tuple_deltas * * @Description: * Read a tuple of delta values from TupleValues (packed deltas). * Return plain integers that will be scaled later. * * @Input: * data :: * Pointer to TupleValues data. * * limit :: * End of valid data. * * num_deltas :: * Number of deltas to read. * * @Output: * deltas :: * Array to store deltas (as plain integers). * * bytes_consumed :: * The number of bytes consumed for this tuple. * * @Return: * FreeType error code. 0 means success. */ static FT_Error tt_varc_read_tuple_deltas( FT_Byte* data, FT_Byte* limit, FT_UInt num_deltas, FT_Int32* deltas, FT_UInt* bytes_consumed ) { FT_Byte* p = data; FT_Byte* start = data; FT_UInt i = 0; /* read deltas using TupleValues (packed delta) format */ while ( i < num_deltas && p < limit ) { FT_Byte control; FT_UInt cnt, j; /* read control byte */ if ( p >= limit ) return FT_THROW( Invalid_Table ); control = *p++; /* extract run count (lower 6 bits), add 1 */ cnt = ( control & 0x3F ) + 1; if ( cnt > num_deltas - i ) cnt = num_deltas - i; /* check top 2 bits for encoding type */ if ( ( control & 0xC0 ) == 0xC0 ) { /* both bits set = 32-bit plain integer values */ if ( p + 4 * cnt > limit ) return FT_THROW( Invalid_Table ); for ( j = 0; j < cnt; j++ ) { /* read as signed 32-bit plain integer */ FT_Int32 val = (FT_Int32)( ( (FT_UInt32)p[0] << 24 ) | ( (FT_UInt32)p[1] << 16 ) | ( (FT_UInt32)p[2] << 8 ) | (FT_UInt32)p[3] ); deltas[i++] = val; /* store as plain integer */ p += 4; } } else if ( control & 0x80 ) { /* bit 7 set = zeros */ for ( j = 0; j < cnt; j++ ) deltas[i++] = 0; } else if ( control & 0x40 ) { /* bit 6 set = 2-byte signed integer values */ if ( p + 2 * cnt > limit ) return FT_THROW( Invalid_Table ); for ( j = 0; j < cnt; j++ ) { /* read as signed 16-bit plain integer */ FT_Short val = (FT_Short)( ( (FT_UShort)p[0] << 8 ) | (FT_UShort)p[1] ); deltas[i++] = val; /* store as plain integer */ p += 2; } } else { /* neither bit set = 1-byte signed integer values */ if ( p + cnt > limit ) return FT_THROW( Invalid_Table ); for ( j = 0; j < cnt; j++ ) { /* read signed byte as plain integer */ FT_Char val = (FT_Char)(*p++); deltas[i++] = val; /* store as plain integer */ } } } /* if we hit the end before reading all deltas, */ /* fill remaining with zeros */ while ( i < num_deltas ) deltas[i++] = 0; /* return the number of bytes consumed */ *bytes_consumed = (FT_UInt)( p - start ); return FT_Err_Ok; } /************************************************************************** * * @Function: * tt_varc_get_item_deltas * * @Description: * Get a tuple of deltas from the `MultiItemVariationStore`. * Unlike `ItemVariationStore`, which returns a single delta, * `MultiItemVariationStore` returns multiple deltas (a tuple). * * @Input: * face :: * The font face. * * varc :: * The VARC table. * * var_index :: * Variation index (outer << 16 | inner). * * num_deltas :: * Number of deltas expected in the tuple. * * shift :: * Number of bits to preserve from fractional interpolation. * The raw integer deltas are effectively shifted left by this * amount. Use shift=2 for F2DOT14 data (returns F16DOT16), * shift=4 for F4DOT12 data (returns F16DOT16), etc. * Maximum value is 16. * * current_coords :: * Current normalized coordinates (F2DOT14). * * num_coords :: * Number of coords in `current_coords`. * * condition_defaults :: * Optional defaults for condition values. If non-NULL, return each * unrounded value's positive sign (0 or 1), instead of scaled deltas. * * @Output: * deltas :: * Array to store the deltas (must have space for `num_deltas`). * Values are raw integer deltas scaled left by `shift` bits. * * @Return: * FreeType error code. 0 means success. */ static FT_Error tt_varc_get_item_deltas( TT_Face face, TT_Varc varc, FT_UInt32 var_index, FT_UInt num_deltas, FT_Long* deltas, FT_UInt shift, FT_Fixed* current_coords, FT_UInt num_coords, FT_Short* condition_defaults ) { #ifdef TT_CONFIG_OPTION_GX_VAR_SUPPORT FT_Error error; FT_Memory memory = face->root.memory; FT_Byte* mvs_data; /* MultiItemVariationStore */ FT_Byte* table_limit; FT_UInt16 format; FT_UInt32 regions_offset; FT_UInt16 data_sets_count; FT_UInt outer, inner; FT_Byte* p; FT_Byte* data_set_offsets; FT_UInt32 data_set_offset; FT_Byte* mvd_data; /* MultiVarData */ FT_Byte mvd_format; FT_UInt16 region_count; FT_Byte* delta_sets; FT_UInt32 delta_sets_offset; FT_Byte* tuple_data; FT_UInt tuple_size; FT_Int64 stack_accumulators[VARC_STACK_DELTA_COUNT]; /* deltas * regions */ FT_Int32 stack_all_deltas[VARC_STACK_DELTA_COUNT * 16]; FT_Int64* accumulators = NULL; FT_Byte* regions_data = NULL; FT_Byte* regions_limit = NULL; FT_UInt32 region_list_count = 0; FT_UInt total_deltas; FT_Int32* all_deltas = NULL; FT_UInt bytes_consumed = 0; FT_Byte* region_indices_ptr; FT_UInt region_idx; FT_UInt i; if ( !varc->multi_var_store_loaded || !varc->multi_var_store ) { /* no variation store - return zeros */ for ( i = 0; i < num_deltas; i++ ) deltas[i] = condition_defaults ? FT_BOOL( condition_defaults[i] > 0 ) : 0; return FT_Err_Ok; } mvs_data = varc->multi_var_store; table_limit = (FT_Byte*)varc->table + varc->table_size; /* split `var_index` into `outer` and `inner` */ outer = var_index >> 16; inner = var_index & 0xFFFF; /* parse `MultiItemVariationStore` header */ p = mvs_data; if ( p + 8 > table_limit ) return FT_THROW( Invalid_Table ); /* read format (u16) - must be 1 */ format = ( (FT_UInt16)p[0] << 8 ) | p[1]; p += 2; if ( format != 1 ) return FT_THROW( Invalid_Table ); /* read regions offset (u32) */ regions_offset = ( (FT_UInt32)p[0] << 24 ) | ( (FT_UInt32)p[1] << 16 ) | ( (FT_UInt32)p[2] << 8 ) | (FT_UInt32)p[3]; p += 4; /* read dataSets count (u16) */ data_sets_count = ( (FT_UInt16)p[0] << 8 ) | p[1]; p += 2; /* validate outer index */ if ( outer >= data_sets_count ) return FT_THROW( Invalid_Table ); /* read data set offset for outer index */ data_set_offsets = p; p = data_set_offsets + outer * 4; if ( p + 4 > table_limit ) return FT_THROW( Invalid_Table ); data_set_offset = ( (FT_UInt32)p[0] << 24 ) | ( (FT_UInt32)p[1] << 16 ) | ( (FT_UInt32)p[2] << 8 ) | (FT_UInt32)p[3]; /* get `MultiVarData` */ if ( data_set_offset > (FT_UInt32)( table_limit - mvs_data ) ) return FT_THROW( Invalid_Table ); mvd_data = mvs_data + data_set_offset; if ( table_limit - mvd_data < 7 ) return FT_THROW( Invalid_Table ); /* parse `MultiVarData` header */ p = mvd_data; /* read format (u8) - must be 1 */ mvd_format = *p++; if ( mvd_format != 1 ) return FT_THROW( Invalid_Table ); /* read `regionIndices` count (u16) */ region_count = ( (FT_UInt16)p[0] << 8 ) | p[1]; p += 2; if ( region_count > (FT_ULong)( table_limit - p ) / 2 ) return FT_THROW( Invalid_Table ); p += region_count * 2; if ( table_limit - p < 4 ) return FT_THROW( Invalid_Table ); delta_sets_offset = ( (FT_UInt32)p[0] << 24 ) | ( (FT_UInt32)p[1] << 16 ) | ( (FT_UInt32)p[2] << 8 ) | (FT_UInt32)p[3]; if ( !delta_sets_offset || delta_sets_offset > (FT_ULong)( table_limit - mvd_data ) || (FT_ULong)( table_limit - mvd_data ) - delta_sets_offset < 4 ) return FT_THROW( Invalid_Table ); delta_sets = mvd_data + delta_sets_offset; /* read tuple from INDEX2 at inner index */ error = tt_varc_read_cff2_index_entry( delta_sets, table_limit, inner, &tuple_data, &tuple_size ); if ( error ) return error; /* allocate 64-bit accumulators for each delta */ if ( num_deltas <= VARC_STACK_DELTA_COUNT ) accumulators = stack_accumulators; else { if ( FT_NEW_ARRAY( accumulators, num_deltas ) ) { error = FT_THROW( Out_Of_Memory ); goto Cleanup; } } /* initialize accumulators to zero */ FT_ARRAY_ZERO( accumulators, num_deltas ); if ( condition_defaults ) { for ( i = 0; i < num_deltas; i++ ) { #ifdef FT_INT64 accumulators[i] = (FT_Int64)condition_defaults[i] * 0x10000L; #else accumulators[i].lo = (FT_UInt32)(FT_Int32)condition_defaults[i] << 16; accumulators[i].hi = condition_defaults[i] < 0 ? 0xFFFFFFFFU : 0; #endif } } /* parse `SparseVarRegionList` to get region definitions */ if ( regions_offset > 0 && regions_offset <= (FT_ULong)( table_limit - mvs_data ) ) { regions_data = mvs_data + regions_offset; regions_limit = table_limit; /* read region count (u32) */ if ( regions_limit - regions_data >= 4 ) { region_list_count = ( (FT_UInt32)regions_data[0] << 24 ) | ( (FT_UInt32)regions_data[1] << 16 ) | ( (FT_UInt32)regions_data[2] << 8 ) | (FT_UInt32)regions_data[3]; regions_data += 4; if ( region_list_count > (FT_ULong)( regions_limit - regions_data ) / 4 ) regions_data = NULL; } else regions_data = NULL; } if ( !regions_data ) { if ( accumulators != stack_accumulators ) FT_FREE( accumulators ); error = FT_THROW( Invalid_Table ); goto Cleanup; } /* read ALL deltas from the flat tuple */ /* (region_count * num_deltas values) */ /* a wrapped product would under-size `all_deltas` below */ if ( num_deltas != 0 && region_count > FT_UINT_MAX / num_deltas ) { if ( accumulators != stack_accumulators ) FT_FREE( accumulators ); error = FT_THROW( Invalid_Table ); goto Cleanup; } total_deltas = region_count * num_deltas; /* allocate space for all deltas */ if ( total_deltas <= VARC_STACK_DELTA_COUNT * 16 ) all_deltas = stack_all_deltas; else { if ( FT_NEW_ARRAY( all_deltas, total_deltas ) ) { if ( accumulators != stack_accumulators ) FT_FREE( accumulators ); error = FT_THROW( Out_Of_Memory ); goto Cleanup; } } /* read all deltas from TupleValues */ error = tt_varc_read_tuple_deltas( tuple_data, tuple_data + tuple_size, total_deltas, all_deltas, &bytes_consumed ); if ( error ) { if ( all_deltas != stack_all_deltas ) FT_FREE( all_deltas ); if ( accumulators != stack_accumulators ) FT_FREE( accumulators ); goto Cleanup; } /* parse regionIndices and accumulate scaled deltas */ /* after format(u8) + region_count(u16) */ region_indices_ptr = mvd_data + 3; for ( region_idx = 0; region_idx < region_count; region_idx++ ) { FT_UInt16 region_index; FT_Fixed region_scalar; FT_UInt k; /* read region index from `regionIndices` array (u16) */ if ( region_indices_ptr + 2 > table_limit ) break; region_index = ( (FT_UInt16)region_indices_ptr[0] << 8 ) | region_indices_ptr[1]; region_indices_ptr += 2; /* evaluate region to get scalar */ if ( regions_data && current_coords ) { /* SparseVarRegionList format: * * u32: regionCount * u32[regionCount]: array of offsets (from start of list) * to each region */ /* back to start (before count) */ FT_Byte* region_list_start = regions_data - 4; FT_Byte* region_offset_ptr; if ( region_index < region_list_count && region_index < (FT_ULong)( regions_limit - regions_data ) / 4 ) { FT_UInt32 region_offset; region_offset_ptr = regions_data + region_index * 4; region_offset = ( (FT_UInt32)region_offset_ptr[0] << 24 ) | ( (FT_UInt32)region_offset_ptr[1] << 16 ) | ( (FT_UInt32)region_offset_ptr[2] << 8 ) | (FT_UInt32)region_offset_ptr[3]; if ( region_offset < (FT_UInt32)( regions_limit - region_list_start ) ) region_scalar = tt_varc_evaluate_region( region_list_start + region_offset, regions_limit, current_coords, num_coords ); else region_scalar = 0; } else region_scalar = 0; } else { /* no coords or regions - use 1.0 for first region, 0 for others */ region_scalar = ( region_idx == 0 ) ? 0x10000 : 0; } /* scale and accumulate deltas for this region; */ /* tuple is organized as regions-major: */ /* */ /* [d0_r0, d1_r0, ..., dN_r0, d0_r1, ...] */ if ( region_scalar != 0 ) { FT_UInt base_offset = region_idx * num_deltas; for ( k = 0; k < num_deltas; k++ ) { /* plain integer */ FT_Int32 region_delta = all_deltas[base_offset + k]; /* accumulate using 64-bit math: accum += delta * scalar */ #ifdef FT_INT64 FT_Int64 product = (FT_Int64)region_delta * region_scalar; accumulators[k] += product; #else /* !FT_INT64 */ /* 32-bit fallback */ if ( (FT_UInt32)region_delta + 0x8000U <= 0xFFFFU ) { /* fast path - multiplication fits in 32 bits */ FT_Int32 product = region_delta * region_scalar; accumulators[k].lo += (FT_UInt32)product; if ( accumulators[k].lo < (FT_UInt32)product ) accumulators[k].hi += 1; if ( product < 0 ) accumulators[k].hi -= 1; } else { /* slow path - full 64-bit signed multiplication */ FT_UInt32 a = (FT_UInt32)ULABS( region_delta ); FT_UInt32 b = (FT_UInt32)ULABS( region_scalar ); FT_UInt32 a_lo = a & 0xFFFF; FT_UInt32 a_hi = a >> 16; FT_UInt32 b_lo = b & 0xFFFF; FT_UInt32 b_hi = b >> 16; FT_UInt32 mid = ( a_lo * b_hi ) + ( a_hi * b_lo ); FT_UInt32 lo = a_lo * b_lo; FT_UInt32 hi = a_hi * b_hi; hi += ( mid >> 16 ) + ( ( ( lo >> 16 ) + ( mid & 0xFFFF ) ) >> 16 ); lo += ( mid << 16 ); if ( ( region_delta < 0 ) != ( region_scalar < 0 ) ) { lo = ~lo + 1; hi = ~hi + !lo; } accumulators[k].lo += lo; if ( accumulators[k].lo < lo ) accumulators[k].hi += 1; accumulators[k].hi += hi; } #endif /* !FT_INT64 */ } } } /* free the flat delta array */ if ( all_deltas != stack_all_deltas ) FT_FREE( all_deltas ); /* Convert 64-bit accumulators to deltas with rounding. */ /* Accumulators hold: raw_int_delta * region_scalar_16.16 */ /* We shift right by (16 - shift) to preserve `shift` fractional */ /* bits from the interpolation. */ { FT_Int right_shift = 16 - shift; FT_Long rounding = right_shift > 0 ? ( 1L << ( right_shift - 1 ) ) : 0; for ( i = 0; i < num_deltas; i++ ) { #ifdef FT_INT64 if ( condition_defaults ) deltas[i] = FT_BOOL( accumulators[i] > 0 ); else deltas[i] = (FT_Long)( ( accumulators[i] + rounding ) >> right_shift ); #else /* 32-bit fallback */ FT_UInt32 hi = accumulators[i].hi; FT_UInt32 lo = accumulators[i].lo; if ( condition_defaults ) { deltas[i] = FT_BOOL( !( hi & 0x80000000U ) && ( hi || lo ) ); continue; } /* add rounding */ lo += (FT_UInt32)rounding; if ( lo < (FT_UInt32)rounding ) hi += 1; /* shift right by (16 - shift) bits */ if ( right_shift == 0 ) deltas[i] = (FT_Long)(FT_Int32)lo; else deltas[i] = (FT_Long)(FT_Int32)( ( hi << ( 16 + shift ) ) | ( lo >> right_shift ) ); #endif } } /* free accumulators */ if ( accumulators != stack_accumulators ) FT_FREE( accumulators ); error = FT_Err_Ok; Cleanup: return error; #else /* !TT_CONFIG_OPTION_GX_VAR_SUPPORT */ FT_UNUSED( face ); FT_UNUSED( varc ); FT_UNUSED( var_index ); FT_UNUSED( num_deltas ); FT_UNUSED( deltas ); FT_UNUSED( shift ); FT_UNUSED( current_coords ); FT_UNUSED( num_coords ); FT_UNUSED( condition_defaults ); return FT_THROW( Unimplemented_Feature ); #endif /* !TT_CONFIG_OPTION_GX_VAR_SUPPORT */ } /************************************************************************** * * Condition Evaluation * */ /************************************************************************** * * @Function: * tt_varc_get_condition * * @Description: * Look up a 'Condition' by index in the top-level `ConditionList`. * ConditionList: * uint32 count, then Offset32[count] from its start. * * @Input: * varc :: * The 'VARC' table structure. * * cond_index :: * The index into the `ConditionList` structure. * * table_limit :: * End of valid table data. * * @Return: * The condition offset. */ static FT_Byte* tt_varc_get_condition( TT_Varc varc, FT_UInt32 cond_index, FT_Byte* table_limit ) { FT_Byte* cl = varc->condition_list; FT_UInt32 count; FT_Byte* p; FT_UInt32 off; if ( !cl || cl + 4 > table_limit ) return NULL; count = ( (FT_UInt32)cl[0] << 24 ) | ( (FT_UInt32)cl[1] << 16 ) | ( (FT_UInt32)cl[2] << 8 ) | (FT_UInt32)cl[3]; if ( cond_index >= count ) return NULL; /* bound the index with division so `cond_index * 4` cannot overflow */ if ( cond_index > (FT_UInt32)( ( table_limit - cl - 4 ) / 4 ) ) return NULL; p = cl + 4 + cond_index * 4; if ( p + 4 > table_limit ) return NULL; off = ( (FT_UInt32)p[0] << 24 ) | ( (FT_UInt32)p[1] << 16 ) | ( (FT_UInt32)p[2] << 8 ) | (FT_UInt32)p[3]; if ( off >= (FT_UInt32)( table_limit - cl ) ) return NULL; return cl + off; } /************************************************************************** * * @Function: * tt_varc_eval_condition * * @Description: * Evaluate a 'Condition', matching HarfBuzz/skrifa/fontTools. * Sub-condition offsets in the And/Or/Negate formats are 24-bit and * relative to the start of the containing condition. * * @Input: * face :: * The font face. * * varc :: * The VARC table. * * cond :: * Pointer to the Condition. * * table_limit :: * End of valid table data. * * coords :: * Current normalized coordinates (F2DOT14). * * coord_count :: * Number of coordinates. * * depth :: * Recursion depth. * * @Return: * TRUE when the condition is satisfied, FALSE otherwise. */ static FT_Bool tt_varc_eval_condition( TT_Face face, TT_Varc varc, FT_Byte* cond, FT_Byte* table_limit, FT_Fixed* coords, FT_UInt num_coords, FT_UInt depth ) { FT_UInt format; /* A zero condition offset represents True. Top-level zero offsets */ /* resolve to the start of the ConditionList. */ if ( cond == varc->condition_list ) return TRUE; if ( !cond || depth > TT_VARC_MAX_NESTING_LEVEL || cond + 2 > table_limit ) return FALSE; format = ( (FT_UInt)cond[0] << 8 ) | cond[1]; switch ( format ) { case 1: /* ConditionAxisRange */ { FT_UInt axis_index; FT_Short min_val, max_val; FT_Fixed coord; if ( cond + 8 > table_limit ) return FALSE; axis_index = ( (FT_UInt)cond[2] << 8 ) | cond[3]; min_val = (FT_Short)( ( (FT_UShort)cond[4] << 8 ) | (FT_UShort)cond[5] ); max_val = (FT_Short)( ( (FT_UShort)cond[6] << 8 ) | (FT_UShort)cond[7] ); coord = ( coords && axis_index < num_coords ) ? coords[axis_index] : 0; /* min_val/max_val are F2DOT14; coords are 16.16 (F2DOT14 << 2) */ return FT_BOOL( coord >= ( (FT_Fixed)min_val * 4 ) && coord <= ( (FT_Fixed)max_val * 4 ) ); } case 2: /* ConditionValue */ { FT_Short default_value; FT_UInt32 var_idx; FT_Long positive; if ( cond + 8 > table_limit ) return FALSE; default_value = (FT_Short)( ( (FT_UShort)cond[2] << 8 ) | (FT_UShort)cond[3] ); var_idx = ( (FT_UInt32)cond[4] << 24 ) | ( (FT_UInt32)cond[5] << 16 ) | ( (FT_UInt32)cond[6] << 8 ) | (FT_UInt32)cond[7]; /* Compare the unrounded default plus interpolated delta. */ /* Even a small positive fraction makes a condition true. */ positive = FT_BOOL( default_value > 0 ); if ( var_idx != 0xFFFFFFFFUL ) (void)tt_varc_get_item_deltas( face, varc, var_idx, 1, &positive, 0, coords, num_coords, &default_value ); return FT_BOOL( positive ); } case 3: /* ConditionAnd */ case 4: /* ConditionOr */ { FT_UInt count, i; FT_Byte* p; FT_Bool is_and = FT_BOOL( format == 3 ); if ( cond + 3 > table_limit ) return FALSE; count = cond[2]; p = cond + 3; if ( p + (FT_Offset)count * 3 > table_limit ) return FALSE; for ( i = 0; i < count; i++ ) { FT_UInt32 sub_off = ( (FT_UInt32)p[0] << 16 ) | ( (FT_UInt32)p[1] << 8 ) | (FT_UInt32)p[2]; FT_Bool r; p += 3; r = sub_off == 0 ? TRUE : tt_varc_eval_condition( face, varc, cond + sub_off, table_limit, coords, num_coords, depth + 1 ); if ( is_and && !r ) return FALSE; if ( !is_and && r ) return TRUE; } return is_and; } case 5: /* ConditionNegate */ { FT_UInt32 sub_off; if ( cond + 5 > table_limit ) return FALSE; sub_off = ( (FT_UInt32)cond[2] << 16 ) | ( (FT_UInt32)cond[3] << 8 ) | (FT_UInt32)cond[4]; if ( sub_off == 0 ) return FALSE; return FT_BOOL( !tt_varc_eval_condition( face, varc, cond + sub_off, table_limit, coords, num_coords, depth + 1 ) ); } default: /* Unknown format: treat as not satisfied (matches fontTools). */ return FALSE; } } /************************************************************************** * * Axis Index Reading * */ /************************************************************************** * * @Function: * tt_varc_get_axis_indices * * @Description: * Read axis indices from the axis indices list (TupleList of integers). * * @Input: * varc :: * The VARC table. * * axis_indices_index :: * Index into the TupleList. * * num_indices :: * Number of indices to read. * * @Output: * indices :: * Array to store axis indices (caller allocates). * * @Return: * FreeType error code. 0 means success. */ static FT_Error tt_varc_get_axis_indices( TT_Varc varc, FT_UInt axis_indices_index, FT_UInt num_indices, FT_UInt* indices ) { FT_Error error; FT_Byte* table_limit = (FT_Byte*)varc->table + varc->table_size; FT_Byte* tuple_data; FT_UInt tuple_size; FT_Byte* tuple_limit; FT_Byte* p; FT_UInt i, j; if ( !varc || !varc->axis_indices_list || !indices ) return FT_THROW( Invalid_Argument ); error = tt_varc_read_cff2_index_entry( varc->axis_indices_list, table_limit, axis_indices_index, &tuple_data, &tuple_size ); if ( error ) return error; tuple_limit = tuple_data + tuple_size; p = tuple_data; /* read indices using TupleValues encoding */ i = 0; while ( i < num_indices && p < tuple_limit ) { FT_Byte control; FT_UInt cnt; control = *p++; cnt = ( control & 0x3F ) + 1; if ( cnt > num_indices - i ) cnt = num_indices - i; if ( ( control & 0xC0 ) == 0xC0 ) { /* 32-bit values */ if ( p + 4 * cnt > tuple_limit ) return FT_THROW( Invalid_Table ); for ( j = 0; j < cnt && i < num_indices; j++, i++ ) { indices[i] = ( (FT_UInt)p[0] << 24 ) | ( (FT_UInt)p[1] << 16 ) | ( (FT_UInt)p[2] << 8 ) | (FT_UInt)p[3]; p += 4; } } else if ( control & 0x80 ) { /* zeros */ for ( j = 0; j < cnt && i < num_indices; j++, i++ ) indices[i] = 0; } else if ( control & 0x40 ) { /* 2-byte values */ if ( p + 2 * cnt > tuple_limit ) return FT_THROW( Invalid_Table ); for ( j = 0; j < cnt && i < num_indices; j++, i++ ) { indices[i] = ( (FT_UInt)p[0] << 8 ) | (FT_UInt)p[1]; p += 2; } } else { /* 1-byte values */ if ( p + cnt > tuple_limit ) return FT_THROW( Invalid_Table ); for ( j = 0; j < cnt && i < num_indices; j++, i++ ) indices[i] = *p++; } } if ( i < num_indices ) return FT_THROW( Invalid_Table ); return FT_Err_Ok; } /************************************************************************** * * Variation Delta Application Functions * */ /************************************************************************** * * @Function: * tt_varc_apply_axis_deltas * * @Description: * Apply variation deltas to axis values using the * `MultiItemVariationStore`. * * @Input: * face :: * The font face. * * varc :: * The VARC table. * * component :: * The component whose axis values to vary. * * current_coords :: * Current normalized coordinates (F2DOT14). * * num_coords :: * Number of coords in `current_coords`. */ static void tt_varc_apply_axis_deltas( TT_Face face, TT_Varc varc, TT_VarcComponent component, FT_Fixed* current_coords, FT_UInt num_coords ) { #ifdef TT_CONFIG_OPTION_GX_VAR_SUPPORT FT_Error error; FT_Memory memory = face->root.memory; FT_Long stack_deltas[VARC_STACK_DELTA_COUNT]; FT_Long* deltas = NULL; FT_UInt i; if ( !varc->multi_var_store_loaded || component->num_axis_values == 0 ) return; /* use stack allocation for small arrays, heap for large */ if ( component->num_axis_values <= VARC_STACK_DELTA_COUNT ) deltas = stack_deltas; else { if ( FT_NEW_ARRAY( deltas, component->num_axis_values ) ) return; } /* Get deltas with shift=2: raw F2DOT14 integers are returned */ /* already shifted to F16DOT16, matching `axis_values` format. */ error = tt_varc_get_item_deltas( face, varc, component->axis_values_var_index, component->num_axis_values, deltas, 2, current_coords, num_coords, NULL ); if ( !error ) { for ( i = 0; i < component->num_axis_values; i++ ) component->axis_values[i] += deltas[i]; } #ifdef FT_DEBUG_LEVEL_TRACE else FT_TRACE2(( "tt_varc_apply_axis_deltas: variation index" " 0x%08lx failed (error 0x%x)\n", (FT_ULong)component->axis_values_var_index, error )); #endif if ( deltas != stack_deltas ) FT_FREE( deltas ); #else /* !TT_CONFIG_OPTION_GX_VAR_SUPPORT */ FT_UNUSED( face ); FT_UNUSED( varc ); FT_UNUSED( component ); FT_UNUSED( current_coords ); FT_UNUSED( num_coords ); #endif /* !TT_CONFIG_OPTION_GX_VAR_SUPPORT */ } /************************************************************************** * * @Function: * tt_varc_apply_transform_deltas * * @Description: * Apply variation deltas to transform values using the * `MultiItemVariationStore`. * * @Input: * face :: * The font face. * * varc :: * The VARC table. * * component :: * The component whose transform values to vary. * * current_coords :: * Current normalized coordinates (F2DOT14). * * num_coords :: * Number of coords in `current_coords`. */ static void tt_varc_apply_transform_deltas( TT_Face face, TT_Varc varc, TT_VarcComponent component, FT_Fixed* current_coords, FT_UInt num_coords ) { #ifdef TT_CONFIG_OPTION_GX_VAR_SUPPORT FT_Error error; FT_Memory memory = face->root.memory; FT_Long stack_deltas[VARC_STACK_DELTA_COUNT]; FT_Long* deltas = NULL; FT_UInt num_deltas = 0; FT_UInt delta_index = 0; if ( !varc->multi_var_store_loaded ) return; /* count how many transform components are present */ if ( component->flags & VARC_HAVE_TRANSLATE_X ) num_deltas++; if ( component->flags & VARC_HAVE_TRANSLATE_Y ) num_deltas++; if ( component->flags & VARC_HAVE_ROTATION ) num_deltas++; if ( component->flags & VARC_HAVE_SCALE_X ) num_deltas++; if ( component->flags & VARC_HAVE_SCALE_Y ) num_deltas++; if ( component->flags & VARC_HAVE_SKEW_X ) num_deltas++; if ( component->flags & VARC_HAVE_SKEW_Y ) num_deltas++; if ( component->flags & VARC_HAVE_TCENTER_X ) num_deltas++; if ( component->flags & VARC_HAVE_TCENTER_Y ) num_deltas++; if ( num_deltas == 0 ) return; /* use stack allocation for small arrays, heap for large */ if ( num_deltas <= VARC_STACK_DELTA_COUNT ) deltas = stack_deltas; else { if ( FT_NEW_ARRAY( deltas, num_deltas ) ) return; } /* Get deltas with shift=4: raw integers shifted left by 4. */ /* F4DOT12 fields (rotation, skew) become F16DOT16 directly. */ /* FWORD and F6DOT10 fields need an additional << 2 at callsite. */ error = tt_varc_get_item_deltas( face, varc, component->transform_var_index, num_deltas, deltas, 4, current_coords, num_coords, NULL ); if ( !error ) { /* Apply deltas to transform components in order. */ /* Deltas are already shifted left by 4 from get_item_deltas. */ /* F4DOT12 fields (shift=4): add directly (already F16DOT16). */ /* FWORD fields (shift=6): need << 2 more for 26.6 format. */ /* F6DOT10 fields (shift=6): need << 2 more for F16DOT16. */ delta_index = 0; if ( component->flags & VARC_HAVE_TRANSLATE_X ) { /* FWORD: 4+2=6 */ component->translate_x += deltas[delta_index] * 4; delta_index++; } if ( component->flags & VARC_HAVE_TRANSLATE_Y ) { /* FWORD: 4+2=6 */ component->translate_y += deltas[delta_index] * 4; delta_index++; } if ( component->flags & VARC_HAVE_ROTATION ) { /* F4DOT12: already 16.16 */ component->rotation += deltas[delta_index]; delta_index++; } if ( component->flags & VARC_HAVE_SCALE_X ) { /* F6DOT10: 4+2=6 */ component->scale_x += deltas[delta_index] * 4; delta_index++; } if ( component->flags & VARC_HAVE_SCALE_Y ) { /* F6DOT10: 4+2=6 */ component->scale_y += deltas[delta_index] * 4; delta_index++; } if ( component->flags & VARC_HAVE_SKEW_X ) { /* F4DOT12: already 16.16 */ component->skew_x += deltas[delta_index]; delta_index++; } if ( component->flags & VARC_HAVE_SKEW_Y ) { /* F4DOT12: already 16.16 */ component->skew_y += deltas[delta_index]; delta_index++; } if ( component->flags & VARC_HAVE_TCENTER_X ) { /* FWORD: 4+2=6 */ component->tcenter_x += deltas[delta_index] * 4; delta_index++; } if ( component->flags & VARC_HAVE_TCENTER_Y ) { /* FWORD: 4+2=6 */ component->tcenter_y += deltas[delta_index] * 4; delta_index++; } /* When ScaleY is absent it mirrors ScaleX. Re-apply the mirror */ /* after variation so a varying uniform scale stays uniform, */ /* matching fontTools and skrifa (which redo this post-delta). */ if ( ( component->flags & VARC_HAVE_SCALE_X ) && !( component->flags & VARC_HAVE_SCALE_Y ) ) component->scale_y = component->scale_x; } #ifdef FT_DEBUG_LEVEL_TRACE else FT_TRACE2(( "tt_varc_apply_transform_deltas: variation index" " 0x%08lx failed (error 0x%x)\n", (FT_ULong)component->transform_var_index, error )); #endif if ( deltas != stack_deltas ) FT_FREE( deltas ); #else /* !TT_CONFIG_OPTION_GX_VAR_SUPPORT */ FT_UNUSED( face ); FT_UNUSED( varc ); FT_UNUSED( component ); FT_UNUSED( current_coords ); FT_UNUSED( num_coords ); #endif /* !TT_CONFIG_OPTION_GX_VAR_SUPPORT */ } /************************************************************************** * * Transform Matrix Functions * */ /************************************************************************** * * @Function: * tt_varc_build_transform * * @Description: * Build a transformation matrix from component transform values. * * Transformation order (per VARC spec): * * 1. translate to transformation center (-tCenterX, -tCenterY) * 2. apply scale (scaleX, scaleY) * 3. apply skew (skewX, skewY) * 4. apply rotation (rotation * pi) * 5. translate back from center (tCenterX, tCenterY) * 6. apply final translation (translateX, translateY) * * @Input: * component :: * The component with transform values. * * @Output: * matrix :: * The resulting 2x2 transformation matrix. * * offset :: * The translation vector. */ static void tt_varc_build_transform( TT_VarcComponent component, FT_Matrix* matrix, FT_Vector* offset ) { FT_Fixed scale_x = component->scale_x; FT_Fixed scale_y = component->scale_y; FT_Fixed skew_x = component->skew_x; FT_Fixed skew_y = component->skew_y; FT_Fixed rotation = component->rotation; /* Build rotation matrix */ if ( rotation != 0 ) { FT_Angle angle; FT_Vector v; /* Rotation is F4DOT12 in multiples of pi, converted to F16DOT16 by */ /* << 4. FT_ANGLE_PI = 180 * 65536 (degrees), so this converts pi */ /* multiples to degrees. */ angle = FT_MulFix( rotation, FT_ANGLE_PI ); FT_Vector_Unit( &v, angle ); /* Rotation matrix: [cos -sin] [sin cos] */ matrix->xx = v.x; matrix->xy = -v.y; matrix->yx = v.y; matrix->yy = v.x; } else { matrix->xx = 0x10000; matrix->xy = 0; matrix->yx = 0; matrix->yy = 0x10000; } /* Apply scale - scale each column, not row. */ /* First column (xx, yx) represents X-axis, scaled by `scale_x`. */ /* Second column (xy, yy) represents Y-axis, scaled by `scale_y`. */ if ( scale_x != 0x10000 || scale_y != 0x10000 ) { matrix->xx = FT_MulFix( matrix->xx, scale_x ); matrix->yx = FT_MulFix( matrix->yx, scale_x ); matrix->xy = FT_MulFix( matrix->xy, scale_y ); matrix->yy = FT_MulFix( matrix->yy, scale_y ); } /* apply skew */ if ( skew_x != 0 || skew_y != 0 ) { FT_Matrix skew_matrix; FT_Fixed tan_x, tan_y; /* Convert skew angles to tangent values. */ /* Skew is in radians * pi, so we need to compute tan(skew * pi). */ tan_x = skew_x ? FT_Tan( FT_MulFix( skew_x, FT_ANGLE_PI ) ) : 0; tan_y = skew_y ? FT_Tan( FT_MulFix( skew_y, FT_ANGLE_PI ) ) : 0; /* The transform uses skew(-skewX, skewY), so only the xy term */ /* (the skewX contribution) is negated; the yx (skewY) term is */ /* positive. Matches HarfBuzz/skrifa/fontTools. */ skew_matrix.xx = 0x10000; skew_matrix.xy = -tan_x; skew_matrix.yx = tan_y; skew_matrix.yy = 0x10000; /* Apply skew on the RIGHT so the linear part is Rot*Scale*Skew */ /* (skew applied to the point first). `FT_Matrix_Multiply(a, b)` */ /* computes b = a*b, so this leaves skew_matrix = matrix*skew; */ /* copy it back into matrix. */ FT_Matrix_Multiply( matrix, &skew_matrix ); *matrix = skew_matrix; } /* calculate translation with center point transformation */ offset->x = component->translate_x; offset->y = component->translate_y; if ( component->tcenter_x != 0 || component->tcenter_y != 0 ) { FT_Vector center; center.x = component->tcenter_x; center.y = component->tcenter_y; /* offset += center - matrix * center */ offset->x += center.x - FT_MulFix( matrix->xx, center.x ) - FT_MulFix( matrix->xy, center.y ); offset->y += center.y - FT_MulFix( matrix->yx, center.x ) - FT_MulFix( matrix->yy, center.y ); } } /************************************************************************** * * Recursion Context Functions * */ /************************************************************************** * * @Function: * tt_varc_context_push * * @Description: * Push a glyph ID onto the recursion stack. * * @Input: * context :: * The recursion context. * * glyph_index :: * The glyph ID to push. * * @Return: * FreeType error code. Returns `FT_Err_Invalid_Table` if cycle * detected or max depth exceeded. */ static FT_Error tt_varc_context_push( TT_VarcContext context, FT_UInt glyph_index ) { FT_UInt i; /* check max recursion depth */ if ( context->recursion_depth >= TT_VARC_MAX_NESTING_LEVEL ) { FT_TRACE2(( "tt_varc_context_push: nesting limit reached" " for glyph %u\n", glyph_index )); return FT_THROW( Invalid_Table ); } /* check for cycles */ for ( i = 0; i < context->recursion_depth; i++ ) { if ( context->visited_glyphs[i] == glyph_index ) { FT_TRACE2(( "tt_varc_context_push: cycle detected at glyph %u\n", glyph_index )); return FT_THROW( Invalid_Table ); /* cycle detected */ } } /* push onto stack */ context->visited_glyphs[context->recursion_depth] = glyph_index; context->recursion_depth++; return FT_Err_Ok; } /************************************************************************** * * @Function: * tt_varc_context_pop * * @Description: * Pop a glyph ID from the recursion stack. * * @Input: * context :: * The recursion context. */ static void tt_varc_context_pop( TT_VarcContext context ) { if ( context->recursion_depth > 0 ) context->recursion_depth--; } /************************************************************************** * * VARC Table Loading Functions * */ /************************************************************************** * * @Function: * tt_face_load_varc * * @Description: * Load the 'VARC' table from the font file. * * @Input: * face :: * The font face. * * stream :: * The font stream. * * @Return: * FreeType error code. 0 means success. */ FT_LOCAL_DEF( FT_Error ) tt_face_load_varc( FT_Face ftface, FT_Stream stream ) { TT_Face face = (TT_Face)ftface; FT_Memory memory = face->root.memory; FT_Error error; FT_Byte* table = NULL; FT_ULong table_size; TT_Varc varc = NULL; FT_Byte* p; /* Initialize VARC loading flag */ face->varc_loading_components = FALSE; face->varc_context = NULL; FT_TRACE2(( "VARC " )); error = face->goto_table( face, TTAG_VARC, stream, &table_size ); if ( error ) { FT_TRACE2(( "is missing\n" )); goto Exit; } if ( table_size < 24 ) /* minimum header size */ { FT_TRACE2(( "is too short\n" )); goto Exit; } if ( FT_FRAME_EXTRACT( table_size, table ) ) { FT_TRACE2(( "could not be read\n" )); goto Exit; } if ( FT_NEW( varc ) ) { FT_TRACE2(( "could not be loaded\n" )); goto Fail; } varc->table = table; varc->table_size = table_size; p = table; /* read header */ varc->version_major = ( p[0] << 8 ) | p[1]; varc->version_minor = ( p[2] << 8 ) | p[3]; p += 4; /* check version */ if ( varc->version_major != 1 || varc->version_minor != 0 ) { FT_TRACE2(( "has unsupported version %u.%u\n", varc->version_major, varc->version_minor )); error = FT_THROW( Invalid_Table ); goto Fail; } /* read offsets */ varc->coverage_offset = ( (FT_ULong)p[0] << 24 ) | ( (FT_ULong)p[1] << 16 ) | ( (FT_ULong)p[2] << 8 ) | (FT_ULong)p[3]; p += 4; varc->multi_var_store_offset = ( (FT_ULong)p[0] << 24 ) | ( (FT_ULong)p[1] << 16 ) | ( (FT_ULong)p[2] << 8 ) | (FT_ULong)p[3]; p += 4; varc->condition_list_offset = ( (FT_ULong)p[0] << 24 ) | ( (FT_ULong)p[1] << 16 ) | ( (FT_ULong)p[2] << 8 ) | (FT_ULong)p[3]; p += 4; varc->axis_indices_list_offset = ( (FT_ULong)p[0] << 24 ) | ( (FT_ULong)p[1] << 16 ) | ( (FT_ULong)p[2] << 8 ) | (FT_ULong)p[3]; p += 4; varc->var_composite_glyphs_offset = ( (FT_ULong)p[0] << 24 ) | ( (FT_ULong)p[1] << 16 ) | ( (FT_ULong)p[2] << 8 ) | (FT_ULong)p[3]; /* validate offsets */ if ( varc->coverage_offset >= table_size || varc->var_composite_glyphs_offset >= table_size ) { FT_TRACE2(( "has invalid offsets\n" )); error = FT_THROW( Invalid_Table ); goto Fail; } /* set up pointers */ varc->coverage = table + varc->coverage_offset; varc->var_composite_glyphs = table + varc->var_composite_glyphs_offset; if ( varc->condition_list_offset > 0 && varc->condition_list_offset < table_size ) varc->condition_list = table + varc->condition_list_offset; else varc->condition_list = NULL; if ( varc->axis_indices_list_offset > 0 && varc->axis_indices_list_offset < table_size ) varc->axis_indices_list = table + varc->axis_indices_list_offset; else varc->axis_indices_list = NULL; #ifdef TT_CONFIG_OPTION_GX_VAR_SUPPORT /* `MultiItemVariationStore` is NOT the same as `ItemVariationStore`! */ /* `MultiItemVariationStore` stores TUPLES of deltas, */ /* not single deltas. */ varc->multi_var_store_loaded = FALSE; varc->multi_var_store = NULL; if ( varc->multi_var_store_offset > 0 && varc->multi_var_store_offset < table_size ) { /* point to the `MultiItemVariationStore` data in the 'VARC' table */ varc->multi_var_store = table + varc->multi_var_store_offset; varc->multi_var_store_loaded = TRUE; /* TODO: Implement proper MultiItemVariationStore parser: * - Format: u16 (must be 1) * - Regions: Offset32 to SparseVarRegionList * - DataSets: Array16 of Offset32 to MultiVarData * MultiVarData format: * - Format: u8 (must be 1) * - RegionIndices: Array16 of u16 * - DeltaSets: INDEX2 of TupleValues (CFF2-style index) */ } #endif /* TT_CONFIG_OPTION_GX_VAR_SUPPORT */ face->varc = varc; FT_TRACE2(( "loaded\n" )); FT_TRACE4(( " version %u.%u, %lu bytes\n", varc->version_major, varc->version_minor, varc->table_size )); FT_TRACE4(( " offsets: coverage %lu, variation store %lu,\n", varc->coverage_offset, varc->multi_var_store_offset )); FT_TRACE4(( " conditions %lu, axis indices %lu, glyphs %lu\n", varc->condition_list_offset, varc->axis_indices_list_offset, varc->var_composite_glyphs_offset )); goto Exit; Fail: FT_FREE( varc ); FT_FRAME_RELEASE( table ); Exit: return error; } /************************************************************************** * * @Function: * tt_face_free_varc * * @Description: * Free the 'VARC' table resources. * * @Input: * face :: * The font face. */ FT_LOCAL_DEF( void ) tt_face_free_varc( FT_Face ftface ) { TT_Face face = (TT_Face)ftface; FT_Memory memory = face->root.memory; FT_Stream stream = face->root.stream; TT_Varc varc = (TT_Varc)face->varc; if ( varc ) { #ifdef TT_CONFIG_OPTION_GX_VAR_SUPPORT /* `MultiItemVariationStore` cleanup. */ /* No special cleanup needed - */ /* we just point to data in the 'VARC' table. */ varc->multi_var_store = NULL; varc->multi_var_store_loaded = FALSE; #endif FT_FRAME_RELEASE( varc->table ); FT_FREE( varc ); face->varc = NULL; } } /************************************************************************** * * @Function: * tt_face_has_varc_glyph * * @Description: * Check whether a glyph has VARC data. * * @Input: * face :: * The font face. * * glyph_index :: * The glyph ID to check. * * @Return: * TRUE if the glyph has VARC data, FALSE otherwise. */ FT_LOCAL_DEF( FT_Bool ) tt_face_has_varc_glyph( FT_Face ftface, FT_UInt glyph_index ) { TT_Face face = (TT_Face)ftface; TT_Varc varc = (TT_Varc)face->varc; FT_Int coverage_idx; if ( !varc ) return FALSE; coverage_idx = tt_varc_get_coverage( varc, glyph_index ); return coverage_idx >= 0; } /************************************************************************** * * VARC Glyph Loading Functions * */ #ifdef TT_CONFIG_OPTION_GX_VAR_SUPPORT /************************************************************************** * * @Function: * tt_varc_set_normalized_coords * * @Description: * Lightweight coordinate swap for VARC component loading. * Unlike `FT_Set_Var_Blend_Coordinates`, this only sets the normalized * coordinates and invalidates the gvar tuple scalar cache. It skips * CVT reload, MVAR, auto-hinter invalidation, PS name construction, * and design coordinate computation - none of which are needed when * loading VARC components with `FT_LOAD_NO_HINTING`. * * @Input: * face :: * The font face. * * coords :: * Current normalized coordinates (F2DOT14). * * coord_count :: * Number of coordinates. */ static void tt_varc_set_normalized_coords( TT_Face face, FT_Fixed* coords, FT_UInt num_coords ) { GX_Blend blend = face->blend; FT_UInt i; if ( !blend || !blend->normalizedcoords ) return; if ( num_coords > blend->num_axis ) num_coords = blend->num_axis; FT_MEM_COPY( blend->normalizedcoords, coords, num_coords * sizeof ( FT_Fixed ) ); /* zero out remaining axes */ for ( i = num_coords; i < blend->num_axis; i++ ) blend->normalizedcoords[i] = 0; /* update `doblend` flag */ face->doblend = FALSE; for ( i = 0; i < blend->num_axis; i++ ) { if ( blend->normalizedcoords[i] ) { face->doblend = TRUE; break; } } /* Update face variation flag so IS_DEFAULT_INSTANCE is correct. */ /* Without this, `load_truetype_glyph` skips gvar deltas entirely. */ if ( face->doblend ) face->root.face_flags |= FT_FACE_FLAG_VARIATION; else face->root.face_flags &= ~FT_FACE_FLAG_VARIATION; /* invalidate gvar tuple scalar cache */ for ( i = 0; i < blend->tuplecount; i++ ) blend->tuplescalars[i] = (FT_Fixed)-0x20000L; } #endif /* TT_CONFIG_OPTION_GX_VAR_SUPPORT */ /************************************************************************** * * @Function: * tt_face_load_varc_glyph * * @Description: * Load a VARC glyph into the glyph slot. * * @Input: * ftface :: * The font face. * * glyph_slot :: * The glyph slot to load into. * * glyph_index :: * The glyph ID to load. * * load_flags :: * Loading flags. * * @Return: * FreeType error code. 0 means success. */ FT_LOCAL_DEF( FT_Error ) tt_face_load_varc_glyph( FT_Face ftface, FT_GlyphSlot glyph_slot, FT_UInt glyph_index, FT_Int32 load_flags ) { TT_Face face = (TT_Face)ftface; FT_Memory memory = face->root.memory; FT_Error error; TT_Varc varc = (TT_Varc)face->varc; FT_Byte* record_data; FT_UInt record_size; FT_Byte* p = NULL; FT_Byte* limit; TT_VarcContext context; FT_Bool context_owner = FALSE; TT_GlyphSlot slot = (TT_GlyphSlot)glyph_slot; FT_GlyphLoader gloader; FT_Fixed* parent_coords = NULL; FT_UInt num_coords = 0; #ifdef TT_CONFIG_OPTION_GX_VAR_SUPPORT FT_Bool static_axes = FALSE; #endif FT_Fixed stack_new_coords[VARC_STACK_COORD_COUNT]; FT_UInt stack_axis_indices[VARC_STACK_INDICES_COUNT]; FT_Fixed stack_axis_values[VARC_STACK_AXIS_COUNT]; FT_Matrix saved_transform_matrix; FT_Vector saved_transform_delta; FT_UInt saved_transform_flags; if ( !varc ) return FT_THROW( Invalid_Table ); /* get or create recursion context */ context = (TT_VarcContext)face->varc_context; if ( !context ) { /* first call - create new context on heap */ if ( FT_NEW( context ) ) return error; FT_ZERO( context ); face->varc_context = context; context_owner = TRUE; /* save font's current variation coordinates */ /* for inheritance at depth 1 */ #ifdef TT_CONFIG_OPTION_GX_VAR_SUPPORT { FT_MM_Var* master; /* `FT_Get_MM_Var` ensures `face->blend` is initialized */ /* (it's lazy) */ error = FT_Get_MM_Var( (FT_Face)face, &master ); if ( !error && master ) { if ( master->num_axis > 0 ) context->num_font_coords = master->num_axis; FT_Done_MM_Var( face->root.driver->root.library, master ); } else if ( error && !FT_HAS_MULTIPLE_MASTERS( (FT_Face)face ) ) { if ( !face->blend ) (void)tt_var_init_gvar( (FT_Face)face ); if ( face->blend && !face->blend->mmvar ) { (void)tt_get_var_blend( (FT_Face)face, &context->num_font_coords, NULL, NULL, NULL ); static_axes = TRUE; } } error = FT_Err_Ok; /* non-fatal */ } if ( context->num_font_coords > 0 ) { if ( !FT_NEW_ARRAY( context->font_coords, context->num_font_coords ) ) { if ( static_axes ) FT_MEM_COPY( context->font_coords, face->blend->normalizedcoords, context->num_font_coords * sizeof ( FT_Fixed ) ); else error = FT_Get_Var_Blend_Coordinates( (FT_Face)face, context->num_font_coords, context->font_coords ); if ( error ) { FT_FREE( context->font_coords ); context->num_font_coords = 0; } else { FT_Generic saved_autohint = face->root.autohint; /* set initial current_coords to font_coords */ context->current_coords = context->font_coords; context->num_current_coords = context->num_font_coords; if ( !static_axes ) { /* Force blend initialization: allocate */ /* `normalizedcoords`, load 'gvar', etc. This ensures */ /* `tt_varc_set_normalized_coords` can work as a */ /* lightweight coordinate swapper later. */ face->root.autohint.data = NULL; face->root.autohint.finalizer = NULL; FT_Set_Var_Blend_Coordinates( (FT_Face)face, context->num_font_coords, context->font_coords ); face->root.autohint = saved_autohint; } } } } #endif /* TT_CONFIG_OPTION_GX_VAR_SUPPORT */ } /* Save the face's current transform (user's transform at top level, */ /* parent's VARC transform at recursive levels). We restore or clear */ /* this before returning so `FT_Load_Glyph`'s post-processing does the */ /* right thing. */ saved_transform_matrix = face->root.internal->transform_matrix; saved_transform_delta = face->root.internal->transform_delta; saved_transform_flags = face->root.internal->transform_flags; /* get parent coords from context for delta evaluation */ parent_coords = context->current_coords; num_coords = context->num_current_coords; /* push this glyph onto recursion stack */ error = tt_varc_context_push( context, glyph_index ); if ( error ) { if ( context_owner ) face->varc_context = NULL; return error; } /* get glyph record */ error = tt_varc_get_glyph_record( varc, glyph_index, &record_data, &record_size ); if ( error ) goto Cleanup; p = record_data; limit = record_data + record_size; FT_TRACE4(( "VARC glyph %u: nesting level %u, %u-byte record\n", glyph_index, context->recursion_depth, record_size )); /* Allocate a reusable temp glyph slot for component loading. */ /* `FT_New_GlyphSlot` prepends to `face->glyph` list, so */ /* `FT_Load_Glyph` will load into this slot. One slot per */ /* recursion level suffices. */ { FT_GlyphSlot temp_glyph; error = FT_New_GlyphSlot( (FT_Face)face, &temp_glyph ); if ( error ) goto Cleanup; } /* Accumulate into the slot's own loader, like the glyf composite */ /* path; the slot owns this storage and frees it on destruction. */ gloader = slot->internal->loader; FT_GlyphLoader_Rewind( gloader ); /* parse components until we run out of data */ while ( p < limit ) { TT_VarcComponentRec component; FT_GlyphSlot component_slot; FT_Matrix matrix; FT_Vector offset; FT_Matrix composed_matrix; FT_Vector composed_delta; FT_Matrix saved_parent_matrix; FT_Vector saved_parent_delta; FT_Bool saved_has_parent; FT_Fixed* new_coords; FT_Bool new_coords_on_heap; FT_Bool has_axis_override; /* parse component */ error = tt_varc_parse_component( face, varc, &p, limit, &component, stack_axis_values, VARC_STACK_AXIS_COUNT ); if ( error ) { /* skip malformed component and continue with others */ FT_TRACE2(( "tt_face_load_varc_glyph: malformed component" " in glyph %u (error 0x%x)\n", glyph_index, error )); break; /* exit loop - can't safely continue parsing */ /* if we don't know component size */ } FT_TRACE5(( " component glyph %u, flags 0x%08lx\n", component.gid, (FT_ULong)component.flags )); /* Evaluate the component's condition, if any. When it is not */ /* satisfied, the component is not rendered, matching HarfBuzz, */ /* skrifa, and fontTools. */ if ( component.flags & VARC_HAVE_CONDITION ) { FT_Byte* table_limit = (FT_Byte*)varc->table + varc->table_size; FT_Byte* cond = tt_varc_get_condition( varc, component.condition_index, table_limit ); #ifdef FT_DEBUG_LEVEL_TRACE if ( !cond ) FT_TRACE2(( " component glyph %u has invalid condition %lu;" " ignoring condition\n", component.gid, (FT_ULong)component.condition_index )); #endif if ( cond && !tt_varc_eval_condition( face, varc, cond, table_limit, parent_coords, num_coords, 0 ) ) { FT_TRACE5(( " condition %lu is false; skipping component\n", (FT_ULong)component.condition_index )); tt_varc_free_component( face, &component ); continue; } #ifdef FT_DEBUG_LEVEL_TRACE else FT_TRACE6(( " condition %lu is true\n", (FT_ULong)component.condition_index )); #endif } /* apply variation deltas to axis values if present */ if ( component.flags & VARC_AXES_HAVE_VARIATION ) tt_varc_apply_axis_deltas( face, varc, &component, parent_coords, num_coords ); /* apply variation deltas to transform values if present */ if ( component.flags & VARC_TRANSFORM_HAS_VARIATION ) tt_varc_apply_transform_deltas( face, varc, &component, parent_coords, num_coords ); /* build transformation matrix */ tt_varc_build_transform( &component, &matrix, &offset ); /* apply axis value overrides if present */ new_coords = NULL; new_coords_on_heap = FALSE; has_axis_override = FALSE; #ifdef TT_CONFIG_OPTION_GX_VAR_SUPPORT if ( ( ( component.num_axis_values > 0 && component.axis_values ) || ( component.flags & VARC_RESET_UNSPECIFIED_AXES ) ) && num_coords > 0 && parent_coords ) { /* allocate new_coords only */ /* (`parent_coords` replaces `saved_coords`) */ if ( num_coords <= VARC_STACK_COORD_COUNT ) new_coords = stack_new_coords; else { if ( FT_NEW_ARRAY( new_coords, num_coords ) ) goto Skip_Axis_Override; new_coords_on_heap = TRUE; } /* start with current coordinates or reset to default */ if ( component.flags & VARC_RESET_UNSPECIFIED_AXES ) { /* Reset unspecified axes to the font's current instance */ /* coordinates, matching HarfBuzz (font->coords), skrifa, and */ /* fontTools. Axes explicitly specified below are overridden */ /* afterwards. */ FT_UInt j; FT_UInt ncopy = context->num_font_coords < num_coords ? context->num_font_coords : num_coords; if ( context->font_coords ) FT_MEM_COPY( new_coords, context->font_coords, ncopy * sizeof ( FT_Fixed ) ); else ncopy = 0; for ( j = ncopy; j < num_coords; j++ ) new_coords[j] = 0; /* default for axes beyond `font_coords` */ } else { /* inherit from parent coords (handles both depth 1 and > 1) */ FT_MEM_COPY( new_coords, parent_coords, num_coords * sizeof ( FT_Fixed ) ); } /* read axis indices and apply values */ if ( component.flags & VARC_HAVE_AXES && varc->axis_indices_list ) { FT_UInt* axis_indices = NULL; FT_UInt j; /* allocate array for axis indices */ if ( component.num_axis_values <= VARC_STACK_INDICES_COUNT ) axis_indices = stack_axis_indices; else { if ( FT_NEW_ARRAY( axis_indices, component.num_axis_values ) ) { if ( new_coords_on_heap ) FT_FREE( new_coords ); new_coords = NULL; goto Skip_Axis_Override; } } /* read axis indices from TupleList */ error = tt_varc_get_axis_indices( varc, component.axis_indices_index, component.num_axis_values, axis_indices ); if ( error ) { FT_TRACE2(( " component glyph %u has invalid axis indices" " (error 0x%x)\n", component.gid, error )); if ( axis_indices != stack_axis_indices ) FT_FREE( axis_indices ); if ( new_coords_on_heap ) FT_FREE( new_coords ); new_coords = NULL; goto Skip_Axis_Override; } /* apply axis values using the indices */ for ( j = 0; j < component.num_axis_values; j++ ) { FT_UInt axis_idx = axis_indices[j]; FT_Fixed v = component.axis_values[j]; if ( axis_idx < num_coords ) { /* Round to the nearest F2DOT14 grid point (a multiple of 4 */ /* in 16.16) before the value feeds the leaf's gvar */ /* interpolation, matching HarfBuzz (roundf) and skrifa. */ if ( v >= 0 ) v = ( v + 2 ) & ~(FT_Fixed)3; else v = NEG_LONG( ( NEG_LONG( v ) + 2 ) & ~(FT_Fixed)3 ); new_coords[axis_idx] = v; FT_TRACE7(( " axis %u: %.5f\n", axis_idx, (double)v / 65536 )); } #ifdef FT_DEBUG_LEVEL_TRACE else FT_TRACE2(( " component glyph %u has out-of-range axis %u\n", component.gid, axis_idx )); #endif } if ( axis_indices != stack_axis_indices ) FT_FREE( axis_indices ); } /* Apply the new normalized coordinates. Use lightweight path */ /* that only swaps coords and invalidates the gvar tuple cache. */ tt_varc_set_normalized_coords( face, new_coords, num_coords ); has_axis_override = TRUE; } Skip_Axis_Override: #endif /* TT_CONFIG_OPTION_GX_VAR_SUPPORT */ /* Load component - allow recursive VARC loading. */ /* Compose transforms: we want (T1*T2)(leaf) not T1(T2(leaf)). */ /* save current parent transform */ saved_parent_matrix = context->parent_matrix; saved_parent_delta = context->parent_delta; saved_has_parent = context->has_parent_transform; /* compose with parent transform if present */ if ( context->has_parent_transform ) { /* Compose: final = parent * child. `FT_Matrix_Multiply(a, b)` */ /* computes b = a*b, so seed the result with the child matrix */ /* and left-multiply by the parent; this matches the */ /* composed_delta below (the translation of parent*child) and */ /* HarfBuzz/skrifa. */ composed_matrix = matrix; FT_Matrix_Multiply( &context->parent_matrix, &composed_matrix ); /* transform offset by parent matrix and add parent delta */ composed_delta.x = FT_MulFix( offset.x, context->parent_matrix.xx ) + FT_MulFix( offset.y, context->parent_matrix.xy ) + context->parent_delta.x; composed_delta.y = FT_MulFix( offset.x, context->parent_matrix.yx ) + FT_MulFix( offset.y, context->parent_matrix.yy ) + context->parent_delta.y; } else { /* no parent transform, use component transform directly */ composed_matrix = matrix; composed_delta = offset; } /* store composed transform in context for child VARC components */ context->parent_matrix = composed_matrix; context->parent_delta = composed_delta; context->has_parent_transform = TRUE; FT_TRACE7(( " matrix: [ %.5f %.5f; %.5f %.5f ]\n", (double)composed_matrix.xx / 65536, (double)composed_matrix.xy / 65536, (double)composed_matrix.yx / 65536, (double)composed_matrix.yy / 65536 )); FT_TRACE7(( " offset: (%.5f, %.5f)\n", (double)composed_delta.x / 64, (double)composed_delta.y / 64 )); /* set child's coords in context for recursive VARC processing */ #ifdef TT_CONFIG_OPTION_GX_VAR_SUPPORT if ( has_axis_override ) { context->current_coords = new_coords; context->num_current_coords = num_coords; } #endif /* Set composed VARC transform on face so `FT_Load_Glyph` applies */ /* it. The matrix is dimensionless (16.16) and works in any */ /* coordinate system. The delta must match the outline's coordinate */ /* space: 26.6 device pixels (scaled) or integer font units */ /* (NO_SCALE). */ { FT_Face_Internal internal = face->root.internal; internal->transform_matrix = composed_matrix; internal->transform_flags = 0; if ( ( composed_matrix.xy | composed_matrix.yx ) || composed_matrix.xx != 0x10000L || composed_matrix.yy != 0x10000L ) internal->transform_flags |= 1; if ( load_flags & FT_LOAD_NO_SCALE ) { /* NO_SCALE: outline in integer font units */ internal->transform_delta.x = ( composed_delta.x + 32 ) >> 6; internal->transform_delta.y = ( composed_delta.y + 32 ) >> 6; } else { FT_Size size = face->root.size; if ( size ) { FT_Fixed x_scale = size->metrics.x_scale; FT_Fixed y_scale = size->metrics.y_scale; /* Convert from 26.6 font units to 26.6 device pixels. */ /* 26.6 * 16.16 = 42.22; >> 22 gives 26.6. */ #ifdef FT_INT64 internal->transform_delta.x = (FT_Pos)( ( (FT_Int64)composed_delta.x * x_scale + 0x200000L ) >> 22 ); internal->transform_delta.y = (FT_Pos)( ( (FT_Int64)composed_delta.y * y_scale + 0x200000L ) >> 22 ); #else internal->transform_delta.x = FT_MulDiv( composed_delta.x, x_scale, 0x400000L ); internal->transform_delta.y = FT_MulDiv( composed_delta.y, y_scale, 0x400000L ); #endif } else { internal->transform_delta.x = ( composed_delta.x + 32 ) >> 6; internal->transform_delta.y = ( composed_delta.y + 32 ) >> 6; } } if ( internal->transform_delta.x | internal->transform_delta.y ) internal->transform_flags |= 2; } /* Load component. Strip IGNORE_TRANSFORM so our VARC transform */ /* is applied by `FT_Load_Glyph`'s post-processing. Add NO_HINTING */ /* to avoid auto-fitter reentrancy: the auto-fitter initializes */ /* per-face metrics lazily, and `FT_Set_Var_Blend_Coordinates` */ /* (called below to change axis values) frees that state, causing */ /* use-after-free if the auto-fitter was mid-initialization. */ { FT_Int32 component_load_flags = ( load_flags & ~(FT_Int32)FT_LOAD_IGNORE_TRANSFORM ) | FT_LOAD_NO_HINTING; error = FT_Load_Glyph( (FT_Face)face, component.gid, component_load_flags ); } #ifdef TT_CONFIG_OPTION_GX_VAR_SUPPORT /* restore parent coordinates */ if ( has_axis_override ) { tt_varc_set_normalized_coords( face, parent_coords, num_coords ); context->current_coords = parent_coords; context->num_current_coords = num_coords; if ( new_coords_on_heap ) FT_FREE( new_coords ); } #endif /* restore parent transform in context */ context->parent_matrix = saved_parent_matrix; context->parent_delta = saved_parent_delta; context->has_parent_transform = saved_has_parent; component_slot = face->root.glyph; if ( error ) { FT_TRACE2(( " component glyph %u failed (error 0x%x); skipped\n", component.gid, error )); tt_varc_free_component( face, &component ); continue; /* skip failed component, continue with others */ } /* `FT_Load_Glyph` already applied our composed transform. * * For base glyphs: the driver loaded and scaled, then * `FT_Load_Glyph` applied our matrix + delta. * * For VARC sub-components: the recursive handler processed * them and cleared the face transform before returning, so * `FT_Load_Glyph`'s post-processing was a no-op. */ if ( component_slot->outline.n_points > 0 ) { FT_Outline* src = &component_slot->outline; FT_Outline* cur = &gloader->current.outline; /* ensure capacity in gloader with geometric growth */ error = FT_GLYPHLOADER_CHECK_POINTS( gloader, src->n_points, src->n_contours ); if ( error ) { tt_varc_free_component( face, &component ); goto Cleanup; } FT_ARRAY_COPY( cur->points, src->points, src->n_points ); FT_ARRAY_COPY( cur->tags, src->tags, src->n_points ); FT_ARRAY_COPY( cur->contours, src->contours, src->n_contours ); cur->n_points = src->n_points; cur->n_contours = src->n_contours; FT_GlyphLoader_Add( gloader ); FT_TRACE5(( " appended %d points in %d contours\n", src->n_points, src->n_contours )); } tt_varc_free_component( face, &component ); } /* Hand the loader's accumulated outline to the slot. */ { FT_Outline* base = &gloader->base.outline; if ( base->n_points > 0 ) { slot->outline = *base; slot->format = FT_GLYPH_FORMAT_OUTLINE; } FT_TRACE4(( "VARC glyph %u: %d points in %d contours\n", glyph_index, base->n_points, base->n_contours )); } error = FT_Err_Ok; Cleanup: /* Free the temp glyph slot for this recursion level. */ /* `face->root.glyph` points to the temp slot we allocated above; */ /* `FT_Done_GlyphSlot` removes it and restores the previous head. */ if ( face->root.glyph != glyph_slot ) FT_Done_GlyphSlot( face->root.glyph ); /* pop from recursion stack */ tt_varc_context_pop( context ); /* clean up context if we own it */ if ( context_owner ) { #ifdef TT_CONFIG_OPTION_GX_VAR_SUPPORT /* Restore font's original coordinates in case we bailed out */ /* mid-component with overridden axis values still active. */ if ( context->font_coords ) tt_varc_set_normalized_coords( face, context->font_coords, context->num_font_coords ); #endif if ( context->font_coords ) FT_FREE( context->font_coords ); FT_FREE( context ); face->varc_context = NULL; } /* Manage face transform for `FT_Load_Glyph`'s post-processing. */ /* At top level: restore user's transform so it gets applied once. */ /* At recursive levels: clear to identity so the parent's */ /* `FT_Load_Glyph` doesn't apply twice. */ if ( context_owner ) { face->root.internal->transform_matrix = saved_transform_matrix; face->root.internal->transform_delta = saved_transform_delta; face->root.internal->transform_flags = saved_transform_flags; } else { face->root.internal->transform_matrix.xx = 0x10000L; face->root.internal->transform_matrix.xy = 0; face->root.internal->transform_matrix.yx = 0; face->root.internal->transform_matrix.yy = 0x10000L; face->root.internal->transform_delta.x = 0; face->root.internal->transform_delta.y = 0; face->root.internal->transform_flags = 0; } #ifdef FT_DEBUG_LEVEL_TRACE if ( error ) FT_TRACE2(( "tt_face_load_varc_glyph: glyph %u failed" " (error 0x%x)\n", glyph_index, error )); #endif return error; } #endif /* TT_CONFIG_OPTION_VARC */ /* END */