[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 );