[sfnt] Rewrite GPOS kerning support. (2/2)
The previous code had a fundamental flaw: it didn't validate the necessary
parts of the 'GPOS' table before accessing it, causing crashes with
malformed data (since `TT_CONFIG_OPTION_GPOS_KERNING` is off by default,
standard fuzzers don't catch these problems). Additionally, it did a lot of
parsing while accessing kerning data, making it rather slow.
The new implementation fixes this. After validation, offsets to the 'GPOS'
lookup subtables used in the 'kern' feature that correspond to 'simple'
kerning (i.e., similar to 'kern' table kerning) are stored in `TT_Face`;
this greatly simplifies and accelerates access to the kerning data.
Testing with font `SF-Pro.ttf` version '1.00', the validation time for the
'GPOS' table increases the start-up time of `FT_New_Face` by less than 1%,
while calls to `FT_Get_Kerning` become about 3.5 times faster.
* include/freetype/internal (gpos_kerning_available): Replace with...
(gpos_lookups_kerning, num_gpos_lookups_kerning): ... these new fields.
Update callers.
* src/ttgpos.c [TT_CONFIG_OPTION_GPOS_KERNING]: A new implementation.
diff --git a/devel/ftoption.h b/devel/ftoption.h
index 9b22a96..10ccf56 100644
--- a/devel/ftoption.h
+++ b/devel/ftoption.h
@@ -785,10 +785,10 @@
/**************************************************************************
*
* Option `TT_CONFIG_OPTION_GPOS_KERNING` enables a basic GPOS kerning
- * implementation (for TrueType fonts only). With this defined, FreeType
- * is able to get kerning pair data from the GPOS 'kern' feature as well as
- * legacy 'kern' tables; without this defined, FreeType will only be able
- * to use legacy 'kern' tables.
+ * implementation (for TrueType and OpenType fonts only). With this
+ * defined, FreeType is able to get kerning pair data from the GPOS 'kern'
+ * feature as well as legacy 'kern' tables; without this defined, FreeType
+ * will only be able to use legacy 'kern' tables.
*
* Note that FreeType does not support more advanced GPOS layout features;
* even the 'kern' feature implemented here doesn't handle more
diff --git a/docs/CHANGES b/docs/CHANGES
index e5fe330..b735643 100644
--- a/docs/CHANGES
+++ b/docs/CHANGES
@@ -43,6 +43,13 @@
outlines, not to ban them completely.
+ II. IMPORTANT BUG FIXES
+
+ - Users of the `TT_CONFIG_OPTION_GPOS_KERNING` configuration option
+ should update; the 'GPOS' table wasn't correctly validated before
+ access, which could lead to crashes with malformed font files.
+
+
III. MISCELLANEOUS
- `FT_Set_Var_Design_Coordinates` and `FT_Set_MM_Blend_Coordinates`
@@ -66,6 +73,10 @@
- The BDF driver now loads fonts 75% faster.
+ - 'GPOS' kern table handling (if the `TT_CONFIG_OPTION_GPOS_KERNING`
+ configuration option is active) is now about 3.5 times faster than
+ before.
+
======================================================================
diff --git a/include/freetype/config/ftoption.h b/include/freetype/config/ftoption.h
index 2307710..da606e4 100644
--- a/include/freetype/config/ftoption.h
+++ b/include/freetype/config/ftoption.h
@@ -785,10 +785,10 @@
/**************************************************************************
*
* Option `TT_CONFIG_OPTION_GPOS_KERNING` enables a basic GPOS kerning
- * implementation (for TrueType fonts only). With this defined, FreeType
- * is able to get kerning pair data from the GPOS 'kern' feature as well as
- * legacy 'kern' tables; without this defined, FreeType will only be able
- * to use legacy 'kern' tables.
+ * implementation (for TrueType and OpenType fonts only). With this
+ * defined, FreeType is able to get kerning pair data from the GPOS 'kern'
+ * feature as well as legacy 'kern' tables; without this defined, FreeType
+ * will only be able to use legacy 'kern' tables.
*
* Note that FreeType does not support more advanced GPOS layout features;
* even the 'kern' feature implemented here doesn't handle more
diff --git a/include/freetype/freetype.h b/include/freetype/freetype.h
index 0bd595f..c52fb83 100644
--- a/include/freetype/freetype.h
+++ b/include/freetype/freetype.h
@@ -3997,13 +3997,13 @@
* out of the scope of this API function -- they can be implemented
* through format-specific interfaces.
*
- * Note that, for TrueType fonts only, this can extract data from both
- * the 'kern' table and the basic, pair-wise kerning feature from the
- * GPOS table (with `TT_CONFIG_OPTION_GPOS_KERNING` enabled), though
- * FreeType does not support the more advanced GPOS layout features; use
- * a library like HarfBuzz for those instead. If a font has both a
- * 'kern' table and kern features of a GPOS table, the 'kern' table will
- * be used.
+ * Note that, for TrueType and OpenType fonts only, this can extract data
+ * from both the 'kern' table and the basic, pair-wise kerning feature
+ * from the GPOS table (with `TT_CONFIG_OPTION_GPOS_KERNING` enabled),
+ * though FreeType does not support the more advanced GPOS layout
+ * features; use a library like HarfBuzz for those instead. If a font
+ * has both a 'kern' table and kern features of a GPOS table, the 'kern'
+ * table will be used.
*
* Also note for right-to-left scripts, the functionality may differ for
* fonts with GPOS tables vs. 'kern' tables. For GPOS, right-to-left
diff --git a/include/freetype/internal/tttypes.h b/include/freetype/internal/tttypes.h
index 245954f..dba8ead 100644
--- a/include/freetype/internal/tttypes.h
+++ b/include/freetype/internal/tttypes.h
@@ -1574,11 +1574,6 @@
FT_UInt32 kern_avail_bits;
FT_UInt32 kern_order_bits;
-#ifdef TT_CONFIG_OPTION_GPOS_KERNING
- FT_Byte* gpos_table;
- FT_Bool gpos_kerning_available;
-#endif
-
#ifdef TT_CONFIG_OPTION_BDF
TT_BDFRec bdf;
#endif /* TT_CONFIG_OPTION_BDF */
@@ -1600,6 +1595,15 @@
/* since 2.12 */
void* svg;
+#ifdef TT_CONFIG_OPTION_GPOS_KERNING
+ /* since 2.13.3 */
+ FT_Byte* gpos_table;
+ /* since 2.14 */
+ /* This is actually an array of GPOS lookup subtables. */
+ FT_UInt32* gpos_lookups_kerning;
+ FT_UInt num_gpos_lookups_kerning;
+#endif
+
} TT_FaceRec;
diff --git a/src/cff/cffdrivr.c b/src/cff/cffdrivr.c
index 6f5afea..f432c9e 100644
--- a/src/cff/cffdrivr.c
+++ b/src/cff/cffdrivr.c
@@ -129,7 +129,7 @@
left_glyph,
right_glyph );
#ifdef TT_CONFIG_OPTION_GPOS_KERNING
- else if ( cffface->gpos_kerning_available )
+ else if ( cffface->num_gpos_lookups_kerning )
kerning->x = sfnt->get_gpos_kerning( cffface,
left_glyph,
right_glyph );
diff --git a/src/sfnt/sfobjs.c b/src/sfnt/sfobjs.c
index 29d0878..75411f7 100644
--- a/src/sfnt/sfobjs.c
+++ b/src/sfnt/sfobjs.c
@@ -1129,7 +1129,7 @@
/* kerning available ? */
if ( face->kern_avail_bits
#ifdef TT_CONFIG_OPTION_GPOS_KERNING
- || face->gpos_kerning_available
+ || face->num_gpos_lookups_kerning
#endif
)
flags |= FT_FACE_FLAG_KERNING;
diff --git a/src/sfnt/ttgpos.c b/src/sfnt/ttgpos.c
index f02a015..4ff5493 100644
--- a/src/sfnt/ttgpos.c
+++ b/src/sfnt/ttgpos.c
@@ -2,36 +2,33 @@
*
* 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.
+ * Routines to parse and access the 'GPOS' table for simple kerning (body).
*
- * 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/typography/opentype/spec/gpos
- *
- * Copyright (C) 2024 by
- * David Saltzman
+ * Copyright (C) 2025 by
+ * David Turner, Robert Wilhelm, and Werner Lemberg.
*
* This file is part of the FreeType project, and may only be used,
* modified, and distributed under the terms of the FreeType project
* license, LICENSE.TXT. By continuing to use, modify, or distribute
* this file you indicate that you have read the license and
* understand and accept it fully.
+ *
*/
+
+#include <freetype/freetype.h>
+#include <freetype/tttables.h>
+#include <freetype/tttags.h>
+
#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
@@ -42,26 +39,936 @@
#define FT_COMPONENT ttgpos
+ /*********************************/
+ /******** ********/
+ /******** GPOS validation ********/
+ /******** ********/
+ /*********************************/
+
+ static FT_Bool
+ tt_face_validate_coverage( FT_Byte* table,
+ FT_Byte* table_limit,
+ FT_UInt max_num_coverage_indices )
+ {
+ FT_UInt format;
+
+ FT_Byte* p = table;
+ FT_Byte* limit;
+
+ FT_Long last_id = -1;
+
+
+ if ( table_limit < p + 4 )
+ return FALSE;
+
+ format = FT_NEXT_USHORT( p );
+ if ( format == 1 )
+ {
+ FT_UInt glyphCount = FT_NEXT_USHORT( p );
+
+
+ if ( glyphCount > max_num_coverage_indices )
+ return FALSE;
+
+ limit = p + glyphCount * 2;
+ if ( table_limit < limit )
+ return FALSE;
+
+ while ( p < limit )
+ {
+ FT_UInt id = FT_NEXT_USHORT( p );
+
+
+ if ( last_id >= id )
+ return FALSE;
+ last_id = id;
+ }
+ }
+ else if ( format == 2 )
+ {
+ FT_UInt rangeCount = FT_NEXT_USHORT( p );
+
+
+ limit = p + rangeCount * 6;
+ if ( table_limit < limit )
+ return FALSE;
+
+ while ( p < limit )
+ {
+ FT_UInt startGlyphID = FT_NEXT_USHORT( p );
+ FT_UInt endGlyphID = FT_NEXT_USHORT( p );
+ FT_UInt startCoverageIndex = FT_NEXT_USHORT( p );
+
+
+ if ( startGlyphID > endGlyphID )
+ return FALSE;
+
+ if ( last_id >= startGlyphID )
+ return FALSE;
+ last_id = endGlyphID;
+
+ /* XXX: Is this modulo 65536 arithmetic? */
+ if ( startCoverageIndex + endGlyphID - startGlyphID >=
+ max_num_coverage_indices )
+ return FALSE;
+ }
+ }
+ else
+ return FALSE;
+
+ return TRUE;
+ }
+
+
+ static FT_Bool
+ tt_face_validate_class_def( FT_Byte* table,
+ FT_Byte* table_limit,
+ FT_UInt num_classes )
+ {
+ FT_UInt format;
+
+ FT_Byte* p = table;
+ FT_Byte* limit;
+
+ FT_UInt max_class_value = 0;
+
+
+ if ( table_limit < p + 2 )
+ return FALSE;
+
+ format = FT_NEXT_USHORT( p );
+ if ( format == 1 )
+ {
+ FT_UInt glyphCount;
+
+
+ if ( table_limit < p + 4 )
+ return FALSE;
+
+ p += 2; /* Skip `startGlyphID`. */
+
+ glyphCount = FT_NEXT_USHORT( p );
+ limit = p + glyphCount * 2;
+ if ( table_limit < limit )
+ return FALSE;
+
+ while ( p < limit )
+ {
+ FT_UInt class_value = FT_NEXT_USHORT( p );
+
+
+ if ( class_value > max_class_value )
+ max_class_value = class_value;
+ }
+ }
+ else if ( format == 2 )
+ {
+ FT_UInt classRangeCount;
+ FT_Long last_id = -1;
+
+
+ if ( table_limit < p + 2 )
+ return FALSE;
+
+ classRangeCount = FT_NEXT_USHORT( p );
+ limit = p + classRangeCount * 6;
+ if ( table_limit < limit )
+ return FALSE;
+
+ while ( p < limit )
+ {
+ FT_UInt startGlyphID = FT_NEXT_USHORT( p );
+ FT_UInt endGlyphID = FT_NEXT_USHORT( p );
+ FT_UInt class_value = FT_NEXT_USHORT( p );
+
+
+ if ( startGlyphID > endGlyphID )
+ return FALSE;
+
+ if ( last_id >= startGlyphID )
+ return FALSE;
+ last_id = endGlyphID;
+
+ if ( class_value > max_class_value )
+ max_class_value = class_value;
+ }
+ }
+ else
+ return FALSE;
+
+ if ( max_class_value + 1 != num_classes )
+ return FALSE;
+
+ return TRUE;
+ }
+
+
+ static FT_Bool
+ tt_face_validate_feature( FT_Byte* table,
+ FT_Byte* table_limit,
+ FT_UInt use_lookup_table_size,
+ FT_Byte* use_lookup_table )
+ {
+ FT_UInt lookupIndexCount;
+
+ FT_Byte* p = table;
+ FT_Byte* limit;
+
+
+ if ( table_limit < p + 4 )
+ return FALSE;
+
+ p += 2; /* Skip `featureParamsOffset`. */
+
+ lookupIndexCount = FT_NEXT_USHORT( p );
+ limit = p + lookupIndexCount * 2;
+ if ( table_limit < limit )
+ return FALSE;
+
+ while ( p < limit )
+ {
+ FT_UInt lookup_index = FT_NEXT_USHORT( p );
+
+
+ if ( lookup_index >= use_lookup_table_size )
+ return FALSE;
+
+ use_lookup_table[lookup_index] = TRUE;
+ }
+
+ return TRUE;
+ }
+
+
+ static FT_Bool
+ tt_face_validate_feature_table( FT_Byte* table,
+ FT_Byte* table_limit,
+ FT_UInt use_lookup_table_size,
+ FT_Byte* use_lookup_table )
+ {
+ FT_UInt featureCount;
+
+ FT_Byte* p = table;
+ FT_Byte* limit;
+
+
+ if ( table_limit < p + 2 )
+ return FALSE;
+
+ featureCount = FT_NEXT_USHORT( p );
+ limit = p + featureCount * 6;
+ if ( table_limit < limit )
+ return FALSE;
+
+ /* We completely ignore GPOS script information */
+ /* and collect lookup tables of all 'kern' features. */
+ while ( p < limit )
+ {
+ FT_ULong featureTag = FT_NEXT_ULONG( p );
+ FT_UInt featureOffset = FT_NEXT_USHORT( p );
+
+
+ if ( featureTag == TTAG_kern )
+ {
+ if ( !tt_face_validate_feature( table + featureOffset,
+ table_limit,
+ use_lookup_table_size,
+ use_lookup_table ) )
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+ }
+
+
+ static FT_Bool
+ tt_face_validate_pair_set( FT_Byte* table,
+ FT_Byte* table_limit )
+ {
+ FT_UInt pairValueCount;
+
+ FT_Byte* p = table;
+ FT_Byte* limit;
+
+ FT_Long last_id = -1;
+
+
+ if ( table_limit < p + 2 )
+ return FALSE;
+
+ /* For our purposes, the first value record only contains X advances */
+ /* while the second one is empty; a `PairValue` record has thus a */
+ /* size of four bytes. */
+ pairValueCount = FT_NEXT_USHORT( p );
+ limit = p + pairValueCount * 4;
+ if ( table_limit < limit )
+ return FALSE;
+
+ /* We validate the order of `secondGlyph` so that binary search works. */
+ while ( p < limit )
+ {
+ FT_UInt id = FT_NEXT_USHORT( p );
+
+
+ if ( last_id >= id )
+ return FALSE;
+
+ last_id = id;
+
+ p += 2; /* Skip `valueRecord1`. */
+ }
+
+ return TRUE;
+ }
+
+
+ static FT_Bool
+ tt_face_validate_pair_pos1( FT_Byte* table,
+ FT_Byte* table_limit,
+ FT_Bool* is_fitting )
+ {
+ FT_Byte* coverage;
+ FT_UInt valueFormat1;
+ FT_UInt valueFormat2;
+
+ /* Subtable format is already checked. */
+ FT_Byte* p = table + 2;
+ FT_Byte* limit;
+
+
+ /* The six bytes for the coverage table offset */
+ /* and the value formats are already checked. */
+ coverage = table + FT_NEXT_USHORT( p );
+
+ /* For the limited purpose of accessing the simplest type of kerning */
+ /* (similar to what FreeType's 'kern' table handling provides) we */
+ /* only consider tables that contains X advance values for the first */
+ /* glyph and no data for the second glyph. */
+ valueFormat1 = FT_NEXT_USHORT( p );
+ valueFormat2 = FT_NEXT_USHORT( p );
+ if ( valueFormat1 == 0x4 && valueFormat2 == 0 )
+ {
+ FT_UInt pairSetCount;
+
+
+ if ( table_limit < p + 2 )
+ return FALSE;
+
+ pairSetCount = FT_NEXT_USHORT( p );
+ limit = p + pairSetCount * 2;
+ if ( table_limit < limit )
+ return FALSE;
+
+ if ( !tt_face_validate_coverage( coverage,
+ table_limit,
+ pairSetCount ) )
+ return FALSE;
+
+ while ( p < limit )
+ {
+ FT_Byte* pair_set = table + FT_NEXT_USHORT( p );
+
+
+ if ( !tt_face_validate_pair_set( pair_set, table_limit ) )
+ return FALSE;
+ }
+
+ *is_fitting = TRUE;
+ }
+
+ return TRUE;
+ }
+
+
+ static FT_Bool
+ tt_face_validate_pair_pos2( FT_Byte* table,
+ FT_Byte* table_limit,
+ FT_Bool* is_fitting )
+ {
+ FT_Byte* coverage;
+ FT_UInt valueFormat1;
+ FT_UInt valueFormat2;
+
+ /* Subtable format is already checked. */
+ FT_Byte* p = table + 2;
+ FT_Byte* limit;
+
+
+ /* The six bytes for the coverage table offset */
+ /* and the value formats are already checked. */
+ coverage = table + FT_NEXT_USHORT( p );
+
+ valueFormat1 = FT_NEXT_USHORT( p );
+ valueFormat2 = FT_NEXT_USHORT( p );
+ if ( valueFormat1 == 0x4 && valueFormat2 == 0 )
+ {
+ FT_Byte* class_def1;
+ FT_Byte* class_def2;
+ FT_UInt class1Count;
+ FT_UInt class2Count;
+
+
+ /* The number of coverage indices is not relevant here. */
+ if ( !tt_face_validate_coverage( coverage, table_limit, FT_UINT_MAX ) )
+ return FALSE;
+
+ if ( table_limit < p + 8 )
+ return FALSE;
+
+ class_def1 = table + FT_NEXT_USHORT( p );
+ class_def2 = table + FT_NEXT_USHORT( p );
+ class1Count = FT_NEXT_USHORT( p );
+ class2Count = FT_NEXT_USHORT( p );
+
+ if ( !tt_face_validate_class_def( class_def1,
+ table_limit,
+ class1Count ) )
+ return FALSE;
+ if ( !tt_face_validate_class_def( class_def2,
+ table_limit,
+ class2Count ) )
+ return FALSE;
+
+ /* For our purposes, the first value record only contains */
+ /* X advances while the second one is empty. */
+ limit = p + class1Count * class2Count * 2;
+ if ( table_limit < limit )
+ return FALSE;
+
+ *is_fitting = TRUE;
+ }
+
+ return TRUE;
+ }
+
+
+ /* The return value is the number of fitting subtables. */
+ static FT_UInt
+ tt_face_validate_lookup_table( FT_Byte* table,
+ FT_Byte* table_limit )
+ {
+ FT_UInt lookupType;
+ FT_UInt real_lookupType = 0;
+ FT_UInt subtableCount;
+
+ FT_Byte* p = table;
+ FT_Byte* limit;
+
+ FT_UInt num_fitting_subtables = 0;
+
+
+ if ( table_limit < p + 6 )
+ return 0;
+
+ lookupType = FT_NEXT_USHORT( p );
+
+ p += 2; /* Skip `lookupFlag`. */
+
+ subtableCount = FT_NEXT_USHORT( p );
+ limit = p + subtableCount * 2;
+ if ( table_limit < limit )
+ return 0;
+
+ while ( p < limit )
+ {
+ FT_Byte* subtable = table + FT_NEXT_USHORT( p );
+ FT_UInt format;
+
+ FT_Bool is_fitting = FALSE;
+
+
+ if ( lookupType == 9 )
+ {
+ /* Positioning extension. */
+ FT_Byte* q = subtable;
+
+
+ if ( table_limit < q + 8 )
+ return 0;
+
+ if ( FT_NEXT_USHORT( q ) != 1 ) /* format */
+ return 0;
+
+ if ( real_lookupType == 0 )
+ real_lookupType = FT_NEXT_USHORT( q );
+ else if ( real_lookupType != FT_NEXT_USHORT( q ) )
+ return 0;
+
+ subtable += FT_PEEK_ULONG( q );
+ }
+ else
+ real_lookupType = lookupType;
+
+ /* Ensure the first eight bytes of the subtable formats. */
+ if ( table_limit < subtable + 8 )
+ return 0;
+
+ format = FT_PEEK_USHORT( subtable );
+
+ if ( real_lookupType == 2 )
+ {
+ if ( format == 1 )
+ {
+ if ( !tt_face_validate_pair_pos1( subtable,
+ table_limit,
+ &is_fitting ) )
+ return 0;
+ }
+ else if ( format == 2 )
+ {
+ if ( !tt_face_validate_pair_pos2( subtable,
+ table_limit,
+ &is_fitting ) )
+ return 0;
+ }
+ else
+ return 0;
+ }
+ else
+ return 0;
+
+ if ( is_fitting )
+ num_fitting_subtables++;
+ }
+
+ return num_fitting_subtables;
+ }
+
+
+ static void
+ tt_face_get_subtable_offsets( FT_Byte* table,
+ FT_Byte* gpos,
+ FT_UInt32* gpos_lookups_kerning,
+ FT_UInt* idx )
+ {
+ FT_UInt lookupType;
+ FT_UInt subtableCount;
+
+ FT_Byte* p = table;
+ FT_Byte* limit;
+
+
+ lookupType = FT_NEXT_USHORT( p );
+
+ p += 2;
+
+ subtableCount = FT_NEXT_USHORT( p );
+ limit = p + subtableCount * 2;
+ while ( p < limit )
+ {
+ FT_Byte* subtable = table + FT_NEXT_USHORT( p );
+ FT_UInt valueFormat1;
+ FT_UInt valueFormat2;
+
+
+ if ( lookupType == 9 )
+ subtable += FT_PEEK_ULONG( subtable + 4 );
+
+ /* Table offsets for `valueFormat[12]` values */
+ /* are identical for both subtable formats. */
+ valueFormat1 = FT_PEEK_USHORT( subtable + 4 );
+ valueFormat2 = FT_PEEK_USHORT( subtable + 6 );
+ if ( valueFormat1 == 0x4 && valueFormat2 == 0 )
+ {
+ /* We store offsets relative to the start of the GPOS table. */
+ gpos_lookups_kerning[(*idx)++] = (FT_UInt32)( subtable - gpos );
+ }
+ }
+ }
+
+
FT_LOCAL_DEF( FT_Error )
tt_face_load_gpos( TT_Face face,
FT_Stream stream )
{
- return FT_Err_Ok;
+ FT_Error error;
+ FT_Memory memory = face->root.memory;
+
+ FT_ULong gpos_length;
+ FT_Byte* gpos;
+ FT_Byte* gpos_limit;
+
+ FT_UInt32* gpos_lookups_kerning;
+
+ FT_UInt featureListOffset;
+
+ FT_UInt lookupListOffset;
+ FT_Byte* lookup_list;
+ FT_UInt lookupCount;
+
+ FT_UInt i;
+
+ FT_Byte* use_lookup_table = NULL;
+ FT_UInt num_fitting_subtables;
+
+ FT_Byte* p;
+ FT_Byte* limit;
+
+
+ face->gpos_table = NULL;
+ face->gpos_lookups_kerning = NULL;
+ face->num_gpos_lookups_kerning = 0;
+
+ gpos = NULL;
+ gpos_lookups_kerning = NULL;
+
+ error = face->goto_table( face, TTAG_GPOS, stream, &gpos_length );
+ if ( error )
+ goto Fail;
+
+ if ( FT_FRAME_EXTRACT( gpos_length, gpos ) )
+ goto Fail;
+
+ if ( gpos_length < 10 )
+ goto Fail;
+
+ gpos_limit = gpos + gpos_length;
+
+ /* We first need the number of GPOS lookups. */
+ lookupListOffset = FT_PEEK_USHORT( gpos + 8 );
+
+ lookup_list = gpos + lookupListOffset;
+ p = lookup_list;
+ if ( gpos_limit < p + 2 )
+ goto Fail;
+
+ lookupCount = FT_NEXT_USHORT( p );
+ limit = p + lookupCount * 2;
+ if ( gpos_limit < limit )
+ goto Fail;
+
+ /* Allocate an auxiliary array for Boolean values that */
+ /* gets filled while walking over all 'kern' features. */
+ if ( FT_NEW_ARRAY( use_lookup_table, lookupCount ) )
+ goto Fail;
+
+ featureListOffset = FT_PEEK_USHORT( gpos + 6 );
+
+ if ( !tt_face_validate_feature_table( gpos + featureListOffset,
+ gpos_limit,
+ lookupCount,
+ use_lookup_table ) )
+ goto Fail;
+
+ /* Now walk over all lookup tables and get the */
+ /* number of fitting subtables. */
+ num_fitting_subtables = 0;
+ for ( i = 0; i < lookupCount; i++ )
+ {
+ FT_UInt lookupOffset;
+
+
+ if ( !use_lookup_table[i] )
+ continue;
+
+ lookupOffset = FT_PEEK_USHORT( p + i * 2 );
+
+ num_fitting_subtables +=
+ tt_face_validate_lookup_table( lookup_list + lookupOffset,
+ gpos_limit );
+
+ }
+
+ /* Loop again over all lookup tables and */
+ /* collect offsets to those subtables. */
+ if ( num_fitting_subtables )
+ {
+ FT_UInt idx;
+
+
+ if ( FT_QNEW_ARRAY( gpos_lookups_kerning, num_fitting_subtables ) )
+ goto Fail;
+
+ idx = 0;
+ for ( i = 0; i < lookupCount; i++ )
+ {
+ FT_UInt lookupOffset;
+
+
+ if ( !use_lookup_table[i] )
+ continue;
+
+ lookupOffset = FT_PEEK_USHORT( p + i * 2 );
+
+ tt_face_get_subtable_offsets( lookup_list + lookupOffset,
+ gpos,
+ gpos_lookups_kerning,
+ &idx );
+ }
+ }
+
+ FT_FREE( use_lookup_table );
+ use_lookup_table = NULL;
+
+ face->gpos_table = gpos;
+ face->gpos_lookups_kerning = gpos_lookups_kerning;
+ face->num_gpos_lookups_kerning = num_fitting_subtables;
+
+ Exit:
+ return error;
+
+ Fail:
+ FT_FREE( gpos );
+ FT_FREE( gpos_lookups_kerning );
+ FT_FREE( use_lookup_table );
+
+ /* If we don't have an explicit error code, set it to a generic value. */
+ if ( !error )
+ error = FT_THROW( Invalid_Table );
+
+ goto Exit;
}
FT_LOCAL_DEF( void )
tt_face_done_gpos( TT_Face face )
{
+ FT_Stream stream = face->root.stream;
+ FT_Memory memory = face->root.memory;
+
+
+ FT_FRAME_RELEASE( face->gpos_table );
+ FT_FREE( face->gpos_lookups_kerning );
+ }
+
+
+ /*********************************/
+ /******** ********/
+ /******** GPOS access ********/
+ /******** ********/
+ /*********************************/
+
+
+ static FT_Long
+ tt_face_get_coverage_index( FT_Byte* table,
+ FT_UInt glyph_index )
+ {
+ FT_Byte* p = table;
+ FT_UInt format = FT_NEXT_USHORT( p );
+ FT_UInt count = FT_NEXT_USHORT( p );
+
+ FT_UInt min, max;
+
+
+ min = 0;
+ max = count;
+
+ if ( format == 1 )
+ {
+ while ( min < max )
+ {
+ FT_UInt mid = min + ( max - min ) / 2;
+ FT_UInt mid_index = FT_PEEK_USHORT( p + mid * 2 );
+
+
+ if ( glyph_index > mid_index )
+ min = mid + 1;
+ else if ( glyph_index < mid_index )
+ max = mid;
+ else
+ return mid;
+ }
+ }
+ else
+ {
+ while ( min < max )
+ {
+ FT_UInt mid = min + ( max - min ) / 2;
+ FT_UInt startGlyphID = FT_PEEK_USHORT( p + mid * 6 );
+ FT_UInt endGlyphID = FT_PEEK_USHORT( p + mid * 6 + 2 );
+
+
+ if ( glyph_index > endGlyphID )
+ min = mid + 1;
+ else if ( glyph_index < startGlyphID )
+ max = mid;
+ else
+ {
+ FT_UInt startCoverageIndex = FT_PEEK_USHORT( p + mid * 6 + 4 );
+
+
+ return startCoverageIndex + glyph_index - startGlyphID;
+ }
+ }
+ }
+
+ return -1;
+ }
+
+
+ static FT_UInt
+ tt_face_get_class( FT_Byte* table,
+ FT_UInt glyph_index )
+ {
+ FT_Byte* p = table;
+ FT_UInt format = FT_NEXT_USHORT( p );
+
+
+ if ( format == 1 )
+ {
+ FT_UInt startGlyphID = FT_NEXT_USHORT( p );
+ FT_UInt glyphCount = FT_NEXT_USHORT( p );
+
+
+ /* XXX: Is this modulo 65536 arithmetic? */
+ if ( startGlyphID <= glyph_index &&
+ startGlyphID + glyphCount >= glyph_index )
+ return FT_PEEK_USHORT( p + ( glyph_index - startGlyphID ) * 2 );
+ }
+ else
+ {
+ FT_UInt count = FT_NEXT_USHORT( p );
+
+ FT_UInt min, max;
+
+
+ min = 0;
+ max = count;
+
+ while ( min < max )
+ {
+ FT_UInt mid = min + ( max - min ) / 2;
+ FT_UInt startGlyphID = FT_PEEK_USHORT( p + mid * 6 );
+ FT_UInt endGlyphID = FT_PEEK_USHORT( p + mid * 6 + 2 );
+
+
+ if ( glyph_index > endGlyphID )
+ min = mid + 1;
+ else if ( glyph_index < startGlyphID )
+ max = mid;
+ else
+ return FT_PEEK_USHORT( p + mid * 6 + 4 );
+ }
+ }
+
+ return 0;
+ }
+
+
+ static FT_Bool
+ tt_face_get_pair_pos1_kerning( FT_Byte* table,
+ FT_UInt first_glyph,
+ FT_UInt second_glyph,
+ FT_Int* kerning )
+ {
+ FT_Byte* coverage = table + FT_PEEK_USHORT( table + 2 );
+ FT_Long coverage_index = tt_face_get_coverage_index( coverage,
+ first_glyph );
+
+ FT_UInt pair_set_offset;
+ FT_Byte* p;
+ FT_UInt count;
+
+ FT_UInt min, max;
+
+
+ if ( coverage_index < 0 )
+ return FALSE;
+
+ pair_set_offset = FT_PEEK_USHORT( table + 10 + coverage_index * 2 );
+ p = table + pair_set_offset;
+ count = FT_NEXT_USHORT( p );
+
+ min = 0;
+ max = count;
+
+ while ( min < max )
+ {
+ FT_UInt mid = min + ( max - min ) / 2;
+ FT_UInt mid_index = FT_PEEK_USHORT( p + mid * 4 );
+
+
+ if ( second_glyph > mid_index )
+ min = max + 1;
+ else if ( second_glyph < mid_index )
+ max = mid;
+ else
+ {
+ *kerning = FT_PEEK_SHORT( p + mid * 4 + 2 );
+
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+ }
+
+
+ static FT_Bool
+ tt_face_get_pair_pos2_kerning( FT_Byte* table,
+ FT_UInt first_glyph,
+ FT_UInt second_glyph,
+ FT_Int* kerning )
+ {
+ FT_Byte* coverage = table + FT_PEEK_USHORT( table + 2 );
+ FT_Long coverage_index = tt_face_get_coverage_index( coverage,
+ first_glyph );
+
+ FT_Byte* class_def1;
+ FT_Byte* class_def2;
+ FT_UInt first_class;
+ FT_UInt second_class;
+ FT_UInt class2Count;
+
+
+ if ( coverage_index < 0 )
+ return FALSE;
+
+ class_def1 = table + FT_PEEK_USHORT( table + 8 );
+ class_def2 = table + FT_PEEK_USHORT( table + 10 );
+
+ class2Count = FT_PEEK_USHORT( table + 14 );
+
+ first_class = tt_face_get_class( class_def1, first_glyph );
+ second_class = tt_face_get_class( class_def2, second_glyph );
+
+ *kerning =
+ FT_PEEK_SHORT( table + 16 +
+ ( first_class * class2Count + second_class ) * 2 );
+
+ return TRUE;
}
FT_LOCAL_DEF( FT_Int )
tt_face_get_gpos_kerning( TT_Face face,
- FT_UInt left_glyph,
- FT_UInt right_glyph )
+ FT_UInt first_glyph,
+ FT_UInt second_glyph )
{
- return 0;
+ FT_Int kerning = 0;
+
+ FT_UInt i;
+
+
+ /* We only have `PairPos` subtables. */
+ for ( i = 0; i < face->num_gpos_lookups_kerning; i++ )
+ {
+ FT_Byte* subtable = face->gpos_table + face->gpos_lookups_kerning[i];
+ FT_Byte* p = subtable;
+
+ FT_UInt format = FT_NEXT_USHORT( p );
+
+
+ if ( format == 1 )
+ {
+ if ( tt_face_get_pair_pos1_kerning( subtable,
+ first_glyph,
+ second_glyph,
+ &kerning ) )
+ break;
+ }
+ else
+ {
+ if ( tt_face_get_pair_pos2_kerning( subtable,
+ first_glyph,
+ second_glyph,
+ &kerning ) )
+ break;
+ }
+ }
+
+ return kerning;
}
#else /* !TT_CONFIG_OPTION_GPOS_KERNING */
diff --git a/src/sfnt/ttgpos.h b/src/sfnt/ttgpos.h
index 570e9e3..42e2a5e 100644
--- a/src/sfnt/ttgpos.h
+++ b/src/sfnt/ttgpos.h
@@ -1,10 +1,9 @@
/****************************************************************************
*
- * ttgpos.c
+ * ttgpos.h
*
- * Load the TrueType GPOS table. The only GPOS layout feature this
- * currently supports is kerning, from x advances in the pair adjustment
- * layout feature.
+ * Routines to parse and access the 'GPOS' table for simple kerning
+ * (specification).
*
* Copyright (C) 2024 by
* David Saltzman
@@ -39,8 +38,8 @@
FT_LOCAL( FT_Int )
tt_face_get_gpos_kerning( TT_Face face,
- FT_UInt left_glyph,
- FT_UInt right_glyph );
+ FT_UInt first_glyph,
+ FT_UInt second_glyph );
#endif /* TT_CONFIG_OPTION_GPOS_KERNING */
diff --git a/src/sfnt/ttkern.c b/src/sfnt/ttkern.c
index f041136..cd6deb2 100644
--- a/src/sfnt/ttkern.c
+++ b/src/sfnt/ttkern.c
@@ -2,8 +2,7 @@
*
* ttkern.c
*
- * Load the basic TrueType kerning table. This doesn't handle
- * kerning data within the GPOS table at the moment.
+ * Routines to parse and access the 'kern' table for kerning (body).
*
* Copyright (C) 1996-2024 by
* David Turner, Robert Wilhelm, and Werner Lemberg.
diff --git a/src/sfnt/ttkern.h b/src/sfnt/ttkern.h
index 2bf3914..8615ab2 100644
--- a/src/sfnt/ttkern.h
+++ b/src/sfnt/ttkern.h
@@ -2,8 +2,8 @@
*
* ttkern.h
*
- * Load the basic TrueType kerning table. This doesn't handle
- * kerning data within the GPOS table at the moment.
+ * Routines to parse and access the 'kern' table for kerning
+ * (specification).
*
* Copyright (C) 1996-2024 by
* David Turner, Robert Wilhelm, and Werner Lemberg.
diff --git a/src/truetype/ttdriver.c b/src/truetype/ttdriver.c
index 5d5341d..41a21d1 100644
--- a/src/truetype/ttdriver.c
+++ b/src/truetype/ttdriver.c
@@ -225,7 +225,7 @@
left_glyph,
right_glyph );
#ifdef TT_CONFIG_OPTION_GPOS_KERNING
- else if ( ttface->gpos_kerning_available )
+ else if ( ttface->num_gpos_lookups_kerning )
kerning->x = sfnt->get_gpos_kerning( ttface,
left_glyph,
right_glyph );