| /**************************************************************************** |
| * |
| * ttgpos.c |
| * |
| * Load the TrueType GPOS table. The only GPOS layout feature this |
| * currently supports is kerning, from x advances in the pair adjustment |
| * layout feature. |
| * |
| * Parts of the implementation were adapted from: |
| * https://github.com/nothings/stb/blob/master/stb_truetype.h |
| * |
| * GPOS spec reference available at: |
| * https://learn.microsoft.com/en-us/typography/opentype/spec/gpos |
| * |
| * Copyright (C) 2024 by |
| * David Saltzman |
| * |
| * 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 <freetype/internal/ftdebug.h> |
| #include <freetype/internal/ftstream.h> |
| #include <freetype/tttags.h> |
| #include "freetype/fttypes.h" |
| #include "freetype/internal/ftobjs.h" |
| #include "ttgpos.h" |
| |
| |
| #ifdef TT_CONFIG_OPTION_GPOS_KERNING |
| |
| /************************************************************************** |
| * |
| * 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 ttgpos |
| |
| |
| typedef enum coverage_table_format_type_ |
| { |
| COVERAGE_TABLE_FORMAT_LIST = 1, |
| COVERAGE_TABLE_FORMAT_RANGE = 2 |
| |
| } coverage_table_format_type; |
| |
| typedef enum class_def_table_format_type_ |
| { |
| CLASS_DEF_TABLE_FORMAT_ARRAY = 1, |
| CLASS_DEF_TABLE_FORMAT_RANGE_GROUPS = 2 |
| |
| } class_def_table_format_type; |
| |
| typedef enum gpos_lookup_type_ |
| { |
| GPOS_LOOKUP_TYPE_NONE = 0, |
| GPOS_LOOKUP_TYPE_SINGLE_ADJUSTMENT = 1, |
| GPOS_LOOKUP_TYPE_PAIR_ADJUSTMENT = 2, |
| GPOS_LOOKUP_TYPE_CURSIVE_ATTACHMENT = 3, |
| GPOS_LOOKUP_TYPE_MARK_TO_BASE_ATTACHMENT = 4, |
| GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE_ATTACHMENT = 5, |
| GPOS_LOOKUP_TYPE_MARK_TO_MARK_ATTACHMENT = 6, |
| GPOS_LOOKUP_TYPE_CONTEXT_POSITIONING = 7, |
| GPOS_LOOKUP_TYPE_CHAINED_CONTEXT_POSITIONING = 8, |
| GPOS_LOOKUP_TYPE_EXTENSION_POSITIONING = 9 |
| |
| } gpos_lookup_type; |
| |
| typedef enum gpos_pair_adjustment_format_ |
| { |
| GPOS_PAIR_ADJUSTMENT_FORMAT_GLYPH_PAIR = 1, |
| GPOS_PAIR_ADJUSTMENT_FORMAT_CLASS_PAIR = 2 |
| |
| } gpos_pair_adjustment_format; |
| |
| typedef enum gpos_value_format_bitmask_ |
| { |
| GPOS_VALUE_FORMAT_NONE = 0x0000, |
| GPOS_VALUE_FORMAT_X_PLACEMENT = 0x0001, |
| GPOS_VALUE_FORMAT_Y_PLACEMENT = 0x0002, |
| GPOS_VALUE_FORMAT_X_ADVANCE = 0x0004, |
| GPOS_VALUE_FORMAT_Y_ADVANCE = 0x0008, |
| GPOS_VALUE_FORMAT_X_PLACEMENT_DEVICE = 0x0010, |
| GPOS_VALUE_FORMAT_Y_PLACEMENT_DEVICE = 0x0020, |
| GPOS_VALUE_FORMAT_X_ADVANCE_DEVICE = 0x0040, |
| GPOS_VALUE_FORMAT_Y_ADVANCE_DEVICE = 0x0080 |
| |
| } gpos_value_format_bitmask; |
| |
| |
| typedef struct TT_GPOS_Subtable_Iterator_Context_ |
| { |
| /* Iteration state. */ |
| FT_Byte* current_lookup_table; |
| gpos_lookup_type current_lookup_type; |
| FT_UShort subtable_count; |
| FT_Byte* subtable_offsets; |
| FT_UInt subtable_idx; |
| |
| /* Element for the current iteration. */ |
| FT_Byte* subtable; |
| gpos_lookup_type subtable_type; |
| |
| } TT_GPOS_Subtable_Iterator_Context; |
| |
| |
| /* Initialize a subtable iterator for a given lookup list index. */ |
| static void |
| tt_gpos_subtable_iterator_init( |
| TT_GPOS_Subtable_Iterator_Context* context, |
| FT_Byte* gpos_table, |
| FT_ULong lookup_list_idx ) |
| { |
| FT_Byte* lookup_list = gpos_table + FT_PEEK_USHORT( gpos_table + 8 ); |
| FT_UInt16 lookup_count = FT_PEEK_USHORT( lookup_list ); |
| |
| |
| if ( lookup_list_idx < lookup_count ) |
| { |
| context->current_lookup_table = |
| lookup_list + FT_PEEK_USHORT( lookup_list + 2 + 2 * lookup_list_idx ); |
| context->current_lookup_type = |
| (gpos_lookup_type)FT_PEEK_USHORT( context->current_lookup_table ); |
| context->subtable_count = |
| FT_PEEK_USHORT( context->current_lookup_table + 4 ); |
| context->subtable_offsets = context->current_lookup_table + 6; |
| } |
| else |
| { |
| context->current_lookup_table = NULL; |
| context->current_lookup_type = GPOS_LOOKUP_TYPE_NONE; |
| context->subtable_count = 0; |
| context->subtable_offsets = NULL; |
| } |
| |
| context->subtable_idx = 0; |
| context->subtable = NULL; |
| context->subtable_type = GPOS_LOOKUP_TYPE_NONE; |
| } |
| |
| |
| /* Get the next subtable. Return whether there was a next one. */ |
| static FT_Bool |
| tt_gpos_subtable_iterator_next( |
| TT_GPOS_Subtable_Iterator_Context* context ) |
| { |
| if ( context->subtable_idx < context->subtable_count ) |
| { |
| FT_UShort subtable_offset = |
| FT_PEEK_USHORT( context->subtable_offsets + |
| 2 * context->subtable_idx ); |
| |
| |
| context->subtable = context->current_lookup_table + subtable_offset; |
| |
| if ( context->current_lookup_type == |
| GPOS_LOOKUP_TYPE_EXTENSION_POSITIONING ) |
| { |
| /* Update type and subtable based on extension positioning header. */ |
| context->subtable_type = |
| (gpos_lookup_type)FT_PEEK_USHORT( context->subtable + 2 ); |
| context->subtable += FT_PEEK_ULONG( context->subtable + 4 ); |
| } |
| else |
| context->subtable_type = context->current_lookup_type; |
| |
| context->subtable_idx++; |
| return TRUE; |
| } |
| |
| return FALSE; |
| } |
| |
| |
| static FT_Int |
| tt_gpos_get_coverage_index( FT_Byte *coverage_table, |
| FT_UInt glyph ) |
| { |
| coverage_table_format_type coverage_format = |
| (coverage_table_format_type)FT_PEEK_USHORT( coverage_table ); |
| |
| |
| switch ( coverage_format ) |
| { |
| case COVERAGE_TABLE_FORMAT_LIST: |
| { |
| FT_UShort glyph_count = FT_PEEK_USHORT( coverage_table + 2 ); |
| |
| FT_Int l = 0; |
| FT_Int r = glyph_count - 1; |
| FT_Int m; |
| |
| FT_Int straw; |
| FT_Int needle = (FT_Int)glyph; |
| |
| |
| /* Binary search. */ |
| while ( l <= r ) |
| { |
| FT_Byte *glyph_array = coverage_table + 4; |
| FT_UShort glyph_id; |
| |
| |
| m = ( l + r ) >> 1; |
| glyph_id = FT_PEEK_USHORT( glyph_array + 2 * m ); |
| straw = glyph_id; |
| |
| if ( needle < straw ) |
| r = m - 1; |
| else if ( needle > straw ) |
| l = m + 1; |
| else |
| return m; |
| } |
| break; |
| } |
| |
| case COVERAGE_TABLE_FORMAT_RANGE: |
| { |
| FT_UShort range_count = FT_PEEK_USHORT( coverage_table + 2 ); |
| FT_Byte *range_array = coverage_table + 4; |
| |
| FT_Int l = 0; |
| FT_Int r = range_count - 1; |
| FT_Int m; |
| |
| FT_Int straw_start; |
| FT_Int straw_end; |
| FT_Int needle = (FT_Int)glyph; |
| |
| |
| /* Binary search. */ |
| while ( l <= r ) |
| { |
| FT_Byte *range_record; |
| |
| |
| m = ( l + r ) >> 1; |
| range_record = range_array + 6 * m; |
| straw_start = FT_PEEK_USHORT( range_record ); |
| straw_end = FT_PEEK_USHORT( range_record + 2 ); |
| |
| if ( needle < straw_start ) |
| r = m - 1; |
| else if ( needle > straw_end ) |
| l = m + 1; |
| else |
| { |
| FT_UShort start_coverage_index = |
| FT_PEEK_USHORT( range_record + 4 ); |
| |
| |
| return (FT_Int)start_coverage_index + (FT_Int)glyph - straw_start; |
| } |
| } |
| break; |
| } |
| } |
| |
| return -1; |
| } |
| |
| |
| static FT_Int |
| tt_gpos_get_glyph_class( FT_Byte *class_def_table, |
| FT_UInt glyph ) |
| { |
| class_def_table_format_type class_def_format = |
| (class_def_table_format_type)FT_PEEK_USHORT( class_def_table ); |
| |
| |
| switch ( class_def_format ) |
| { |
| case CLASS_DEF_TABLE_FORMAT_ARRAY: |
| { |
| FT_UInt start_glyph_id = FT_PEEK_USHORT( class_def_table + 2 ); |
| FT_UInt glyph_count = FT_PEEK_USHORT( class_def_table + 4 ); |
| FT_Byte *class_value_array = class_def_table + 6; |
| |
| |
| if ( glyph >= start_glyph_id && |
| glyph < start_glyph_id + glyph_count ) |
| return (FT_Int)FT_PEEK_USHORT( class_value_array + |
| 2 * ( glyph - start_glyph_id ) ); |
| break; |
| } |
| |
| case CLASS_DEF_TABLE_FORMAT_RANGE_GROUPS: |
| { |
| FT_UShort class_range_count = FT_PEEK_USHORT( class_def_table + 2 ); |
| FT_Byte *class_range_records = class_def_table + 4; |
| |
| FT_Int l = 0; |
| FT_Int r = class_range_count - 1; |
| FT_Int m; |
| |
| FT_Int straw_start; |
| FT_Int straw_end; |
| FT_Int needle = (FT_Int)glyph; |
| |
| |
| while ( l <= r ) |
| { |
| FT_Byte *class_range_record; |
| |
| |
| m = ( l + r ) >> 1; |
| class_range_record = class_range_records + 6 * m; |
| straw_start = FT_PEEK_USHORT( class_range_record ); |
| straw_end = FT_PEEK_USHORT( class_range_record + 2 ); |
| |
| if ( needle < straw_start ) |
| r = m - 1; |
| else if ( needle > straw_end ) |
| l = m + 1; |
| else |
| return (FT_Int)FT_PEEK_USHORT( class_range_record + 4 ); |
| } |
| break; |
| } |
| } |
| |
| /* "All glyphs not assigned to a class fall into class 0." */ |
| /* (OpenType spec) */ |
| return 0; |
| } |
| |
| |
| FT_LOCAL_DEF( FT_Error ) |
| tt_face_load_gpos( TT_Face face, |
| FT_Stream stream ) |
| { |
| FT_Error error; |
| FT_ULong table_size; |
| |
| |
| /* The GPOS table is optional; exit silently if it is missing. */ |
| error = face->goto_table( face, TTAG_GPOS, stream, &table_size ); |
| if ( error ) |
| goto Exit; |
| |
| if ( table_size < 4 ) /* the case of a malformed table */ |
| { |
| FT_ERROR(( "tt_face_load_gpos:" |
| " GPOS table is too small - ignored\n" )); |
| error = FT_THROW( Table_Missing ); |
| goto Exit; |
| } |
| |
| if ( FT_FRAME_EXTRACT( table_size, face->gpos_table ) ) |
| { |
| FT_ERROR(( "tt_face_load_gpos:" |
| " could not extract GPOS table\n" )); |
| goto Exit; |
| } |
| |
| face->gpos_kerning_available = FALSE; |
| |
| if ( face->gpos_table ) |
| { |
| FT_Byte* feature_list = face->gpos_table + |
| FT_PEEK_USHORT( face->gpos_table + 6 ); |
| FT_UInt16 feature_count = FT_PEEK_USHORT( feature_list ); |
| FT_Byte* feature_records = feature_list + 2; |
| |
| FT_UInt idx; |
| |
| |
| for ( idx = 0; idx < feature_count; idx++, feature_records += 6 ) |
| { |
| FT_ULong feature_tag = FT_PEEK_ULONG( feature_records ); |
| |
| |
| if ( feature_tag == TTAG_kern ) |
| { |
| face->gpos_kerning_available = TRUE; |
| break; |
| } |
| } |
| } |
| |
| Exit: |
| return error; |
| } |
| |
| |
| FT_LOCAL_DEF( void ) |
| tt_face_done_gpos( TT_Face face ) |
| { |
| FT_Stream stream = face->root.stream; |
| |
| |
| FT_FRAME_RELEASE( face->gpos_table ); |
| } |
| |
| |
| FT_LOCAL_DEF( FT_Int ) |
| tt_face_get_gpos_kerning( TT_Face face, |
| FT_UInt left_glyph, |
| FT_UInt right_glyph ) |
| { |
| FT_Byte* feature_list; |
| FT_UInt16 feature_count; |
| FT_Byte* feature_records; |
| FT_UInt feature_idx; |
| |
| |
| if ( !face->gpos_kerning_available ) |
| return 0; |
| |
| feature_list = face->gpos_table + |
| FT_PEEK_USHORT( face->gpos_table + 6 ); |
| feature_count = FT_PEEK_USHORT( feature_list ); |
| feature_records = feature_list + 2; |
| |
| for ( feature_idx = 0; |
| feature_idx < feature_count; |
| feature_idx++, feature_records += 6 ) |
| { |
| FT_ULong feature_tag = FT_PEEK_ULONG( feature_records ); |
| FT_Byte* feature_table; |
| FT_UInt16 lookup_idx_count; |
| FT_UInt16 lookup_idx; |
| |
| |
| if ( feature_tag != TTAG_kern ) |
| continue; |
| |
| feature_table = feature_list + FT_PEEK_USHORT( feature_records + 4 ); |
| lookup_idx_count = FT_PEEK_USHORT( feature_table + 2 ); |
| |
| for ( lookup_idx = 0; lookup_idx < lookup_idx_count; lookup_idx++ ) |
| { |
| FT_UInt16 lookup_list_idx = |
| FT_PEEK_USHORT( feature_table + 4 + 2 * lookup_idx ); |
| TT_GPOS_Subtable_Iterator_Context subtable_iter; |
| |
| |
| tt_gpos_subtable_iterator_init( &subtable_iter, |
| face->gpos_table, |
| lookup_list_idx ); |
| |
| while ( tt_gpos_subtable_iterator_next( &subtable_iter ) ) |
| { |
| FT_Byte* subtable; |
| |
| gpos_value_format_bitmask value_format_1; |
| gpos_value_format_bitmask value_format_2; |
| gpos_pair_adjustment_format format; |
| |
| FT_UShort coverage_offset; |
| FT_Int coverage_index; |
| |
| |
| if ( subtable_iter.subtable_type != |
| GPOS_LOOKUP_TYPE_PAIR_ADJUSTMENT ) |
| continue; |
| |
| subtable = subtable_iter.subtable; |
| |
| value_format_1 = |
| (gpos_value_format_bitmask)FT_PEEK_USHORT( subtable + 4 ); |
| value_format_2 = |
| (gpos_value_format_bitmask)FT_PEEK_USHORT( subtable + 6 ); |
| |
| if ( !( value_format_1 == GPOS_VALUE_FORMAT_X_ADVANCE && |
| value_format_2 == GPOS_VALUE_FORMAT_NONE ) ) |
| continue; |
| |
| format = (gpos_pair_adjustment_format)FT_PEEK_USHORT( subtable ); |
| |
| coverage_offset = FT_PEEK_USHORT( subtable + 2 ); |
| coverage_index = |
| tt_gpos_get_coverage_index( subtable + coverage_offset, |
| left_glyph ); |
| |
| if ( coverage_index == -1 ) |
| continue; |
| |
| switch ( format ) |
| { |
| case GPOS_PAIR_ADJUSTMENT_FORMAT_GLYPH_PAIR: |
| { |
| FT_Int l, r, m; |
| FT_Int straw, needle; |
| |
| FT_Int value_record_pair_size_in_bytes = 2; |
| |
| FT_UShort pair_set_count = FT_PEEK_USHORT( subtable + 8 ); |
| FT_UShort pair_pos_offset; |
| |
| FT_Byte* pair_value_table; |
| FT_UShort pair_value_count; |
| FT_Byte* pair_value_array; |
| |
| |
| if ( coverage_index >= pair_set_count ) |
| return 0; |
| |
| pair_pos_offset = |
| FT_PEEK_USHORT( subtable + 10 + 2 * coverage_index ); |
| |
| pair_value_table = subtable + pair_pos_offset; |
| pair_value_count = FT_PEEK_USHORT( pair_value_table ); |
| pair_value_array = pair_value_table + 2; |
| |
| needle = (FT_Int)right_glyph; |
| r = pair_value_count - 1; |
| l = 0; |
| |
| /* Binary search. */ |
| while ( l <= r ) |
| { |
| FT_UShort second_glyph; |
| FT_Byte* pair_value; |
| |
| |
| m = ( l + r ) >> 1; |
| pair_value = pair_value_array + |
| ( 2 + value_record_pair_size_in_bytes ) * m; |
| second_glyph = FT_PEEK_USHORT( pair_value ); |
| straw = second_glyph; |
| |
| if ( needle < straw ) |
| r = m - 1; |
| else if ( needle > straw ) |
| l = m + 1; |
| else |
| { |
| FT_Short x_advance = FT_PEEK_SHORT( pair_value + 2 ); |
| |
| |
| return x_advance; |
| } |
| } |
| break; |
| } |
| |
| case GPOS_PAIR_ADJUSTMENT_FORMAT_CLASS_PAIR: |
| { |
| FT_UShort class_def1_offset = FT_PEEK_USHORT( subtable + 8 ); |
| FT_UShort class_def2_offset = FT_PEEK_USHORT( subtable + 10 ); |
| |
| FT_Int left_glyph_class = |
| tt_gpos_get_glyph_class( subtable + class_def1_offset, |
| left_glyph ); |
| FT_Int right_glyph_class = |
| tt_gpos_get_glyph_class( subtable + class_def2_offset, |
| right_glyph ); |
| |
| FT_UShort class1_count = FT_PEEK_USHORT( subtable + 12 ); |
| FT_UShort class2_count = FT_PEEK_USHORT( subtable + 14 ); |
| |
| FT_Byte *class1_records, *class2_records; |
| FT_Short x_advance; |
| |
| |
| if ( left_glyph_class < 0 || |
| left_glyph_class >= class1_count ) |
| return 0; /* malformed */ |
| if ( right_glyph_class < 0 || |
| right_glyph_class >= class2_count ) |
| return 0; /* malformed */ |
| |
| if ( right_glyph_class == 0 ) |
| continue; /* right glyph not found in this table */ |
| |
| class1_records = subtable + 16; |
| class2_records = |
| class1_records + 2 * ( left_glyph_class * class2_count ); |
| |
| x_advance = |
| FT_PEEK_SHORT( class2_records + 2 * right_glyph_class ); |
| |
| return x_advance; |
| } |
| } |
| } |
| } |
| } |
| |
| return 0; |
| } |
| |
| #else /* !TT_CONFIG_OPTION_GPOS_KERNING */ |
| |
| /* ANSI C doesn't like empty source files */ |
| typedef int tt_gpos_dummy_; |
| |
| #endif /* !TT_CONFIG_OPTION_GPOS_KERNING */ |
| |
| |
| /* END */ |