lib/filigree/src/font/gpos.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const binary = @import("binary.zig");
   3 const gdef_table = @import("gdef.zig");
   4 const layout = @import("../layout/root.zig");
   5 const lookups = @import("lookups.zig");
   6 const model = @import("model.zig");
   7 
   8 const FontError = model.FontError;
   9 const FeatureRange = model.FeatureRange;
  10 const Gdef = gdef_table.Gdef;
  11 const GlyphClass = model.GlyphClass;
  12 const GposContextualPairAdjustment = model.GposContextualPairAdjustment;
  13 const GposCursiveAttachment = model.GposCursiveAttachment;
  14 const GposPairAdjustment = model.GposPairAdjustment;
  15 const GposPairAdjustmentBefore = model.GposPairAdjustmentBefore;
  16 const GposSingleAdjustment = model.GposSingleAdjustment;
  17 const GposValueAdjustment = model.GposValueAdjustment;
  18 const LayoutSelection = model.LayoutSelection;
  19 const LookupFilter = lookups.LookupFilter;
  20 const Table = binary.Table;
  21 const Tag = model.Tag;
  22 const VariationSetting = model.VariationSetting;
  23 const defaultScriptTag = model.defaultScriptTag;
  24 const featureEnabled = model.featureEnabled;
  25 const noGposVariationIndex = model.noGposVariationIndex;
  26 const tagLiteral = model.tag;
  27 const featureTableContainsLookup = lookups.featureTableContainsLookup;
  28 const gposChildOffset = lookups.gposChildOffset;
  29 const gposChildOffset32 = lookups.gposChildOffset32;
  30 const gposLookupFilter = lookups.gposLookupFilter;
  31 const gposOffset = lookups.gposOffset;
  32 const hasGposBytes = lookups.gposHasBytes;
  33 const lookupGlyphIgnored = lookups.lookupGlyphIgnored;
  34 const lookupFilterSkipsGlyphs = lookups.lookupFilterSkipsGlyphs;
  35 const matchingFeatureSubstitutionOffset = lookups.matchingFeatureSubstitutionOffset;
  36 const previousLookupGlyphIndex = previousLookupGlyphIndexLocal;
  37 const readGposI16 = lookups.gposReadI16;
  38 const readGposU16 = lookups.gposReadU16;
  39 const readGposU32 = lookups.gposReadU32;
  40 const scriptTagListContains = lookups.scriptTagListContains;
  41 const substitutedFeatureOffset = lookups.substitutedFeatureOffset;
  42 const tag = model.tag;
  43 
  44 const ChainedContextMatch = struct {
  45     lookup_records_offset: usize,
  46     lookup_count: u16,
  47     input_count: u16,
  48 };
  49 
  50 fn featureMayBeEnabled(selection: LayoutSelection, feature_tag: Tag, default_on: bool) bool {
  51     var globally_enabled = default_on;
  52     var ranged_enabled_after_global = false;
  53     for (selection.features) |setting| {
  54         if (setting.tag != feature_tag) continue;
  55         if (setting.source == null) {
  56             globally_enabled = setting.value != 0;
  57             ranged_enabled_after_global = false;
  58             continue;
  59         }
  60         if (setting.value != 0) ranged_enabled_after_global = true;
  61     }
  62     return globally_enabled or ranged_enabled_after_global;
  63 }
  64 
  65 fn gposFeatureDefaultOn(selection: LayoutSelection, feature_tag: Tag) bool {
  66     return switch (feature_tag) {
  67         tagLiteral("kern") => !selection.vertical,
  68         tagLiteral("vkrn") => selection.vertical,
  69         tagLiteral("dist"), tagLiteral("curs") => !selection.vertical,
  70         tagLiteral("mark"), tagLiteral("mkmk"), tagLiteral("abvm"), tagLiteral("blwm") => true,
  71         else => false,
  72     };
  73 }
  74 
  75 pub const Gpos = struct {
  76     table: Table,
  77     script_list_offset: usize,
  78     feature_list_offset: usize,
  79     lookup_list_offset: usize,
  80     feature_variations_offset: ?usize,
  81 
  82     pub const init = initImpl;
  83     pub const lookupCount = lookupCountImpl;
  84     pub const lookupResolvedType = lookupResolvedTypeImpl;
  85     pub const pairAdjustment = pairAdjustmentImpl;
  86     pub const pairAdjustmentBefore = pairAdjustmentBeforeImpl;
  87     pub const pairAdjustmentBeforeAtLookup = pairAdjustmentBeforeAtLookupImpl;
  88     pub const singleAdjustment = singleAdjustmentImpl;
  89     pub const singleAdjustmentAt = singleAdjustmentAtImpl;
  90     pub const singleAdjustmentAtLookup = singleAdjustmentAtLookupImpl;
  91     pub const cursiveAttachment = cursiveAttachmentImpl;
  92     pub const cursiveAttachmentAtLookup = cursiveAttachmentAtLookupImpl;
  93     pub const markToBaseAdjustment = markToBaseAdjustmentImpl;
  94     pub const markToBaseAdjustmentAtLookup = markToBaseAdjustmentAtLookupImpl;
  95     pub const markToLigatureAdjustment = markToLigatureAdjustmentImpl;
  96     pub const markToLigatureAdjustmentAtLookup = markToLigatureAdjustmentAtLookupImpl;
  97     pub const markToMarkAdjustment = markToMarkAdjustmentImpl;
  98     pub const markToMarkAdjustmentAtLookup = markToMarkAdjustmentAtLookupImpl;
  99     pub const hasActiveLookupType = hasActiveLookupTypeImpl;
 100     pub const hasActiveSingleAdjustment = hasActiveSingleAdjustmentImpl;
 101     pub const hasActiveContextualPairAdjustment = hasActiveContextualPairAdjustmentImpl;
 102     const lookupMayBeActive = lookupMayBeActiveImpl;
 103     const lookupHasResolvedType = lookupHasResolvedTypeImpl;
 104     const lookupPairAdjustment = lookupPairAdjustmentImpl;
 105     const lookupPairAdjustmentBefore = lookupPairAdjustmentBeforeImpl;
 106     pub const contextualPairAdjustmentAt = contextualPairAdjustmentAtImpl;
 107     pub const contextualPairAdjustmentAtLookup = contextualPairAdjustmentAtLookupImpl;
 108     const lookupContextualPairAdjustmentAt = lookupContextualPairAdjustmentAtImpl;
 109     const lookupNestedPairAdjustment = lookupNestedPairAdjustmentImpl;
 110     const contextualPairAdjustment = contextualPairAdjustmentImpl;
 111     const contextualPairAdjustmentFormat1 = contextualPairAdjustmentFormat1Impl;
 112     const contextualPairAdjustmentFormat2 = contextualPairAdjustmentFormat2Impl;
 113     const contextualPairAdjustmentFormat3 = contextualPairAdjustmentFormat3Impl;
 114     const chainedContextualPairAdjustment = chainedContextualPairAdjustmentImpl;
 115     const chainedContextualPairAdjustmentFormat1 = chainedContextualPairAdjustmentFormat1Impl;
 116     const chainedContextualPairAdjustmentFormat2 = chainedContextualPairAdjustmentFormat2Impl;
 117     const chainedContextualPairAdjustmentFormat3 = chainedContextualPairAdjustmentFormat3Impl;
 118     const chainedContextualMatchFormat1 = chainedContextualMatchFormat1Impl;
 119     const chainedContextualMatchFormat2 = chainedContextualMatchFormat2Impl;
 120     const chainedContextualRuleSetMatch = chainedContextualRuleSetMatchImpl;
 121     const chainedContextualRuleMatch = chainedContextualRuleMatchImpl;
 122     const chainedContextualClassSetMatch = chainedContextualClassSetMatchImpl;
 123     const chainedContextualClassRuleMatch = chainedContextualClassRuleMatchImpl;
 124     const contextualPairAdjustmentRuleSet = contextualPairAdjustmentRuleSetImpl;
 125     const contextualPairAdjustmentClassSet = contextualPairAdjustmentClassSetImpl;
 126     const contextualPairAdjustmentClassRule = contextualPairAdjustmentClassRuleImpl;
 127     const contextualPairAdjustmentRule = contextualPairAdjustmentRuleImpl;
 128     const contextualPairAdjustmentRecords = contextualPairAdjustmentRecordsImpl;
 129     const lookupSingleAdjustment = lookupSingleAdjustmentImpl;
 130     const lookupSingleAdjustmentAt = lookupSingleAdjustmentAtImpl;
 131     const contextualSingleAdjustment = contextualSingleAdjustmentImpl;
 132     const contextualSingleAdjustmentFormat1 = contextualSingleAdjustmentFormat1Impl;
 133     const contextualSingleAdjustmentFormat2 = contextualSingleAdjustmentFormat2Impl;
 134     const contextualSingleAdjustmentFormat3 = contextualSingleAdjustmentFormat3Impl;
 135     const chainedContextualSingleAdjustment = chainedContextualSingleAdjustmentImpl;
 136     const chainedContextualSingleAdjustmentFormat1 = chainedContextualSingleAdjustmentFormat1Impl;
 137     const chainedContextualSingleAdjustmentFormat2 = chainedContextualSingleAdjustmentFormat2Impl;
 138     const chainedContextualSingleAdjustmentFormat3 = chainedContextualSingleAdjustmentFormat3Impl;
 139     const contextualSingleAdjustmentRuleSet = contextualSingleAdjustmentRuleSetImpl;
 140     const contextualSingleAdjustmentClassSet = contextualSingleAdjustmentClassSetImpl;
 141     const contextualSingleAdjustmentClassRule = contextualSingleAdjustmentClassRuleImpl;
 142     const contextualSingleAdjustmentRule = contextualSingleAdjustmentRuleImpl;
 143     const contextualSingleAdjustmentRecords = contextualSingleAdjustmentRecordsImpl;
 144     const lookupCursiveAttachment = lookupCursiveAttachmentImpl;
 145     const lookupMarkToBaseAdjustment = lookupMarkToBaseAdjustmentImpl;
 146     const lookupMarkToLigatureAdjustment = lookupMarkToLigatureAdjustmentImpl;
 147     const lookupMarkToMarkAdjustment = lookupMarkToMarkAdjustmentImpl;
 148     const lookupOffset = lookupOffsetImpl;
 149     const lookupIsActive = lookupIsActiveImpl;
 150     const featureTag = featureTagImpl;
 151     const featureContainsLookup = featureContainsLookupImpl;
 152     const featureOffset = featureOffsetImpl;
 153     const featureRecord = featureRecordImpl;
 154 };
 155 
 156 fn initImpl(data: []const u8, gpos_table: Table) FontError!Gpos {
 157     const bytes = data[gpos_table.offset..][0..gpos_table.len];
 158     if (bytes.len < 10) return error.InvalidGpos;
 159     const major = try readGposU16(bytes, 0);
 160     const minor = try readGposU16(bytes, 2);
 161     if (major != 1 or (minor != 0 and minor != 1)) return error.InvalidGpos;
 162     if (minor == 1 and bytes.len < 14) return error.InvalidGpos;
 163 
 164     const script_list_offset = try gposOffset(bytes, try readGposU16(bytes, 4));
 165     const feature_list_offset = try gposOffset(bytes, try readGposU16(bytes, 6));
 166     const lookup_list_offset = try gposOffset(bytes, try readGposU16(bytes, 8));
 167     const feature_variations_offset = if (minor == 1) blk: {
 168         const relative = try readGposU32(bytes, 10);
 169         if (relative == 0) break :blk null;
 170         break :blk try gposChildOffset32(bytes, 0, relative, 8);
 171     } else null;
 172 
 173     return .{
 174         .table = gpos_table,
 175         .script_list_offset = script_list_offset,
 176         .feature_list_offset = feature_list_offset,
 177         .lookup_list_offset = lookup_list_offset,
 178         .feature_variations_offset = feature_variations_offset,
 179     };
 180 }
 181 
 182 fn lookupCountImpl(self: Gpos, data: []const u8) FontError!u16 {
 183     const bytes = data[self.table.offset..][0..self.table.len];
 184     return try readGposU16(bytes, self.lookup_list_offset);
 185 }
 186 
 187 fn lookupResolvedTypeImpl(self: Gpos, data: []const u8, lookup_index: u16) FontError!u16 {
 188     const bytes = data[self.table.offset..][0..self.table.len];
 189     const lookup_offset = try self.lookupOffset(bytes, lookup_index);
 190     const lookup_type = try readGposU16(bytes, lookup_offset);
 191     if (lookup_type != 9) return lookup_type;
 192     const subtable_count = try readGposU16(bytes, lookup_offset + 4);
 193     if (!hasGposBytes(bytes, lookup_offset + 6, @as(usize, subtable_count) * 2)) return error.InvalidGpos;
 194     for (0..subtable_count) |i| {
 195         const subtable_offset = try gposChildOffset(bytes, lookup_offset, try readGposU16(bytes, lookup_offset + 6 + i * 2), 8);
 196         const resolved = try resolveGposSubtable(bytes, lookup_type, subtable_offset);
 197         return resolved.lookup_type;
 198     }
 199     return 0;
 200 }
 201 
 202 fn pairAdjustmentImpl(self: Gpos, data: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, gdef: ?Gdef, left_glyph: u32, right_glyph: u32) FontError!GposPairAdjustment {
 203     var result = GposPairAdjustment{};
 204     if (left_glyph > std.math.maxInt(u16) or right_glyph > std.math.maxInt(u16)) return result;
 205     const left: u16 = @intCast(left_glyph);
 206     const right: u16 = @intCast(right_glyph);
 207     const bytes = data[self.table.offset..][0..self.table.len];
 208     const lookup_count = try readGposU16(bytes, self.lookup_list_offset);
 209     for (0..lookup_count) |lookup_index| {
 210         const index: u16 = @intCast(lookup_index);
 211         if (!try self.lookupIsActive(bytes, face, selection, index, null)) continue;
 212         if (try self.lookupPairAdjustment(data, bytes, gdef, index, left, right)) |adjustment| {
 213             result.add(adjustment);
 214         }
 215     }
 216     return result;
 217 }
 218 
 219 fn pairAdjustmentBeforeImpl(self: Gpos, data: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, gdef: ?Gdef, glyph_ids: []const u32, glyph_index: usize, source: ?FeatureRange) FontError!?GposPairAdjustmentBefore {
 220     if (glyph_index == 0 or glyph_index >= glyph_ids.len or glyph_ids[glyph_index] > std.math.maxInt(u16)) return null;
 221     const bytes = data[self.table.offset..][0..self.table.len];
 222     const lookup_count = try readGposU16(bytes, self.lookup_list_offset);
 223     var result = GposPairAdjustment{};
 224     var result_left_offset: usize = 0;
 225     var found = false;
 226 
 227     for (0..lookup_count) |lookup_index| {
 228         const index: u16 = @intCast(lookup_index);
 229         if (!try self.lookupIsActive(bytes, face, selection, index, source)) continue;
 230         if (try self.lookupPairAdjustmentBefore(data, bytes, gdef, index, glyph_ids, glyph_index)) |adjustment| {
 231             if (found and adjustment.left_offset != result_left_offset) {
 232                 if (!result.isZero()) return .{ .adjustment = result, .left_offset = result_left_offset };
 233                 result = adjustment.adjustment;
 234                 result_left_offset = adjustment.left_offset;
 235                 found = true;
 236                 continue;
 237             }
 238             result.add(adjustment.adjustment);
 239             result_left_offset = adjustment.left_offset;
 240             found = true;
 241         }
 242     }
 243     if (!found) return null;
 244     return .{ .adjustment = result, .left_offset = result_left_offset };
 245 }
 246 
 247 fn pairAdjustmentBeforeAtLookupImpl(self: Gpos, data: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, gdef: ?Gdef, lookup_index: u16, glyph_ids: []const u32, glyph_index: usize, source: ?FeatureRange) FontError!?GposPairAdjustmentBefore {
 248     if (glyph_index == 0 or glyph_index >= glyph_ids.len or glyph_ids[glyph_index] > std.math.maxInt(u16)) return null;
 249     const bytes = data[self.table.offset..][0..self.table.len];
 250     if (!try self.lookupIsActive(bytes, face, selection, lookup_index, source)) return null;
 251     return try self.lookupPairAdjustmentBefore(data, bytes, gdef, lookup_index, glyph_ids, glyph_index);
 252 }
 253 
 254 fn singleAdjustmentImpl(self: Gpos, data: []const u8, selection: LayoutSelection, gdef: ?Gdef, glyph_id: u32) FontError!GposValueAdjustment {
 255     var glyphs = [_]u32{glyph_id};
 256     const result = (try self.singleAdjustmentAt(data, null, selection, gdef, &glyphs, 0, null)) orelse return .{};
 257     if (result.target_offset != 0) return .{};
 258     return result.adjustment;
 259 }
 260 
 261 fn singleAdjustmentAtImpl(self: Gpos, data: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, gdef: ?Gdef, glyph_ids: []const u32, glyph_index: usize, source: ?FeatureRange) FontError!?GposSingleAdjustment {
 262     var result = GposValueAdjustment{};
 263     if (glyph_index >= glyph_ids.len or glyph_ids[glyph_index] > std.math.maxInt(u16)) return null;
 264     const bytes = data[self.table.offset..][0..self.table.len];
 265     const lookup_count = try readGposU16(bytes, self.lookup_list_offset);
 266     var found = false;
 267     for (0..lookup_count) |lookup_index| {
 268         const index: u16 = @intCast(lookup_index);
 269         if (try self.lookupSingleAdjustmentAt(data, bytes, face, selection, gdef, index, glyph_ids, glyph_index, source, true, true)) |adjustment| {
 270             if (adjustment.target_offset != 0 or adjustment.skip_count != 0) {
 271                 if (found and !result.isZero()) return .{ .adjustment = result };
 272                 return adjustment;
 273             }
 274             result.add(adjustment.adjustment);
 275             found = true;
 276         }
 277     }
 278     if (!found) return null;
 279     return .{ .adjustment = result };
 280 }
 281 
 282 fn singleAdjustmentAtLookupImpl(self: Gpos, data: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, gdef: ?Gdef, lookup_index: u16, glyph_ids: []const u32, glyph_index: usize, source: ?FeatureRange) FontError!?GposSingleAdjustment {
 283     if (glyph_index >= glyph_ids.len or glyph_ids[glyph_index] > std.math.maxInt(u16)) return null;
 284     const bytes = data[self.table.offset..][0..self.table.len];
 285     return try self.lookupSingleAdjustmentAt(data, bytes, face, selection, gdef, lookup_index, glyph_ids, glyph_index, source, true, true);
 286 }
 287 
 288 fn cursiveAttachmentImpl(self: Gpos, data: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, gdef: ?Gdef, exit_glyph: u32, entry_glyph: u32, source: ?FeatureRange) FontError!?GposCursiveAttachment {
 289     if (exit_glyph > std.math.maxInt(u16) or entry_glyph > std.math.maxInt(u16)) return null;
 290     const exit: u16 = @intCast(exit_glyph);
 291     const entry: u16 = @intCast(entry_glyph);
 292     const bytes = data[self.table.offset..][0..self.table.len];
 293     const lookup_count = try readGposU16(bytes, self.lookup_list_offset);
 294     var result: ?GposCursiveAttachment = null;
 295     for (0..lookup_count) |lookup_index| {
 296         const index: u16 = @intCast(lookup_index);
 297         if (!try self.lookupIsActive(bytes, face, selection, index, source)) continue;
 298         if (try self.lookupCursiveAttachment(data, bytes, gdef, index, exit, entry)) |attachment| {
 299             result = attachment;
 300         }
 301     }
 302     return result;
 303 }
 304 
 305 fn cursiveAttachmentAtLookupImpl(self: Gpos, data: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, gdef: ?Gdef, lookup_index: u16, exit_glyph: u32, entry_glyph: u32, source: ?FeatureRange) FontError!?GposCursiveAttachment {
 306     if (exit_glyph > std.math.maxInt(u16) or entry_glyph > std.math.maxInt(u16)) return null;
 307     const bytes = data[self.table.offset..][0..self.table.len];
 308     if (!try self.lookupIsActive(bytes, face, selection, lookup_index, source)) return null;
 309     return try self.lookupCursiveAttachment(data, bytes, gdef, lookup_index, @intCast(exit_glyph), @intCast(entry_glyph));
 310 }
 311 
 312 fn markToBaseAdjustmentImpl(self: Gpos, data: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, gdef: ?Gdef, base_glyph: u32, mark_glyph: u32, source: ?FeatureRange) FontError!?GposValueAdjustment {
 313     if (base_glyph > std.math.maxInt(u16) or mark_glyph > std.math.maxInt(u16)) return null;
 314     const base: u16 = @intCast(base_glyph);
 315     const mark: u16 = @intCast(mark_glyph);
 316     const bytes = data[self.table.offset..][0..self.table.len];
 317     const lookup_count = try readGposU16(bytes, self.lookup_list_offset);
 318     var result: ?GposValueAdjustment = null;
 319     for (0..lookup_count) |lookup_index| {
 320         const index: u16 = @intCast(lookup_index);
 321         if (!try self.lookupIsActive(bytes, face, selection, index, source)) continue;
 322         if (try self.lookupMarkToBaseAdjustment(data, bytes, gdef, index, base, mark)) |adjustment| {
 323             result = adjustment;
 324         }
 325     }
 326     return result;
 327 }
 328 
 329 fn markToBaseAdjustmentAtLookupImpl(self: Gpos, data: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, gdef: ?Gdef, lookup_index: u16, base_glyph: u32, mark_glyph: u32, source: ?FeatureRange) FontError!?GposValueAdjustment {
 330     if (base_glyph > std.math.maxInt(u16) or mark_glyph > std.math.maxInt(u16)) return null;
 331     const bytes = data[self.table.offset..][0..self.table.len];
 332     if (!try self.lookupIsActive(bytes, face, selection, lookup_index, source)) return null;
 333     return try self.lookupMarkToBaseAdjustment(data, bytes, gdef, lookup_index, @intCast(base_glyph), @intCast(mark_glyph));
 334 }
 335 
 336 fn markToLigatureAdjustmentImpl(self: Gpos, data: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, gdef: ?Gdef, ligature_glyph: u32, mark_glyph: u32, component_index: u16, source: ?FeatureRange) FontError!?GposValueAdjustment {
 337     if (ligature_glyph > std.math.maxInt(u16) or mark_glyph > std.math.maxInt(u16)) return null;
 338     const ligature: u16 = @intCast(ligature_glyph);
 339     const mark: u16 = @intCast(mark_glyph);
 340     const bytes = data[self.table.offset..][0..self.table.len];
 341     const lookup_count = try readGposU16(bytes, self.lookup_list_offset);
 342     var result: ?GposValueAdjustment = null;
 343     for (0..lookup_count) |lookup_index| {
 344         const index: u16 = @intCast(lookup_index);
 345         if (!try self.lookupIsActive(bytes, face, selection, index, source)) continue;
 346         if (try self.lookupMarkToLigatureAdjustment(data, bytes, gdef, index, ligature, mark, component_index)) |adjustment| {
 347             result = adjustment;
 348         }
 349     }
 350     return result;
 351 }
 352 
 353 fn markToLigatureAdjustmentAtLookupImpl(self: Gpos, data: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, gdef: ?Gdef, lookup_index: u16, ligature_glyph: u32, mark_glyph: u32, component_index: u16, source: ?FeatureRange) FontError!?GposValueAdjustment {
 354     if (ligature_glyph > std.math.maxInt(u16) or mark_glyph > std.math.maxInt(u16)) return null;
 355     const bytes = data[self.table.offset..][0..self.table.len];
 356     if (!try self.lookupIsActive(bytes, face, selection, lookup_index, source)) return null;
 357     return try self.lookupMarkToLigatureAdjustment(data, bytes, gdef, lookup_index, @intCast(ligature_glyph), @intCast(mark_glyph), component_index);
 358 }
 359 
 360 fn markToMarkAdjustmentImpl(self: Gpos, data: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, gdef: ?Gdef, base_mark_glyph: u32, mark_glyph: u32, source: ?FeatureRange) FontError!?GposValueAdjustment {
 361     if (base_mark_glyph > std.math.maxInt(u16) or mark_glyph > std.math.maxInt(u16)) return null;
 362     const base_mark: u16 = @intCast(base_mark_glyph);
 363     const mark: u16 = @intCast(mark_glyph);
 364     const bytes = data[self.table.offset..][0..self.table.len];
 365     const lookup_count = try readGposU16(bytes, self.lookup_list_offset);
 366     var result: ?GposValueAdjustment = null;
 367     for (0..lookup_count) |lookup_index| {
 368         const index: u16 = @intCast(lookup_index);
 369         if (!try self.lookupIsActive(bytes, face, selection, index, source)) continue;
 370         if (try self.lookupMarkToMarkAdjustment(data, bytes, gdef, index, base_mark, mark)) |adjustment| {
 371             result = adjustment;
 372         }
 373     }
 374     return result;
 375 }
 376 
 377 fn markToMarkAdjustmentAtLookupImpl(self: Gpos, data: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, gdef: ?Gdef, lookup_index: u16, base_mark_glyph: u32, mark_glyph: u32, source: ?FeatureRange) FontError!?GposValueAdjustment {
 378     if (base_mark_glyph > std.math.maxInt(u16) or mark_glyph > std.math.maxInt(u16)) return null;
 379     const bytes = data[self.table.offset..][0..self.table.len];
 380     if (!try self.lookupIsActive(bytes, face, selection, lookup_index, source)) return null;
 381     return try self.lookupMarkToMarkAdjustment(data, bytes, gdef, lookup_index, @intCast(base_mark_glyph), @intCast(mark_glyph));
 382 }
 383 
 384 fn hasActiveLookupTypeImpl(self: Gpos, data: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, wanted_lookup_type: u16) FontError!bool {
 385     const bytes = data[self.table.offset..][0..self.table.len];
 386     const lookup_count = try readGposU16(bytes, self.lookup_list_offset);
 387     for (0..lookup_count) |lookup_index| {
 388         const index: u16 = @intCast(lookup_index);
 389         if (!try self.lookupMayBeActive(bytes, face, selection, index)) continue;
 390         const lookup_offset = try self.lookupOffset(bytes, index);
 391         if (try self.lookupHasResolvedType(bytes, lookup_offset, wanted_lookup_type)) return true;
 392     }
 393     return false;
 394 }
 395 
 396 noinline fn hasActiveSingleAdjustmentImpl(self: Gpos, data: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection) FontError!bool {
 397     if (try self.hasActiveLookupType(data, face, selection, 1)) return true;
 398     if (try self.hasActiveLookupType(data, face, selection, 7)) return true;
 399     return try self.hasActiveLookupType(data, face, selection, 8);
 400 }
 401 
 402 noinline fn hasActiveContextualPairAdjustmentImpl(self: Gpos, data: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection) FontError!bool {
 403     if (try self.hasActiveLookupType(data, face, selection, 7)) return true;
 404     return try self.hasActiveLookupType(data, face, selection, 8);
 405 }
 406 
 407 fn lookupMayBeActiveImpl(self: Gpos, bytes: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, lookup_index: u16) FontError!bool {
 408     const langsys_offset = (try gposLangSysOffset(bytes, self.script_list_offset, selection)) orelse return false;
 409     const required_feature_index = try readGposU16(bytes, langsys_offset + 2);
 410     if (required_feature_index != 0xffff) {
 411         const feature_index = required_feature_index;
 412         if (try self.featureContainsLookup(bytes, face, selection, feature_index, lookup_index)) return true;
 413     }
 414 
 415     const feature_count = try readGposU16(bytes, langsys_offset + 4);
 416     if (!hasGposBytes(bytes, langsys_offset + 6, @as(usize, feature_count) * 2)) return error.InvalidGpos;
 417     for (0..feature_count) |i| {
 418         const feature_index = try readGposU16(bytes, langsys_offset + 6 + i * 2);
 419         const feature_tag = try self.featureTag(bytes, feature_index);
 420         if (!featureMayBeEnabled(selection, feature_tag, gposFeatureDefaultOn(selection, feature_tag))) continue;
 421         if (try self.featureContainsLookup(bytes, face, selection, feature_index, lookup_index)) return true;
 422     }
 423     return false;
 424 }
 425 
 426 fn lookupHasResolvedTypeImpl(self: Gpos, bytes: []const u8, lookup_offset: usize, wanted_lookup_type: u16) FontError!bool {
 427     _ = self;
 428     const lookup_type = try readGposU16(bytes, lookup_offset);
 429     if (lookup_type == wanted_lookup_type) return true;
 430     if (lookup_type != 9) return false;
 431     const subtable_count = try readGposU16(bytes, lookup_offset + 4);
 432     if (!hasGposBytes(bytes, lookup_offset + 6, @as(usize, subtable_count) * 2)) return error.InvalidGpos;
 433     for (0..subtable_count) |i| {
 434         const subtable_offset = try gposChildOffset(bytes, lookup_offset, try readGposU16(bytes, lookup_offset + 6 + i * 2), 8);
 435         const resolved = try resolveGposSubtable(bytes, lookup_type, subtable_offset);
 436         if (resolved.lookup_type == wanted_lookup_type) return true;
 437     }
 438     return false;
 439 }
 440 
 441 fn lookupPairAdjustmentImpl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, lookup_index: u16, left_glyph: u16, right_glyph: u16) FontError!?GposPairAdjustment {
 442     const lookup_offset = try self.lookupOffset(bytes, lookup_index);
 443 
 444     const lookup_type = try readGposU16(bytes, lookup_offset);
 445     const filter = try gposLookupFilter(bytes, lookup_offset);
 446     if (lookup_type != 2 and lookup_type != 9) return null;
 447     if (try lookupGlyphIgnored(data, gdef, filter, left_glyph)) return null;
 448     if (try lookupGlyphIgnored(data, gdef, filter, right_glyph)) return null;
 449     const subtable_count = try readGposU16(bytes, lookup_offset + 4);
 450     if (!hasGposBytes(bytes, lookup_offset + 6, @as(usize, subtable_count) * 2)) return error.InvalidGpos;
 451 
 452     for (0..subtable_count) |i| {
 453         const subtable_offset = try gposChildOffset(bytes, lookup_offset, try readGposU16(bytes, lookup_offset + 6 + i * 2), 2);
 454         const resolved = try resolveGposSubtable(bytes, lookup_type, subtable_offset);
 455         if (resolved.lookup_type != 2) continue;
 456         if (try pairAdjustmentSubtable(bytes, resolved.subtable_offset, left_glyph, right_glyph)) |adjustment| return adjustment;
 457     }
 458     return null;
 459 }
 460 
 461 fn lookupPairAdjustmentBeforeImpl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, lookup_index: u16, glyph_ids: []const u32, glyph_index: usize) FontError!?GposPairAdjustmentBefore {
 462     const lookup_offset = try self.lookupOffset(bytes, lookup_index);
 463 
 464     const lookup_type = try readGposU16(bytes, lookup_offset);
 465     const filter = try gposLookupFilter(bytes, lookup_offset);
 466     if (lookup_type != 2 and lookup_type != 9) return null;
 467 
 468     const right_glyph: u16 = @intCast(glyph_ids[glyph_index]);
 469     if (try lookupGlyphIgnored(data, gdef, filter, right_glyph)) return null;
 470     const left_index = (try previousLookupGlyphIndex(data, gdef, filter, glyph_ids, glyph_index)) orelse return null;
 471     if (glyph_ids[left_index] > std.math.maxInt(u16)) return null;
 472     const left_glyph: u16 = @intCast(glyph_ids[left_index]);
 473 
 474     const subtable_count = try readGposU16(bytes, lookup_offset + 4);
 475     if (!hasGposBytes(bytes, lookup_offset + 6, @as(usize, subtable_count) * 2)) return error.InvalidGpos;
 476 
 477     for (0..subtable_count) |i| {
 478         const subtable_offset = try gposChildOffset(bytes, lookup_offset, try readGposU16(bytes, lookup_offset + 6 + i * 2), 2);
 479         const resolved = try resolveGposSubtable(bytes, lookup_type, subtable_offset);
 480         if (resolved.lookup_type != 2) continue;
 481         if (try pairAdjustmentSubtable(bytes, resolved.subtable_offset, left_glyph, right_glyph)) |adjustment| {
 482             return .{ .adjustment = adjustment, .left_offset = glyph_index - left_index };
 483         }
 484     }
 485     return null;
 486 }
 487 
 488 fn contextualPairAdjustmentAtImpl(self: Gpos, data: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, gdef: ?Gdef, glyph_ids: []const u32, glyph_index: usize, source: ?FeatureRange) FontError!?GposContextualPairAdjustment {
 489     if (glyph_index >= glyph_ids.len or glyph_ids[glyph_index] > std.math.maxInt(u16)) return null;
 490     const bytes = data[self.table.offset..][0..self.table.len];
 491     const lookup_count = try readGposU16(bytes, self.lookup_list_offset);
 492     for (0..lookup_count) |lookup_index| {
 493         const index: u16 = @intCast(lookup_index);
 494         if (try self.lookupContextualPairAdjustmentAt(data, bytes, face, selection, gdef, index, glyph_ids, glyph_index, source)) |adjustment| return adjustment;
 495     }
 496     return null;
 497 }
 498 
 499 fn contextualPairAdjustmentAtLookupImpl(self: Gpos, data: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, gdef: ?Gdef, lookup_index: u16, glyph_ids: []const u32, glyph_index: usize, source: ?FeatureRange) FontError!?GposContextualPairAdjustment {
 500     if (glyph_index >= glyph_ids.len or glyph_ids[glyph_index] > std.math.maxInt(u16)) return null;
 501     const bytes = data[self.table.offset..][0..self.table.len];
 502     return try self.lookupContextualPairAdjustmentAt(data, bytes, face, selection, gdef, lookup_index, glyph_ids, glyph_index, source);
 503 }
 504 
 505 fn lookupContextualPairAdjustmentAtImpl(self: Gpos, data: []const u8, bytes: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, gdef: ?Gdef, lookup_index: u16, glyph_ids: []const u32, glyph_index: usize, source: ?FeatureRange) FontError!?GposContextualPairAdjustment {
 506     const lookup_offset = try self.lookupOffset(bytes, lookup_index);
 507     if (!try self.lookupIsActive(bytes, face, selection, lookup_index, source)) return null;
 508 
 509     const lookup_type = try readGposU16(bytes, lookup_offset);
 510     const filter = try gposLookupFilter(bytes, lookup_offset);
 511     if (lookup_type != 7 and lookup_type != 8 and lookup_type != 9) return null;
 512     if (filter.flags != 0 or filter.mark_filtering_set != null) return null;
 513     const subtable_count = try readGposU16(bytes, lookup_offset + 4);
 514     if (!hasGposBytes(bytes, lookup_offset + 6, @as(usize, subtable_count) * 2)) return error.InvalidGpos;
 515 
 516     for (0..subtable_count) |i| {
 517         const subtable_offset = try gposChildOffset(bytes, lookup_offset, try readGposU16(bytes, lookup_offset + 6 + i * 2), 2);
 518         const resolved = try resolveGposSubtable(bytes, lookup_type, subtable_offset);
 519         switch (resolved.lookup_type) {
 520             7 => {
 521                 if (try self.contextualPairAdjustment(data, bytes, gdef, resolved.subtable_offset, glyph_ids, glyph_index)) |adjustment| return adjustment;
 522             },
 523             8 => {
 524                 if (try self.chainedContextualPairAdjustment(data, bytes, gdef, resolved.subtable_offset, glyph_ids, glyph_index)) |adjustment| return adjustment;
 525             },
 526             else => continue,
 527         }
 528     }
 529     return null;
 530 }
 531 
 532 fn lookupNestedPairAdjustmentImpl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, lookup_index: u16, glyph_ids: []const u32, glyph_index: usize) FontError!?GposPairAdjustment {
 533     if (glyph_index + 1 >= glyph_ids.len) return null;
 534     if (glyph_ids[glyph_index] > std.math.maxInt(u16) or glyph_ids[glyph_index + 1] > std.math.maxInt(u16)) return null;
 535     return try self.lookupPairAdjustment(data, bytes, gdef, lookup_index, @intCast(glyph_ids[glyph_index]), @intCast(glyph_ids[glyph_index + 1]));
 536 }
 537 
 538 fn contextualPairAdjustmentImpl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposContextualPairAdjustment {
 539     if (glyph_index >= glyph_ids.len) return null;
 540     if (!hasGposBytes(bytes, subtable_offset, 2)) return error.InvalidGpos;
 541     return switch (try readGposU16(bytes, subtable_offset)) {
 542         1 => self.contextualPairAdjustmentFormat1(data, bytes, gdef, subtable_offset, glyph_ids, glyph_index),
 543         2 => self.contextualPairAdjustmentFormat2(data, bytes, gdef, subtable_offset, glyph_ids, glyph_index),
 544         3 => self.contextualPairAdjustmentFormat3(data, bytes, gdef, subtable_offset, glyph_ids, glyph_index),
 545         else => error.InvalidGpos,
 546     };
 547 }
 548 
 549 fn contextualPairAdjustmentFormat1Impl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposContextualPairAdjustment {
 550     if (!hasGposBytes(bytes, subtable_offset, 8)) return error.InvalidGpos;
 551     const coverage_offset = try gposChildOffset(bytes, subtable_offset, try readGposU16(bytes, subtable_offset + 2), 2);
 552     const coverage = layout.Coverage.init(bytes, coverage_offset) catch return error.InvalidGpos;
 553     const coverage_index = (coverage.index(bytes, glyph_ids[glyph_index]) catch return error.InvalidGpos) orelse return null;
 554     const rule_set_count = try readGposU16(bytes, subtable_offset + 4);
 555     if (coverage_index >= rule_set_count) return error.InvalidGpos;
 556     const rule_set_offsets = subtable_offset + 6;
 557     if (!hasGposBytes(bytes, rule_set_offsets, @as(usize, rule_set_count) * 2)) return error.InvalidGpos;
 558     const rule_set_relative = try readGposU16(bytes, rule_set_offsets + @as(usize, coverage_index) * 2);
 559     if (rule_set_relative == 0) return null;
 560     const rule_set_offset = try gposChildOffset(bytes, subtable_offset, rule_set_relative, 2);
 561     return try self.contextualPairAdjustmentRuleSet(data, bytes, gdef, rule_set_offset, glyph_ids, glyph_index);
 562 }
 563 
 564 fn contextualPairAdjustmentFormat2Impl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposContextualPairAdjustment {
 565     if (!hasGposBytes(bytes, subtable_offset, 8)) return error.InvalidGpos;
 566     const coverage_offset = try gposChildOffset(bytes, subtable_offset, try readGposU16(bytes, subtable_offset + 2), 2);
 567     const coverage = layout.Coverage.init(bytes, coverage_offset) catch return error.InvalidGpos;
 568     if ((coverage.index(bytes, glyph_ids[glyph_index]) catch return error.InvalidGpos) == null) return null;
 569     const class_def_offset = try gposChildOffset(bytes, subtable_offset, try readGposU16(bytes, subtable_offset + 4), 2);
 570     const class_def = layout.ClassDef.init(bytes, class_def_offset) catch return error.InvalidGpos;
 571     const rule_set_count = try readGposU16(bytes, subtable_offset + 6);
 572     const rule_set_offsets = subtable_offset + 8;
 573     if (!hasGposBytes(bytes, rule_set_offsets, @as(usize, rule_set_count) * 2)) return error.InvalidGpos;
 574     const first_class = class_def.class(bytes, glyph_ids[glyph_index]) catch return error.InvalidGpos;
 575     if (first_class >= rule_set_count) return null;
 576     const rule_set_relative = try readGposU16(bytes, rule_set_offsets + @as(usize, first_class) * 2);
 577     if (rule_set_relative == 0) return null;
 578     const rule_set_offset = try gposChildOffset(bytes, subtable_offset, rule_set_relative, 2);
 579     return try self.contextualPairAdjustmentClassSet(data, bytes, gdef, class_def, rule_set_offset, glyph_ids, glyph_index);
 580 }
 581 
 582 fn contextualPairAdjustmentFormat3Impl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposContextualPairAdjustment {
 583     if (!hasGposBytes(bytes, subtable_offset, 6)) return error.InvalidGpos;
 584     const glyph_count = try readGposU16(bytes, subtable_offset + 2);
 585     const lookup_count = try readGposU16(bytes, subtable_offset + 4);
 586     if (glyph_count == 0) return error.InvalidGpos;
 587     if (@as(usize, glyph_count) > glyph_ids.len - glyph_index) return null;
 588     const coverage_offsets = subtable_offset + 6;
 589     if (!hasGposBytes(bytes, coverage_offsets, @as(usize, glyph_count) * 2)) return error.InvalidGpos;
 590     for (0..glyph_count) |sequence_index| {
 591         const coverage_offset = try gposChildOffset(bytes, subtable_offset, try readGposU16(bytes, coverage_offsets + sequence_index * 2), 2);
 592         if (!try gposGlyphInCoverage(bytes, coverage_offset, glyph_ids[glyph_index + sequence_index])) return null;
 593     }
 594 
 595     const lookup_records_offset = coverage_offsets + @as(usize, glyph_count) * 2;
 596     return try self.contextualPairAdjustmentRecords(data, bytes, gdef, lookup_records_offset, lookup_count, glyph_ids, glyph_index, glyph_count);
 597 }
 598 
 599 fn chainedContextualPairAdjustmentImpl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposContextualPairAdjustment {
 600     if (glyph_index >= glyph_ids.len) return null;
 601     if (!hasGposBytes(bytes, subtable_offset, 2)) return error.InvalidGpos;
 602     return switch (try readGposU16(bytes, subtable_offset)) {
 603         1 => self.chainedContextualPairAdjustmentFormat1(data, bytes, gdef, subtable_offset, glyph_ids, glyph_index),
 604         2 => self.chainedContextualPairAdjustmentFormat2(data, bytes, gdef, subtable_offset, glyph_ids, glyph_index),
 605         3 => self.chainedContextualPairAdjustmentFormat3(data, bytes, gdef, subtable_offset, glyph_ids, glyph_index),
 606         else => error.InvalidGpos,
 607     };
 608 }
 609 
 610 fn chainedContextualPairAdjustmentFormat1Impl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposContextualPairAdjustment {
 611     const match = (try self.chainedContextualMatchFormat1(bytes, subtable_offset, glyph_ids, glyph_index)) orelse return null;
 612     return try self.contextualPairAdjustmentRecords(data, bytes, gdef, match.lookup_records_offset, match.lookup_count, glyph_ids, glyph_index, match.input_count);
 613 }
 614 
 615 fn chainedContextualPairAdjustmentFormat2Impl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposContextualPairAdjustment {
 616     const match = (try self.chainedContextualMatchFormat2(bytes, subtable_offset, glyph_ids, glyph_index)) orelse return null;
 617     return try self.contextualPairAdjustmentRecords(data, bytes, gdef, match.lookup_records_offset, match.lookup_count, glyph_ids, glyph_index, match.input_count);
 618 }
 619 
 620 fn chainedContextualPairAdjustmentFormat3Impl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposContextualPairAdjustment {
 621     if (!hasGposBytes(bytes, subtable_offset, 4)) return error.InvalidGpos;
 622     const backtrack_count = try readGposU16(bytes, subtable_offset + 2);
 623     const backtrack_offsets = subtable_offset + 4;
 624     if (!hasGposBytes(bytes, backtrack_offsets, @as(usize, backtrack_count) * 2)) return error.InvalidGpos;
 625     if (@as(usize, backtrack_count) > glyph_index) return null;
 626     for (0..backtrack_count) |backtrack_index| {
 627         const coverage_offset = try gposChildOffset(bytes, subtable_offset, try readGposU16(bytes, backtrack_offsets + backtrack_index * 2), 2);
 628         const match_index = glyph_index - backtrack_index - 1;
 629         if (!try gposGlyphInCoverage(bytes, coverage_offset, glyph_ids[match_index])) return null;
 630     }
 631 
 632     const input_count_offset = backtrack_offsets + @as(usize, backtrack_count) * 2;
 633     const input_count = try readGposU16(bytes, input_count_offset);
 634     if (input_count == 0) return error.InvalidGpos;
 635     const input_offsets = input_count_offset + 2;
 636     if (!hasGposBytes(bytes, input_offsets, @as(usize, input_count) * 2)) return error.InvalidGpos;
 637     if (@as(usize, input_count) > glyph_ids.len - glyph_index) return null;
 638     for (0..input_count) |input_index| {
 639         const coverage_offset = try gposChildOffset(bytes, subtable_offset, try readGposU16(bytes, input_offsets + input_index * 2), 2);
 640         if (!try gposGlyphInCoverage(bytes, coverage_offset, glyph_ids[glyph_index + input_index])) return null;
 641     }
 642 
 643     const lookahead_count_offset = input_offsets + @as(usize, input_count) * 2;
 644     const lookahead_count = try readGposU16(bytes, lookahead_count_offset);
 645     const lookahead_offsets = lookahead_count_offset + 2;
 646     if (!hasGposBytes(bytes, lookahead_offsets, @as(usize, lookahead_count) * 2)) return error.InvalidGpos;
 647     const lookahead_start = glyph_index + @as(usize, input_count);
 648     if (@as(usize, lookahead_count) > glyph_ids.len - lookahead_start) return null;
 649     for (0..lookahead_count) |lookahead_index| {
 650         const coverage_offset = try gposChildOffset(bytes, subtable_offset, try readGposU16(bytes, lookahead_offsets + lookahead_index * 2), 2);
 651         if (!try gposGlyphInCoverage(bytes, coverage_offset, glyph_ids[lookahead_start + lookahead_index])) return null;
 652     }
 653 
 654     const lookup_count_offset = lookahead_offsets + @as(usize, lookahead_count) * 2;
 655     const lookup_count = try readGposU16(bytes, lookup_count_offset);
 656     const lookup_records_offset = lookup_count_offset + 2;
 657     return try self.contextualPairAdjustmentRecords(data, bytes, gdef, lookup_records_offset, lookup_count, glyph_ids, glyph_index, input_count);
 658 }
 659 
 660 fn chainedContextualMatchFormat1Impl(self: Gpos, bytes: []const u8, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?ChainedContextMatch {
 661     if (!hasGposBytes(bytes, subtable_offset, 8)) return error.InvalidGpos;
 662     const coverage_offset = try gposChildOffset(bytes, subtable_offset, try readGposU16(bytes, subtable_offset + 2), 2);
 663     const coverage = layout.Coverage.init(bytes, coverage_offset) catch return error.InvalidGpos;
 664     const coverage_index = (coverage.index(bytes, glyph_ids[glyph_index]) catch return error.InvalidGpos) orelse return null;
 665     const rule_set_count = try readGposU16(bytes, subtable_offset + 4);
 666     if (coverage_index >= rule_set_count) return error.InvalidGpos;
 667     const rule_set_offsets = subtable_offset + 6;
 668     if (!hasGposBytes(bytes, rule_set_offsets, @as(usize, rule_set_count) * 2)) return error.InvalidGpos;
 669     const rule_set_relative = try readGposU16(bytes, rule_set_offsets + @as(usize, coverage_index) * 2);
 670     if (rule_set_relative == 0) return null;
 671     const rule_set_offset = try gposChildOffset(bytes, subtable_offset, rule_set_relative, 2);
 672     return try self.chainedContextualRuleSetMatch(bytes, rule_set_offset, glyph_ids, glyph_index);
 673 }
 674 
 675 fn chainedContextualMatchFormat2Impl(self: Gpos, bytes: []const u8, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?ChainedContextMatch {
 676     if (!hasGposBytes(bytes, subtable_offset, 12)) return error.InvalidGpos;
 677     const coverage_offset = try gposChildOffset(bytes, subtable_offset, try readGposU16(bytes, subtable_offset + 2), 2);
 678     const coverage = layout.Coverage.init(bytes, coverage_offset) catch return error.InvalidGpos;
 679     if ((coverage.index(bytes, glyph_ids[glyph_index]) catch return error.InvalidGpos) == null) return null;
 680 
 681     const backtrack_class_def_offset = try gposChildOffset(bytes, subtable_offset, try readGposU16(bytes, subtable_offset + 4), 2);
 682     const input_class_def_offset = try gposChildOffset(bytes, subtable_offset, try readGposU16(bytes, subtable_offset + 6), 2);
 683     const lookahead_class_def_offset = try gposChildOffset(bytes, subtable_offset, try readGposU16(bytes, subtable_offset + 8), 2);
 684     const backtrack_class_def = layout.ClassDef.init(bytes, backtrack_class_def_offset) catch return error.InvalidGpos;
 685     const input_class_def = layout.ClassDef.init(bytes, input_class_def_offset) catch return error.InvalidGpos;
 686     const lookahead_class_def = layout.ClassDef.init(bytes, lookahead_class_def_offset) catch return error.InvalidGpos;
 687 
 688     const rule_set_count = try readGposU16(bytes, subtable_offset + 10);
 689     const rule_set_offsets = subtable_offset + 12;
 690     if (!hasGposBytes(bytes, rule_set_offsets, @as(usize, rule_set_count) * 2)) return error.InvalidGpos;
 691     const first_class = input_class_def.class(bytes, glyph_ids[glyph_index]) catch return error.InvalidGpos;
 692     if (first_class >= rule_set_count) return null;
 693     const rule_set_relative = try readGposU16(bytes, rule_set_offsets + @as(usize, first_class) * 2);
 694     if (rule_set_relative == 0) return null;
 695     const rule_set_offset = try gposChildOffset(bytes, subtable_offset, rule_set_relative, 2);
 696     return try self.chainedContextualClassSetMatch(bytes, backtrack_class_def, input_class_def, lookahead_class_def, rule_set_offset, glyph_ids, glyph_index);
 697 }
 698 
 699 fn chainedContextualRuleSetMatchImpl(self: Gpos, bytes: []const u8, rule_set_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?ChainedContextMatch {
 700     const rule_count = try readGposU16(bytes, rule_set_offset);
 701     const rule_offsets = rule_set_offset + 2;
 702     if (!hasGposBytes(bytes, rule_offsets, @as(usize, rule_count) * 2)) return error.InvalidGpos;
 703     for (0..rule_count) |rule_index| {
 704         const rule_relative = try readGposU16(bytes, rule_offsets + rule_index * 2);
 705         if (rule_relative == 0) continue;
 706         const rule_offset = try gposChildOffset(bytes, rule_set_offset, rule_relative, 2);
 707         if (try self.chainedContextualRuleMatch(bytes, rule_offset, glyph_ids, glyph_index)) |match| return match;
 708     }
 709     return null;
 710 }
 711 
 712 fn chainedContextualRuleMatchImpl(self: Gpos, bytes: []const u8, rule_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?ChainedContextMatch {
 713     _ = self;
 714     const backtrack_count = try readGposU16(bytes, rule_offset);
 715     const backtrack_sequence_offset = rule_offset + 2;
 716     if (!hasGposBytes(bytes, backtrack_sequence_offset, @as(usize, backtrack_count) * 2)) return error.InvalidGpos;
 717     if (@as(usize, backtrack_count) > glyph_index) return null;
 718     for (0..backtrack_count) |backtrack_index| {
 719         const glyph = glyph_ids[glyph_index - backtrack_index - 1];
 720         if (glyph > std.math.maxInt(u16) or glyph != try readGposU16(bytes, backtrack_sequence_offset + backtrack_index * 2)) return null;
 721     }
 722 
 723     const input_count_offset = backtrack_sequence_offset + @as(usize, backtrack_count) * 2;
 724     const input_count = try readGposU16(bytes, input_count_offset);
 725     if (input_count == 0) return error.InvalidGpos;
 726     if (@as(usize, input_count) > glyph_ids.len - glyph_index) return null;
 727     const input_sequence_offset = input_count_offset + 2;
 728     const input_sequence_len = @as(usize, input_count - 1) * 2;
 729     if (!hasGposBytes(bytes, input_sequence_offset, input_sequence_len)) return error.InvalidGpos;
 730     for (1..input_count) |input_index| {
 731         const glyph = glyph_ids[glyph_index + input_index];
 732         if (glyph > std.math.maxInt(u16) or glyph != try readGposU16(bytes, input_sequence_offset + (input_index - 1) * 2)) return null;
 733     }
 734 
 735     const lookahead_count_offset = input_sequence_offset + input_sequence_len;
 736     const lookahead_count = try readGposU16(bytes, lookahead_count_offset);
 737     const lookahead_sequence_offset = lookahead_count_offset + 2;
 738     if (!hasGposBytes(bytes, lookahead_sequence_offset, @as(usize, lookahead_count) * 2)) return error.InvalidGpos;
 739     const lookahead_start = glyph_index + @as(usize, input_count);
 740     if (@as(usize, lookahead_count) > glyph_ids.len - lookahead_start) return null;
 741     for (0..lookahead_count) |lookahead_index| {
 742         const glyph = glyph_ids[lookahead_start + lookahead_index];
 743         if (glyph > std.math.maxInt(u16) or glyph != try readGposU16(bytes, lookahead_sequence_offset + lookahead_index * 2)) return null;
 744     }
 745 
 746     const lookup_count_offset = lookahead_sequence_offset + @as(usize, lookahead_count) * 2;
 747     const lookup_count = try readGposU16(bytes, lookup_count_offset);
 748     const lookup_records_offset = lookup_count_offset + 2;
 749     if (!hasGposBytes(bytes, lookup_records_offset, @as(usize, lookup_count) * 4)) return error.InvalidGpos;
 750     return .{
 751         .lookup_records_offset = lookup_records_offset,
 752         .lookup_count = lookup_count,
 753         .input_count = input_count,
 754     };
 755 }
 756 
 757 fn chainedContextualClassSetMatchImpl(
 758     self: Gpos,
 759     bytes: []const u8,
 760     backtrack_class_def: layout.ClassDef,
 761     input_class_def: layout.ClassDef,
 762     lookahead_class_def: layout.ClassDef,
 763     rule_set_offset: usize,
 764     glyph_ids: []const u32,
 765     glyph_index: usize,
 766 ) FontError!?ChainedContextMatch {
 767     const rule_count = try readGposU16(bytes, rule_set_offset);
 768     const rule_offsets = rule_set_offset + 2;
 769     if (!hasGposBytes(bytes, rule_offsets, @as(usize, rule_count) * 2)) return error.InvalidGpos;
 770     for (0..rule_count) |rule_index| {
 771         const rule_relative = try readGposU16(bytes, rule_offsets + rule_index * 2);
 772         if (rule_relative == 0) continue;
 773         const rule_offset = try gposChildOffset(bytes, rule_set_offset, rule_relative, 2);
 774         if (try self.chainedContextualClassRuleMatch(bytes, backtrack_class_def, input_class_def, lookahead_class_def, rule_offset, glyph_ids, glyph_index)) |match| return match;
 775     }
 776     return null;
 777 }
 778 
 779 fn chainedContextualClassRuleMatchImpl(
 780     self: Gpos,
 781     bytes: []const u8,
 782     backtrack_class_def: layout.ClassDef,
 783     input_class_def: layout.ClassDef,
 784     lookahead_class_def: layout.ClassDef,
 785     rule_offset: usize,
 786     glyph_ids: []const u32,
 787     glyph_index: usize,
 788 ) FontError!?ChainedContextMatch {
 789     _ = self;
 790     const backtrack_count = try readGposU16(bytes, rule_offset);
 791     const backtrack_sequence_offset = rule_offset + 2;
 792     if (!hasGposBytes(bytes, backtrack_sequence_offset, @as(usize, backtrack_count) * 2)) return error.InvalidGpos;
 793     if (@as(usize, backtrack_count) > glyph_index) return null;
 794     for (0..backtrack_count) |backtrack_index| {
 795         const glyph = glyph_ids[glyph_index - backtrack_index - 1];
 796         if (glyph > std.math.maxInt(u16)) return null;
 797         const expected_class = try readGposU16(bytes, backtrack_sequence_offset + backtrack_index * 2);
 798         const actual_class = backtrack_class_def.class(bytes, glyph) catch return error.InvalidGpos;
 799         if (actual_class != expected_class) return null;
 800     }
 801 
 802     const input_count_offset = backtrack_sequence_offset + @as(usize, backtrack_count) * 2;
 803     const input_count = try readGposU16(bytes, input_count_offset);
 804     if (input_count == 0) return error.InvalidGpos;
 805     if (@as(usize, input_count) > glyph_ids.len - glyph_index) return null;
 806     const input_sequence_offset = input_count_offset + 2;
 807     const input_sequence_len = @as(usize, input_count - 1) * 2;
 808     if (!hasGposBytes(bytes, input_sequence_offset, input_sequence_len)) return error.InvalidGpos;
 809     for (1..input_count) |input_index| {
 810         const glyph = glyph_ids[glyph_index + input_index];
 811         if (glyph > std.math.maxInt(u16)) return null;
 812         const expected_class = try readGposU16(bytes, input_sequence_offset + (input_index - 1) * 2);
 813         const actual_class = input_class_def.class(bytes, glyph) catch return error.InvalidGpos;
 814         if (actual_class != expected_class) return null;
 815     }
 816 
 817     const lookahead_count_offset = input_sequence_offset + input_sequence_len;
 818     const lookahead_count = try readGposU16(bytes, lookahead_count_offset);
 819     const lookahead_sequence_offset = lookahead_count_offset + 2;
 820     if (!hasGposBytes(bytes, lookahead_sequence_offset, @as(usize, lookahead_count) * 2)) return error.InvalidGpos;
 821     const lookahead_start = glyph_index + @as(usize, input_count);
 822     if (@as(usize, lookahead_count) > glyph_ids.len - lookahead_start) return null;
 823     for (0..lookahead_count) |lookahead_index| {
 824         const glyph = glyph_ids[lookahead_start + lookahead_index];
 825         if (glyph > std.math.maxInt(u16)) return null;
 826         const expected_class = try readGposU16(bytes, lookahead_sequence_offset + lookahead_index * 2);
 827         const actual_class = lookahead_class_def.class(bytes, glyph) catch return error.InvalidGpos;
 828         if (actual_class != expected_class) return null;
 829     }
 830 
 831     const lookup_count_offset = lookahead_sequence_offset + @as(usize, lookahead_count) * 2;
 832     const lookup_count = try readGposU16(bytes, lookup_count_offset);
 833     const lookup_records_offset = lookup_count_offset + 2;
 834     if (!hasGposBytes(bytes, lookup_records_offset, @as(usize, lookup_count) * 4)) return error.InvalidGpos;
 835     return .{
 836         .lookup_records_offset = lookup_records_offset,
 837         .lookup_count = lookup_count,
 838         .input_count = input_count,
 839     };
 840 }
 841 
 842 fn contextualPairAdjustmentRuleSetImpl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, rule_set_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposContextualPairAdjustment {
 843     const rule_count = try readGposU16(bytes, rule_set_offset);
 844     const rule_offsets = rule_set_offset + 2;
 845     if (!hasGposBytes(bytes, rule_offsets, @as(usize, rule_count) * 2)) return error.InvalidGpos;
 846     for (0..rule_count) |rule_index| {
 847         const rule_relative = try readGposU16(bytes, rule_offsets + rule_index * 2);
 848         if (rule_relative == 0) continue;
 849         const rule_offset = try gposChildOffset(bytes, rule_set_offset, rule_relative, 4);
 850         if (try self.contextualPairAdjustmentRule(data, bytes, gdef, rule_offset, glyph_ids, glyph_index)) |adjustment| return adjustment;
 851     }
 852     return null;
 853 }
 854 
 855 fn contextualPairAdjustmentClassSetImpl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, class_def: layout.ClassDef, rule_set_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposContextualPairAdjustment {
 856     const rule_count = try readGposU16(bytes, rule_set_offset);
 857     const rule_offsets = rule_set_offset + 2;
 858     if (!hasGposBytes(bytes, rule_offsets, @as(usize, rule_count) * 2)) return error.InvalidGpos;
 859     for (0..rule_count) |rule_index| {
 860         const rule_relative = try readGposU16(bytes, rule_offsets + rule_index * 2);
 861         if (rule_relative == 0) continue;
 862         const rule_offset = try gposChildOffset(bytes, rule_set_offset, rule_relative, 4);
 863         if (try self.contextualPairAdjustmentClassRule(data, bytes, gdef, class_def, rule_offset, glyph_ids, glyph_index)) |adjustment| return adjustment;
 864     }
 865     return null;
 866 }
 867 
 868 fn contextualPairAdjustmentClassRuleImpl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, class_def: layout.ClassDef, rule_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposContextualPairAdjustment {
 869     const glyph_count = try readGposU16(bytes, rule_offset);
 870     const lookup_count = try readGposU16(bytes, rule_offset + 2);
 871     if (glyph_count == 0) return error.InvalidGpos;
 872     if (@as(usize, glyph_count) > glyph_ids.len - glyph_index) return null;
 873     const input_sequence_offset = rule_offset + 4;
 874     const input_sequence_len = @as(usize, glyph_count - 1) * 2;
 875     if (!hasGposBytes(bytes, input_sequence_offset, input_sequence_len)) return error.InvalidGpos;
 876     for (1..glyph_count) |sequence_index| {
 877         const glyph = glyph_ids[glyph_index + sequence_index];
 878         if (glyph > std.math.maxInt(u16)) return null;
 879         const expected_class = try readGposU16(bytes, input_sequence_offset + (sequence_index - 1) * 2);
 880         const actual_class = class_def.class(bytes, glyph) catch return error.InvalidGpos;
 881         if (actual_class != expected_class) return null;
 882     }
 883 
 884     const lookup_records_offset = input_sequence_offset + input_sequence_len;
 885     return try self.contextualPairAdjustmentRecords(data, bytes, gdef, lookup_records_offset, lookup_count, glyph_ids, glyph_index, glyph_count);
 886 }
 887 
 888 fn contextualPairAdjustmentRuleImpl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, rule_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposContextualPairAdjustment {
 889     const glyph_count = try readGposU16(bytes, rule_offset);
 890     const lookup_count = try readGposU16(bytes, rule_offset + 2);
 891     if (glyph_count == 0) return error.InvalidGpos;
 892     if (@as(usize, glyph_count) > glyph_ids.len - glyph_index) return null;
 893     const input_sequence_offset = rule_offset + 4;
 894     const input_sequence_len = @as(usize, glyph_count - 1) * 2;
 895     if (!hasGposBytes(bytes, input_sequence_offset, input_sequence_len)) return error.InvalidGpos;
 896     for (1..glyph_count) |sequence_index| {
 897         const glyph = glyph_ids[glyph_index + sequence_index];
 898         if (glyph > std.math.maxInt(u16) or glyph != try readGposU16(bytes, input_sequence_offset + (sequence_index - 1) * 2)) return null;
 899     }
 900 
 901     const lookup_records_offset = input_sequence_offset + input_sequence_len;
 902     return try self.contextualPairAdjustmentRecords(data, bytes, gdef, lookup_records_offset, lookup_count, glyph_ids, glyph_index, glyph_count);
 903 }
 904 
 905 fn contextualPairAdjustmentRecordsImpl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, lookup_records_offset: usize, lookup_count: u16, glyph_ids: []const u32, glyph_index: usize, input_count: u16) FontError!?GposContextualPairAdjustment {
 906     if (!hasGposBytes(bytes, lookup_records_offset, @as(usize, lookup_count) * 4)) return error.InvalidGpos;
 907     for (0..lookup_count) |lookup_record_index| {
 908         const record_offset = lookup_records_offset + lookup_record_index * 4;
 909         const sequence_index = try readGposU16(bytes, record_offset);
 910         const nested_lookup_index = try readGposU16(bytes, record_offset + 2);
 911         if (sequence_index >= input_count or sequence_index + 1 >= input_count) return error.InvalidGpos;
 912         const nested_glyph_index = glyph_index + @as(usize, sequence_index);
 913         const adjustment = (try self.lookupNestedPairAdjustment(data, bytes, gdef, nested_lookup_index, glyph_ids, nested_glyph_index)) orelse continue;
 914         return .{
 915             .adjustment = adjustment,
 916             .target_offset = @intCast(sequence_index),
 917             .skip_count = if (adjustment.right.isZero()) @as(usize, sequence_index) + 1 else input_count,
 918         };
 919     }
 920     return null;
 921 }
 922 
 923 fn lookupSingleAdjustmentImpl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, lookup_index: u16, glyph_id: u16) FontError!?GposValueAdjustment {
 924     var glyphs = [_]u32{glyph_id};
 925     const adjustment = (try self.lookupSingleAdjustmentAt(data, bytes, null, .{}, gdef, lookup_index, &glyphs, 0, null, false, false)) orelse return null;
 926     if (adjustment.target_offset != 0) return null;
 927     return adjustment.adjustment;
 928 }
 929 
 930 fn lookupSingleAdjustmentAtImpl(self: Gpos, data: []const u8, bytes: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, gdef: ?Gdef, lookup_index: u16, glyph_ids: []const u32, glyph_index: usize, source: ?FeatureRange, require_active: bool, allow_context: bool) FontError!?GposSingleAdjustment {
 931     if (glyph_index >= glyph_ids.len or glyph_ids[glyph_index] > std.math.maxInt(u16)) return null;
 932     const lookup_offset = try self.lookupOffset(bytes, lookup_index);
 933     if (require_active and !try self.lookupIsActive(bytes, face, selection, lookup_index, source)) return null;
 934 
 935     const lookup_type = try readGposU16(bytes, lookup_offset);
 936     const filter = try gposLookupFilter(bytes, lookup_offset);
 937     if (lookup_type != 1 and lookup_type != 7 and lookup_type != 8 and lookup_type != 9) return null;
 938     const glyph_id: u16 = @intCast(glyph_ids[glyph_index]);
 939     if (try lookupGlyphIgnored(data, gdef, filter, glyph_id)) return null;
 940     const subtable_count = try readGposU16(bytes, lookup_offset + 4);
 941     if (!hasGposBytes(bytes, lookup_offset + 6, @as(usize, subtable_count) * 2)) return error.InvalidGpos;
 942 
 943     for (0..subtable_count) |i| {
 944         const subtable_offset = try gposChildOffset(bytes, lookup_offset, try readGposU16(bytes, lookup_offset + 6 + i * 2), 2);
 945         const resolved = try resolveGposSubtable(bytes, lookup_type, subtable_offset);
 946         switch (resolved.lookup_type) {
 947             1 => {
 948                 if (try singleAdjustmentSubtable(bytes, resolved.subtable_offset, glyph_id)) |adjustment| return .{ .adjustment = adjustment };
 949             },
 950             7 => {
 951                 if (!allow_context) return null;
 952                 if (filter.flags != 0 or filter.mark_filtering_set != null) return null;
 953                 if (try self.contextualSingleAdjustment(data, bytes, selection, gdef, resolved.subtable_offset, glyph_ids, glyph_index)) |adjustment| return adjustment;
 954             },
 955             8 => {
 956                 if (!allow_context) return null;
 957                 if (filter.flags != 0 or filter.mark_filtering_set != null) return null;
 958                 if (try self.chainedContextualSingleAdjustment(data, bytes, selection, gdef, resolved.subtable_offset, glyph_ids, glyph_index)) |adjustment| return adjustment;
 959             },
 960             else => continue,
 961         }
 962     }
 963     return null;
 964 }
 965 
 966 fn contextualSingleAdjustmentImpl(self: Gpos, data: []const u8, bytes: []const u8, selection: LayoutSelection, gdef: ?Gdef, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposSingleAdjustment {
 967     if (glyph_index >= glyph_ids.len) return null;
 968     if (!hasGposBytes(bytes, subtable_offset, 2)) return error.InvalidGpos;
 969     return switch (try readGposU16(bytes, subtable_offset)) {
 970         1 => self.contextualSingleAdjustmentFormat1(data, bytes, selection, gdef, subtable_offset, glyph_ids, glyph_index),
 971         2 => self.contextualSingleAdjustmentFormat2(data, bytes, selection, gdef, subtable_offset, glyph_ids, glyph_index),
 972         3 => self.contextualSingleAdjustmentFormat3(data, bytes, selection, gdef, subtable_offset, glyph_ids, glyph_index),
 973         else => error.InvalidGpos,
 974     };
 975 }
 976 
 977 fn contextualSingleAdjustmentFormat1Impl(self: Gpos, data: []const u8, bytes: []const u8, selection: LayoutSelection, gdef: ?Gdef, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposSingleAdjustment {
 978     if (!hasGposBytes(bytes, subtable_offset, 8)) return error.InvalidGpos;
 979     const coverage_offset = try gposChildOffset(bytes, subtable_offset, try readGposU16(bytes, subtable_offset + 2), 2);
 980     const coverage = layout.Coverage.init(bytes, coverage_offset) catch return error.InvalidGpos;
 981     const coverage_index = (coverage.index(bytes, glyph_ids[glyph_index]) catch return error.InvalidGpos) orelse return null;
 982     const rule_set_count = try readGposU16(bytes, subtable_offset + 4);
 983     if (coverage_index >= rule_set_count) return error.InvalidGpos;
 984     const rule_set_offsets = subtable_offset + 6;
 985     if (!hasGposBytes(bytes, rule_set_offsets, @as(usize, rule_set_count) * 2)) return error.InvalidGpos;
 986     const rule_set_relative = try readGposU16(bytes, rule_set_offsets + @as(usize, coverage_index) * 2);
 987     if (rule_set_relative == 0) return null;
 988     const rule_set_offset = try gposChildOffset(bytes, subtable_offset, rule_set_relative, 2);
 989     return try self.contextualSingleAdjustmentRuleSet(data, bytes, selection, gdef, rule_set_offset, glyph_ids, glyph_index);
 990 }
 991 
 992 fn contextualSingleAdjustmentFormat2Impl(self: Gpos, data: []const u8, bytes: []const u8, selection: LayoutSelection, gdef: ?Gdef, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposSingleAdjustment {
 993     if (!hasGposBytes(bytes, subtable_offset, 8)) return error.InvalidGpos;
 994     const coverage_offset = try gposChildOffset(bytes, subtable_offset, try readGposU16(bytes, subtable_offset + 2), 2);
 995     const coverage = layout.Coverage.init(bytes, coverage_offset) catch return error.InvalidGpos;
 996     if ((coverage.index(bytes, glyph_ids[glyph_index]) catch return error.InvalidGpos) == null) return null;
 997     const class_def_offset = try gposChildOffset(bytes, subtable_offset, try readGposU16(bytes, subtable_offset + 4), 2);
 998     const class_def = layout.ClassDef.init(bytes, class_def_offset) catch return error.InvalidGpos;
 999     const rule_set_count = try readGposU16(bytes, subtable_offset + 6);
1000     const rule_set_offsets = subtable_offset + 8;
1001     if (!hasGposBytes(bytes, rule_set_offsets, @as(usize, rule_set_count) * 2)) return error.InvalidGpos;
1002     const first_class = class_def.class(bytes, glyph_ids[glyph_index]) catch return error.InvalidGpos;
1003     if (first_class >= rule_set_count) return null;
1004     const rule_set_relative = try readGposU16(bytes, rule_set_offsets + @as(usize, first_class) * 2);
1005     if (rule_set_relative == 0) return null;
1006     const rule_set_offset = try gposChildOffset(bytes, subtable_offset, rule_set_relative, 2);
1007     return try self.contextualSingleAdjustmentClassSet(data, bytes, selection, gdef, class_def, rule_set_offset, glyph_ids, glyph_index);
1008 }
1009 
1010 fn contextualSingleAdjustmentFormat3Impl(self: Gpos, data: []const u8, bytes: []const u8, selection: LayoutSelection, gdef: ?Gdef, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposSingleAdjustment {
1011     if (!hasGposBytes(bytes, subtable_offset, 6)) return error.InvalidGpos;
1012     const glyph_count = try readGposU16(bytes, subtable_offset + 2);
1013     const lookup_count = try readGposU16(bytes, subtable_offset + 4);
1014     if (glyph_count == 0) return error.InvalidGpos;
1015     if (@as(usize, glyph_count) > glyph_ids.len - glyph_index) return null;
1016     const coverage_offsets = subtable_offset + 6;
1017     if (!hasGposBytes(bytes, coverage_offsets, @as(usize, glyph_count) * 2)) return error.InvalidGpos;
1018     for (0..glyph_count) |sequence_index| {
1019         const coverage_offset = try gposChildOffset(bytes, subtable_offset, try readGposU16(bytes, coverage_offsets + sequence_index * 2), 2);
1020         if (!try gposGlyphInCoverage(bytes, coverage_offset, glyph_ids[glyph_index + sequence_index])) return null;
1021     }
1022 
1023     const lookup_records_offset = coverage_offsets + @as(usize, glyph_count) * 2;
1024     return try self.contextualSingleAdjustmentRecords(data, bytes, selection, gdef, lookup_records_offset, lookup_count, glyph_ids, glyph_index, glyph_count);
1025 }
1026 
1027 fn chainedContextualSingleAdjustmentImpl(self: Gpos, data: []const u8, bytes: []const u8, selection: LayoutSelection, gdef: ?Gdef, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposSingleAdjustment {
1028     if (glyph_index >= glyph_ids.len) return null;
1029     if (!hasGposBytes(bytes, subtable_offset, 2)) return error.InvalidGpos;
1030     return switch (try readGposU16(bytes, subtable_offset)) {
1031         1 => self.chainedContextualSingleAdjustmentFormat1(data, bytes, selection, gdef, subtable_offset, glyph_ids, glyph_index),
1032         2 => self.chainedContextualSingleAdjustmentFormat2(data, bytes, selection, gdef, subtable_offset, glyph_ids, glyph_index),
1033         3 => self.chainedContextualSingleAdjustmentFormat3(data, bytes, selection, gdef, subtable_offset, glyph_ids, glyph_index),
1034         else => error.InvalidGpos,
1035     };
1036 }
1037 
1038 fn chainedContextualSingleAdjustmentFormat1Impl(self: Gpos, data: []const u8, bytes: []const u8, selection: LayoutSelection, gdef: ?Gdef, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposSingleAdjustment {
1039     const match = (try self.chainedContextualMatchFormat1(bytes, subtable_offset, glyph_ids, glyph_index)) orelse return null;
1040     return try self.contextualSingleAdjustmentRecords(data, bytes, selection, gdef, match.lookup_records_offset, match.lookup_count, glyph_ids, glyph_index, match.input_count);
1041 }
1042 
1043 fn chainedContextualSingleAdjustmentFormat2Impl(self: Gpos, data: []const u8, bytes: []const u8, selection: LayoutSelection, gdef: ?Gdef, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposSingleAdjustment {
1044     const match = (try self.chainedContextualMatchFormat2(bytes, subtable_offset, glyph_ids, glyph_index)) orelse return null;
1045     return try self.contextualSingleAdjustmentRecords(data, bytes, selection, gdef, match.lookup_records_offset, match.lookup_count, glyph_ids, glyph_index, match.input_count);
1046 }
1047 
1048 fn chainedContextualSingleAdjustmentFormat3Impl(self: Gpos, data: []const u8, bytes: []const u8, selection: LayoutSelection, gdef: ?Gdef, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposSingleAdjustment {
1049     if (!hasGposBytes(bytes, subtable_offset, 4)) return error.InvalidGpos;
1050     const backtrack_count = try readGposU16(bytes, subtable_offset + 2);
1051     const backtrack_offsets = subtable_offset + 4;
1052     if (!hasGposBytes(bytes, backtrack_offsets, @as(usize, backtrack_count) * 2)) return error.InvalidGpos;
1053     if (@as(usize, backtrack_count) > glyph_index) return null;
1054     for (0..backtrack_count) |backtrack_index| {
1055         const coverage_offset = try gposChildOffset(bytes, subtable_offset, try readGposU16(bytes, backtrack_offsets + backtrack_index * 2), 2);
1056         const match_index = glyph_index - backtrack_index - 1;
1057         if (!try gposGlyphInCoverage(bytes, coverage_offset, glyph_ids[match_index])) return null;
1058     }
1059 
1060     const input_count_offset = backtrack_offsets + @as(usize, backtrack_count) * 2;
1061     const input_count = try readGposU16(bytes, input_count_offset);
1062     if (input_count == 0) return error.InvalidGpos;
1063     const input_offsets = input_count_offset + 2;
1064     if (!hasGposBytes(bytes, input_offsets, @as(usize, input_count) * 2)) return error.InvalidGpos;
1065     if (@as(usize, input_count) > glyph_ids.len - glyph_index) return null;
1066     for (0..input_count) |input_index| {
1067         const coverage_offset = try gposChildOffset(bytes, subtable_offset, try readGposU16(bytes, input_offsets + input_index * 2), 2);
1068         if (!try gposGlyphInCoverage(bytes, coverage_offset, glyph_ids[glyph_index + input_index])) return null;
1069     }
1070 
1071     const lookahead_count_offset = input_offsets + @as(usize, input_count) * 2;
1072     const lookahead_count = try readGposU16(bytes, lookahead_count_offset);
1073     const lookahead_offsets = lookahead_count_offset + 2;
1074     if (!hasGposBytes(bytes, lookahead_offsets, @as(usize, lookahead_count) * 2)) return error.InvalidGpos;
1075     const lookahead_start = glyph_index + @as(usize, input_count);
1076     if (@as(usize, lookahead_count) > glyph_ids.len - lookahead_start) return null;
1077     for (0..lookahead_count) |lookahead_index| {
1078         const coverage_offset = try gposChildOffset(bytes, subtable_offset, try readGposU16(bytes, lookahead_offsets + lookahead_index * 2), 2);
1079         if (!try gposGlyphInCoverage(bytes, coverage_offset, glyph_ids[lookahead_start + lookahead_index])) return null;
1080     }
1081 
1082     const lookup_count_offset = lookahead_offsets + @as(usize, lookahead_count) * 2;
1083     const lookup_count = try readGposU16(bytes, lookup_count_offset);
1084     const lookup_records_offset = lookup_count_offset + 2;
1085     return try self.contextualSingleAdjustmentRecords(data, bytes, selection, gdef, lookup_records_offset, lookup_count, glyph_ids, glyph_index, input_count);
1086 }
1087 
1088 fn contextualSingleAdjustmentRuleSetImpl(self: Gpos, data: []const u8, bytes: []const u8, selection: LayoutSelection, gdef: ?Gdef, rule_set_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposSingleAdjustment {
1089     const rule_count = try readGposU16(bytes, rule_set_offset);
1090     const rule_offsets = rule_set_offset + 2;
1091     if (!hasGposBytes(bytes, rule_offsets, @as(usize, rule_count) * 2)) return error.InvalidGpos;
1092     for (0..rule_count) |rule_index| {
1093         const rule_relative = try readGposU16(bytes, rule_offsets + rule_index * 2);
1094         if (rule_relative == 0) continue;
1095         const rule_offset = try gposChildOffset(bytes, rule_set_offset, rule_relative, 4);
1096         if (try self.contextualSingleAdjustmentRule(data, bytes, selection, gdef, rule_offset, glyph_ids, glyph_index)) |adjustment| return adjustment;
1097     }
1098     return null;
1099 }
1100 
1101 fn contextualSingleAdjustmentClassSetImpl(self: Gpos, data: []const u8, bytes: []const u8, selection: LayoutSelection, gdef: ?Gdef, class_def: layout.ClassDef, rule_set_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposSingleAdjustment {
1102     const rule_count = try readGposU16(bytes, rule_set_offset);
1103     const rule_offsets = rule_set_offset + 2;
1104     if (!hasGposBytes(bytes, rule_offsets, @as(usize, rule_count) * 2)) return error.InvalidGpos;
1105     for (0..rule_count) |rule_index| {
1106         const rule_relative = try readGposU16(bytes, rule_offsets + rule_index * 2);
1107         if (rule_relative == 0) continue;
1108         const rule_offset = try gposChildOffset(bytes, rule_set_offset, rule_relative, 4);
1109         if (try self.contextualSingleAdjustmentClassRule(data, bytes, selection, gdef, class_def, rule_offset, glyph_ids, glyph_index)) |adjustment| return adjustment;
1110     }
1111     return null;
1112 }
1113 
1114 fn contextualSingleAdjustmentClassRuleImpl(self: Gpos, data: []const u8, bytes: []const u8, selection: LayoutSelection, gdef: ?Gdef, class_def: layout.ClassDef, rule_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposSingleAdjustment {
1115     const glyph_count = try readGposU16(bytes, rule_offset);
1116     const lookup_count = try readGposU16(bytes, rule_offset + 2);
1117     if (glyph_count == 0) return error.InvalidGpos;
1118     if (@as(usize, glyph_count) > glyph_ids.len - glyph_index) return null;
1119     const input_sequence_offset = rule_offset + 4;
1120     const input_sequence_len = @as(usize, glyph_count - 1) * 2;
1121     if (!hasGposBytes(bytes, input_sequence_offset, input_sequence_len)) return error.InvalidGpos;
1122     for (1..glyph_count) |sequence_index| {
1123         const glyph = glyph_ids[glyph_index + sequence_index];
1124         if (glyph > std.math.maxInt(u16)) return null;
1125         const expected_class = try readGposU16(bytes, input_sequence_offset + (sequence_index - 1) * 2);
1126         const actual_class = class_def.class(bytes, glyph) catch return error.InvalidGpos;
1127         if (actual_class != expected_class) return null;
1128     }
1129 
1130     const lookup_records_offset = input_sequence_offset + input_sequence_len;
1131     return try self.contextualSingleAdjustmentRecords(data, bytes, selection, gdef, lookup_records_offset, lookup_count, glyph_ids, glyph_index, glyph_count);
1132 }
1133 
1134 fn contextualSingleAdjustmentRuleImpl(self: Gpos, data: []const u8, bytes: []const u8, selection: LayoutSelection, gdef: ?Gdef, rule_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GposSingleAdjustment {
1135     const glyph_count = try readGposU16(bytes, rule_offset);
1136     const lookup_count = try readGposU16(bytes, rule_offset + 2);
1137     if (glyph_count == 0) return error.InvalidGpos;
1138     if (@as(usize, glyph_count) > glyph_ids.len - glyph_index) return null;
1139     const input_sequence_offset = rule_offset + 4;
1140     const input_sequence_len = @as(usize, glyph_count - 1) * 2;
1141     if (!hasGposBytes(bytes, input_sequence_offset, input_sequence_len)) return error.InvalidGpos;
1142     for (1..glyph_count) |sequence_index| {
1143         const glyph = glyph_ids[glyph_index + sequence_index];
1144         if (glyph > std.math.maxInt(u16) or glyph != try readGposU16(bytes, input_sequence_offset + (sequence_index - 1) * 2)) return null;
1145     }
1146 
1147     const lookup_records_offset = input_sequence_offset + input_sequence_len;
1148     return try self.contextualSingleAdjustmentRecords(data, bytes, selection, gdef, lookup_records_offset, lookup_count, glyph_ids, glyph_index, glyph_count);
1149 }
1150 
1151 fn contextualSingleAdjustmentRecordsImpl(self: Gpos, data: []const u8, bytes: []const u8, selection: LayoutSelection, gdef: ?Gdef, lookup_records_offset: usize, lookup_count: u16, glyph_ids: []const u32, glyph_index: usize, input_count: u16) FontError!?GposSingleAdjustment {
1152     if (!hasGposBytes(bytes, lookup_records_offset, @as(usize, lookup_count) * 4)) return error.InvalidGpos;
1153     for (0..lookup_count) |lookup_record_index| {
1154         const record_offset = lookup_records_offset + lookup_record_index * 4;
1155         const sequence_index = try readGposU16(bytes, record_offset);
1156         const nested_lookup_index = try readGposU16(bytes, record_offset + 2);
1157         if (sequence_index >= input_count) return error.InvalidGpos;
1158         const nested_glyph_index = glyph_index + @as(usize, sequence_index);
1159         var adjustment = (try self.lookupSingleAdjustmentAt(data, bytes, null, selection, gdef, nested_lookup_index, glyph_ids, nested_glyph_index, null, false, false)) orelse continue;
1160         adjustment.target_offset = @intCast(sequence_index);
1161         adjustment.skip_count = input_count;
1162         return adjustment;
1163     }
1164     return null;
1165 }
1166 
1167 fn lookupCursiveAttachmentImpl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, lookup_index: u16, exit_glyph: u16, entry_glyph: u16) FontError!?GposCursiveAttachment {
1168     const lookup_offset = try self.lookupOffset(bytes, lookup_index);
1169 
1170     const lookup_type = try readGposU16(bytes, lookup_offset);
1171     const filter = try gposLookupFilter(bytes, lookup_offset);
1172     if (lookup_type != 3 and lookup_type != 9) return null;
1173     if (try lookupGlyphIgnored(data, gdef, filter, exit_glyph)) return null;
1174     if (try lookupGlyphIgnored(data, gdef, filter, entry_glyph)) return null;
1175     const subtable_count = try readGposU16(bytes, lookup_offset + 4);
1176     if (!hasGposBytes(bytes, lookup_offset + 6, @as(usize, subtable_count) * 2)) return error.InvalidGpos;
1177 
1178     for (0..subtable_count) |i| {
1179         const subtable_offset = try gposChildOffset(bytes, lookup_offset, try readGposU16(bytes, lookup_offset + 6 + i * 2), 2);
1180         const resolved = try resolveGposSubtable(bytes, lookup_type, subtable_offset);
1181         if (resolved.lookup_type != 3) continue;
1182         if (try cursiveAttachmentSubtable(bytes, resolved.subtable_offset, exit_glyph, entry_glyph)) |attachment| return attachment;
1183     }
1184     return null;
1185 }
1186 
1187 fn lookupMarkToBaseAdjustmentImpl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, lookup_index: u16, base_glyph: u16, mark_glyph: u16) FontError!?GposValueAdjustment {
1188     const lookup_offset = try self.lookupOffset(bytes, lookup_index);
1189 
1190     const lookup_type = try readGposU16(bytes, lookup_offset);
1191     const filter = try gposLookupFilter(bytes, lookup_offset);
1192     if (lookup_type != 4 and lookup_type != 9) return null;
1193     if (try lookupGlyphIgnored(data, gdef, filter, base_glyph)) return null;
1194     if (try lookupGlyphIgnored(data, gdef, filter, mark_glyph)) return null;
1195     const subtable_count = try readGposU16(bytes, lookup_offset + 4);
1196     if (!hasGposBytes(bytes, lookup_offset + 6, @as(usize, subtable_count) * 2)) return error.InvalidGpos;
1197 
1198     for (0..subtable_count) |i| {
1199         const subtable_offset = try gposChildOffset(bytes, lookup_offset, try readGposU16(bytes, lookup_offset + 6 + i * 2), 2);
1200         const resolved = try resolveGposSubtable(bytes, lookup_type, subtable_offset);
1201         if (resolved.lookup_type != 4) continue;
1202         if (try markToBaseAdjustmentSubtable(bytes, resolved.subtable_offset, base_glyph, mark_glyph)) |adjustment| return adjustment;
1203     }
1204     return null;
1205 }
1206 
1207 fn lookupMarkToLigatureAdjustmentImpl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, lookup_index: u16, ligature_glyph: u16, mark_glyph: u16, component_index: u16) FontError!?GposValueAdjustment {
1208     const lookup_offset = try self.lookupOffset(bytes, lookup_index);
1209 
1210     const lookup_type = try readGposU16(bytes, lookup_offset);
1211     const filter = try gposLookupFilter(bytes, lookup_offset);
1212     if (lookup_type != 5 and lookup_type != 9) return null;
1213     if (try lookupGlyphIgnored(data, gdef, filter, ligature_glyph)) return null;
1214     if (try lookupGlyphIgnored(data, gdef, filter, mark_glyph)) return null;
1215     const subtable_count = try readGposU16(bytes, lookup_offset + 4);
1216     if (!hasGposBytes(bytes, lookup_offset + 6, @as(usize, subtable_count) * 2)) return error.InvalidGpos;
1217 
1218     for (0..subtable_count) |i| {
1219         const subtable_offset = try gposChildOffset(bytes, lookup_offset, try readGposU16(bytes, lookup_offset + 6 + i * 2), 2);
1220         const resolved = try resolveGposSubtable(bytes, lookup_type, subtable_offset);
1221         if (resolved.lookup_type != 5) continue;
1222         if (try markToLigatureAdjustmentSubtable(bytes, resolved.subtable_offset, ligature_glyph, mark_glyph, component_index)) |adjustment| return adjustment;
1223     }
1224     return null;
1225 }
1226 
1227 fn lookupMarkToMarkAdjustmentImpl(self: Gpos, data: []const u8, bytes: []const u8, gdef: ?Gdef, lookup_index: u16, base_mark_glyph: u16, mark_glyph: u16) FontError!?GposValueAdjustment {
1228     const lookup_offset = try self.lookupOffset(bytes, lookup_index);
1229 
1230     const lookup_type = try readGposU16(bytes, lookup_offset);
1231     const filter = try gposLookupFilter(bytes, lookup_offset);
1232     if (lookup_type != 6 and lookup_type != 9) return null;
1233     if (try lookupGlyphIgnored(data, gdef, filter, base_mark_glyph)) return null;
1234     if (try lookupGlyphIgnored(data, gdef, filter, mark_glyph)) return null;
1235     const subtable_count = try readGposU16(bytes, lookup_offset + 4);
1236     if (!hasGposBytes(bytes, lookup_offset + 6, @as(usize, subtable_count) * 2)) return error.InvalidGpos;
1237 
1238     for (0..subtable_count) |i| {
1239         const subtable_offset = try gposChildOffset(bytes, lookup_offset, try readGposU16(bytes, lookup_offset + 6 + i * 2), 2);
1240         const resolved = try resolveGposSubtable(bytes, lookup_type, subtable_offset);
1241         if (resolved.lookup_type != 6) continue;
1242         if (try markToMarkAdjustmentSubtable(bytes, resolved.subtable_offset, base_mark_glyph, mark_glyph)) |adjustment| return adjustment;
1243     }
1244     return null;
1245 }
1246 
1247 fn lookupOffsetImpl(self: Gpos, bytes: []const u8, lookup_index: u16) FontError!usize {
1248     const lookup_count = try readGposU16(bytes, self.lookup_list_offset);
1249     if (lookup_index >= lookup_count) return error.InvalidGpos;
1250     const lookup_record = self.lookup_list_offset + 2 + @as(usize, lookup_index) * 2;
1251     return try gposChildOffset(bytes, self.lookup_list_offset, try readGposU16(bytes, lookup_record), 6);
1252 }
1253 
1254 fn lookupIsActiveImpl(self: Gpos, bytes: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, lookup_index: u16, source: ?FeatureRange) FontError!bool {
1255     const langsys_offset = (try gposLangSysOffset(bytes, self.script_list_offset, selection)) orelse return false;
1256     const required_feature_index = try readGposU16(bytes, langsys_offset + 2);
1257     if (required_feature_index != 0xffff) {
1258         const feature_index = required_feature_index;
1259         if (try self.featureContainsLookup(bytes, face, selection, feature_index, lookup_index)) return true;
1260     }
1261 
1262     const feature_count = try readGposU16(bytes, langsys_offset + 4);
1263     if (!hasGposBytes(bytes, langsys_offset + 6, @as(usize, feature_count) * 2)) return error.InvalidGpos;
1264     for (0..feature_count) |i| {
1265         const feature_index = try readGposU16(bytes, langsys_offset + 6 + i * 2);
1266         const feature_tag = try self.featureTag(bytes, feature_index);
1267         if (!featureEnabled(selection, feature_tag, gposFeatureDefaultOn(selection, feature_tag), source)) continue;
1268         if (try self.featureContainsLookup(bytes, face, selection, feature_index, lookup_index)) return true;
1269     }
1270     return false;
1271 }
1272 
1273 fn featureTagImpl(self: Gpos, bytes: []const u8, feature_index: u16) FontError!Tag {
1274     const feature_record = try self.featureRecord(bytes, feature_index);
1275     return try readGposU32(bytes, feature_record);
1276 }
1277 
1278 fn featureContainsLookupImpl(self: Gpos, bytes: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, feature_index: u16, lookup_index: u16) FontError!bool {
1279     const feature_offset = try self.featureOffset(bytes, face, selection, feature_index);
1280     return try featureTableContainsLookup(.gpos, bytes, feature_offset, lookup_index);
1281 }
1282 
1283 fn featureOffsetImpl(self: Gpos, bytes: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, feature_index: u16) FontError!usize {
1284     if (try matchingFeatureSubstitutionOffset(.gpos, bytes, self.feature_variations_offset, face, selection)) |substitution_offset| {
1285         if (try substitutedFeatureOffset(.gpos, bytes, substitution_offset, feature_index)) |alternate_offset| return alternate_offset;
1286     }
1287     const feature_record = try self.featureRecord(bytes, feature_index);
1288     return try gposChildOffset(bytes, self.feature_list_offset, try readGposU16(bytes, feature_record + 4), 4);
1289 }
1290 
1291 fn featureRecordImpl(self: Gpos, bytes: []const u8, feature_index: u16) FontError!usize {
1292     const feature_count = try readGposU16(bytes, self.feature_list_offset);
1293     if (feature_index >= feature_count) return error.InvalidGpos;
1294     const feature_record = self.feature_list_offset + 2 + @as(usize, feature_index) * 6;
1295     if (!hasGposBytes(bytes, feature_record, 6)) return error.InvalidGpos;
1296     return feature_record;
1297 }
1298 
1299 const ResolvedGposSubtable = struct {
1300     lookup_type: u16,
1301     subtable_offset: usize,
1302 };
1303 
1304 fn resolveGposSubtable(data: []const u8, lookup_type: u16, subtable_offset: usize) FontError!ResolvedGposSubtable {
1305     if (lookup_type != 9) return .{ .lookup_type = lookup_type, .subtable_offset = subtable_offset };
1306     if (!hasGposBytes(data, subtable_offset, 8)) return error.InvalidGpos;
1307     if (try readGposU16(data, subtable_offset) != 1) return error.InvalidGpos;
1308     const extension_lookup_type = try readGposU16(data, subtable_offset + 2);
1309     if (extension_lookup_type == 9) return error.InvalidGpos;
1310     const extension_offset = try readGposU32(data, subtable_offset + 4);
1311     return .{
1312         .lookup_type = extension_lookup_type,
1313         .subtable_offset = try gposChildOffset32(data, subtable_offset, extension_offset, 2),
1314     };
1315 }
1316 
1317 fn previousLookupGlyphIndexLocal(data: []const u8, gdef: ?Gdef, filter: LookupFilter, glyph_ids: []const u32, glyph_index: usize) FontError!?usize {
1318     var index = glyph_index;
1319     while (index > 0) {
1320         index -= 1;
1321         const glyph_id = glyph_ids[index];
1322         if (glyph_id > std.math.maxInt(u16)) return null;
1323         if (try lookupGlyphIgnored(data, gdef, filter, @intCast(glyph_id))) continue;
1324         return index;
1325     }
1326     return null;
1327 }
1328 
1329 fn gposLangSysOffset(data: []const u8, script_list_offset: usize, selection: LayoutSelection) FontError!?usize {
1330     if (selection.script_tags.len != 0) {
1331         for (selection.script_tags) |script_tag| {
1332             if (try gposLangSysOffsetForScript(data, script_list_offset, script_tag, selection.language)) |langsys_offset| return langsys_offset;
1333         }
1334         if (!scriptTagListContains(selection.script_tags, defaultScriptTag)) {
1335             return try gposLangSysOffsetForScript(data, script_list_offset, defaultScriptTag, selection.language);
1336         }
1337         return null;
1338     }
1339     if (try gposScriptOffset(data, script_list_offset, selection.script)) |script_offset| {
1340         return try gposLangSysOffsetInScript(data, script_offset, selection.language);
1341     }
1342     if (selection.script != defaultScriptTag) {
1343         if (try gposScriptOffset(data, script_list_offset, defaultScriptTag)) |script_offset| {
1344             return try gposLangSysOffsetInScript(data, script_offset, selection.language);
1345         }
1346     }
1347     return null;
1348 }
1349 
1350 fn gposLangSysOffsetForScript(data: []const u8, script_list_offset: usize, script_tag: Tag, language: ?Tag) FontError!?usize {
1351     const script_offset = (try gposScriptOffset(data, script_list_offset, script_tag)) orelse return null;
1352     return try gposLangSysOffsetInScript(data, script_offset, language);
1353 }
1354 
1355 fn gposScriptOffset(data: []const u8, script_list_offset: usize, script_tag: Tag) FontError!?usize {
1356     const script_count = try readGposU16(data, script_list_offset);
1357     if (!hasGposBytes(data, script_list_offset + 2, @as(usize, script_count) * 6)) return error.InvalidGpos;
1358     for (0..script_count) |i| {
1359         const record = script_list_offset + 2 + i * 6;
1360         if (try readGposU32(data, record) != script_tag) continue;
1361         return try gposChildOffset(data, script_list_offset, try readGposU16(data, record + 4), 4);
1362     }
1363     return null;
1364 }
1365 
1366 fn gposLangSysOffsetInScript(data: []const u8, script_offset: usize, language: ?Tag) FontError!?usize {
1367     if (language) |language_tag| {
1368         const langsys_count = try readGposU16(data, script_offset + 2);
1369         const records_offset = script_offset + 4;
1370         if (!hasGposBytes(data, records_offset, @as(usize, langsys_count) * 6)) return error.InvalidGpos;
1371         for (0..langsys_count) |i| {
1372             const record = records_offset + i * 6;
1373             if (try readGposU32(data, record) != language_tag) continue;
1374             return try gposChildOffset(data, script_offset, try readGposU16(data, record + 4), 6);
1375         }
1376     }
1377 
1378     const default_langsys_offset = try readGposU16(data, script_offset);
1379     if (default_langsys_offset == 0) return null;
1380     return try gposChildOffset(data, script_offset, default_langsys_offset, 6);
1381 }
1382 
1383 fn gposGlyphInCoverage(data: []const u8, coverage_offset: usize, glyph_id: u32) FontError!bool {
1384     const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGpos;
1385     return (coverage.index(data, glyph_id) catch return error.InvalidGpos) != null;
1386 }
1387 
1388 fn pairAdjustmentSubtable(data: []const u8, subtable_offset: usize, left_glyph: u16, right_glyph: u16) FontError!?GposPairAdjustment {
1389     if (!hasGposBytes(data, subtable_offset, 2)) return error.InvalidGpos;
1390     return switch (try readGposU16(data, subtable_offset)) {
1391         1 => pairAdjustmentFormat1(data, subtable_offset, left_glyph, right_glyph),
1392         2 => pairAdjustmentFormat2(data, subtable_offset, left_glyph, right_glyph),
1393         else => error.InvalidGpos,
1394     };
1395 }
1396 
1397 fn singleAdjustmentSubtable(data: []const u8, subtable_offset: usize, glyph_id: u16) FontError!?GposValueAdjustment {
1398     if (!hasGposBytes(data, subtable_offset, 2)) return error.InvalidGpos;
1399     return switch (try readGposU16(data, subtable_offset)) {
1400         1 => singleAdjustmentFormat1(data, subtable_offset, glyph_id),
1401         2 => singleAdjustmentFormat2(data, subtable_offset, glyph_id),
1402         else => error.InvalidGpos,
1403     };
1404 }
1405 
1406 fn singleAdjustmentFormat1(data: []const u8, subtable_offset: usize, glyph_id: u16) FontError!?GposValueAdjustment {
1407     if (!hasGposBytes(data, subtable_offset, 6)) return error.InvalidGpos;
1408     const coverage_offset = try gposChildOffset(data, subtable_offset, try readGposU16(data, subtable_offset + 2), 2);
1409     const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGpos;
1410     if ((coverage.index(data, glyph_id) catch return error.InvalidGpos) == null) return null;
1411     const value_format = try readGposU16(data, subtable_offset + 4);
1412     return try readGposValueRecord(data, subtable_offset, subtable_offset + 6, value_format);
1413 }
1414 
1415 fn singleAdjustmentFormat2(data: []const u8, subtable_offset: usize, glyph_id: u16) FontError!?GposValueAdjustment {
1416     if (!hasGposBytes(data, subtable_offset, 8)) return error.InvalidGpos;
1417     const coverage_offset = try gposChildOffset(data, subtable_offset, try readGposU16(data, subtable_offset + 2), 2);
1418     const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGpos;
1419     const coverage_index = (coverage.index(data, glyph_id) catch return error.InvalidGpos) orelse return null;
1420     const value_format = try readGposU16(data, subtable_offset + 4);
1421     const value_count = try readGposU16(data, subtable_offset + 6);
1422     if (coverage_index >= value_count) return error.InvalidGpos;
1423     const value_size = try gposValueRecordSize(value_format);
1424     const value_records_offset = subtable_offset + 8;
1425     if (value_size != 0 and @as(usize, value_count) > (std.math.maxInt(usize) - 8) / value_size) return error.InvalidGpos;
1426     if (!hasGposBytes(data, value_records_offset, @as(usize, value_count) * value_size)) return error.InvalidGpos;
1427     return try readGposValueRecord(data, subtable_offset, value_records_offset + @as(usize, coverage_index) * value_size, value_format);
1428 }
1429 
1430 fn cursiveAttachmentSubtable(data: []const u8, subtable_offset: usize, exit_glyph: u16, entry_glyph: u16) FontError!?GposCursiveAttachment {
1431     if (!hasGposBytes(data, subtable_offset, 6)) return error.InvalidGpos;
1432     if (try readGposU16(data, subtable_offset) != 1) return error.InvalidGpos;
1433     const coverage_offset = try gposChildOffset(data, subtable_offset, try readGposU16(data, subtable_offset + 2), 2);
1434     const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGpos;
1435     const exit_index = (coverage.index(data, exit_glyph) catch return error.InvalidGpos) orelse return null;
1436     const entry_index = (coverage.index(data, entry_glyph) catch return error.InvalidGpos) orelse return null;
1437     const entry_exit_count = try readGposU16(data, subtable_offset + 4);
1438     if (exit_index >= entry_exit_count or entry_index >= entry_exit_count) return error.InvalidGpos;
1439     const records_offset = subtable_offset + 6;
1440     if (!hasGposBytes(data, records_offset, @as(usize, entry_exit_count) * 4)) return error.InvalidGpos;
1441 
1442     const exit_record = records_offset + @as(usize, exit_index) * 4;
1443     const entry_record = records_offset + @as(usize, entry_index) * 4;
1444     const exit_anchor_relative = try readGposU16(data, exit_record + 2);
1445     const entry_anchor_relative = try readGposU16(data, entry_record);
1446     if (exit_anchor_relative == 0 or entry_anchor_relative == 0) return null;
1447 
1448     const exit_anchor_offset = try gposChildOffset(data, subtable_offset, exit_anchor_relative, 6);
1449     const entry_anchor_offset = try gposChildOffset(data, subtable_offset, entry_anchor_relative, 6);
1450     const exit_anchor = try readGposAnchor(data, exit_anchor_offset);
1451     const entry_anchor = try readGposAnchor(data, entry_anchor_offset);
1452 
1453     return .{
1454         .x_anchor_delta = @as(i32, exit_anchor.x) - @as(i32, entry_anchor.x),
1455         .y_anchor_delta = @as(i32, exit_anchor.y) - @as(i32, entry_anchor.y),
1456     };
1457 }
1458 
1459 fn pairAdjustmentFormat1(data: []const u8, subtable_offset: usize, left_glyph: u16, right_glyph: u16) FontError!?GposPairAdjustment {
1460     if (!hasGposBytes(data, subtable_offset, 10)) return error.InvalidGpos;
1461     const coverage_offset = try gposChildOffset(data, subtable_offset, try readGposU16(data, subtable_offset + 2), 2);
1462     const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGpos;
1463     const coverage_index = (coverage.index(data, left_glyph) catch return error.InvalidGpos) orelse return null;
1464     const value_format_1 = try readGposU16(data, subtable_offset + 4);
1465     const value_format_2 = try readGposU16(data, subtable_offset + 6);
1466     const value_size_1 = try gposValueRecordSize(value_format_1);
1467     const value_size_2 = try gposValueRecordSize(value_format_2);
1468     const pair_set_count = try readGposU16(data, subtable_offset + 8);
1469     if (coverage_index >= pair_set_count) return error.InvalidGpos;
1470     const pair_set_offsets = subtable_offset + 10;
1471     if (!hasGposBytes(data, pair_set_offsets, @as(usize, pair_set_count) * 2)) return error.InvalidGpos;
1472     const pair_set_offset = try gposChildOffset(data, subtable_offset, try readGposU16(data, pair_set_offsets + @as(usize, coverage_index) * 2), 2);
1473     const pair_value_count = try readGposU16(data, pair_set_offset);
1474     const pair_record_size = 2 + value_size_1 + value_size_2;
1475     if (@as(usize, pair_value_count) > std.math.maxInt(usize) / pair_record_size) return error.InvalidGpos;
1476     const pair_values_offset = pair_set_offset + 2;
1477     if (!hasGposBytes(data, pair_values_offset, @as(usize, pair_value_count) * pair_record_size)) return error.InvalidGpos;
1478 
1479     var low: usize = 0;
1480     var high: usize = pair_value_count;
1481     while (low < high) {
1482         const mid = low + (high - low) / 2;
1483         const record = pair_values_offset + mid * pair_record_size;
1484         const second_glyph = try readGposU16(data, record);
1485         if (right_glyph < second_glyph) {
1486             high = mid;
1487         } else if (right_glyph > second_glyph) {
1488             low = mid + 1;
1489         } else {
1490             const value_1 = try readGposValueRecord(data, pair_set_offset, record + 2, value_format_1);
1491             const value_2 = try readGposValueRecord(data, pair_set_offset, record + 2 + value_size_1, value_format_2);
1492             return .{ .left = value_1, .right = value_2 };
1493         }
1494     }
1495     return null;
1496 }
1497 
1498 fn pairAdjustmentFormat2(data: []const u8, subtable_offset: usize, left_glyph: u16, right_glyph: u16) FontError!?GposPairAdjustment {
1499     if (!hasGposBytes(data, subtable_offset, 16)) return error.InvalidGpos;
1500     const coverage_offset = try gposChildOffset(data, subtable_offset, try readGposU16(data, subtable_offset + 2), 2);
1501     const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGpos;
1502     if ((coverage.index(data, left_glyph) catch return error.InvalidGpos) == null) return null;
1503     const value_format_1 = try readGposU16(data, subtable_offset + 4);
1504     const value_format_2 = try readGposU16(data, subtable_offset + 6);
1505     const value_size_1 = try gposValueRecordSize(value_format_1);
1506     const value_size_2 = try gposValueRecordSize(value_format_2);
1507     const class_def_1_offset = try gposChildOffset(data, subtable_offset, try readGposU16(data, subtable_offset + 8), 2);
1508     const class_def_2_offset = try gposChildOffset(data, subtable_offset, try readGposU16(data, subtable_offset + 10), 2);
1509     const class_count_1 = try readGposU16(data, subtable_offset + 12);
1510     const class_count_2 = try readGposU16(data, subtable_offset + 14);
1511     const class_def_1 = layout.ClassDef.init(data, class_def_1_offset) catch return error.InvalidGpos;
1512     const class_def_2 = layout.ClassDef.init(data, class_def_2_offset) catch return error.InvalidGpos;
1513     const class_1 = class_def_1.class(data, left_glyph) catch return error.InvalidGpos;
1514     const class_2 = class_def_2.class(data, right_glyph) catch return error.InvalidGpos;
1515     if (class_1 >= class_count_1 or class_2 >= class_count_2) return error.InvalidGpos;
1516 
1517     const record_size = value_size_1 + value_size_2;
1518     const class_pair_count = @as(usize, class_count_1) * @as(usize, class_count_2);
1519     if (record_size != 0 and class_pair_count > std.math.maxInt(usize) / record_size) return error.InvalidGpos;
1520     const class_records_offset = subtable_offset + 16;
1521     if (!hasGposBytes(data, class_records_offset, class_pair_count * record_size)) return error.InvalidGpos;
1522     const class_record_index = @as(usize, class_1) * @as(usize, class_count_2) + @as(usize, class_2);
1523     const record = class_records_offset + class_record_index * record_size;
1524     const value_1 = try readGposValueRecord(data, subtable_offset, record, value_format_1);
1525     const value_2 = try readGposValueRecord(data, subtable_offset, record + value_size_1, value_format_2);
1526     return .{ .left = value_1, .right = value_2 };
1527 }
1528 
1529 fn markToBaseAdjustmentSubtable(data: []const u8, subtable_offset: usize, base_glyph: u16, mark_glyph: u16) FontError!?GposValueAdjustment {
1530     if (!hasGposBytes(data, subtable_offset, 12)) return error.InvalidGpos;
1531     if (try readGposU16(data, subtable_offset) != 1) return error.InvalidGpos;
1532 
1533     const mark_coverage_offset = try gposChildOffset(data, subtable_offset, try readGposU16(data, subtable_offset + 2), 2);
1534     const base_coverage_offset = try gposChildOffset(data, subtable_offset, try readGposU16(data, subtable_offset + 4), 2);
1535     const mark_class_count = try readGposU16(data, subtable_offset + 6);
1536     const mark_array_offset = try gposChildOffset(data, subtable_offset, try readGposU16(data, subtable_offset + 8), 2);
1537     const base_array_offset = try gposChildOffset(data, subtable_offset, try readGposU16(data, subtable_offset + 10), 2);
1538 
1539     const mark_coverage = layout.Coverage.init(data, mark_coverage_offset) catch return error.InvalidGpos;
1540     const base_coverage = layout.Coverage.init(data, base_coverage_offset) catch return error.InvalidGpos;
1541     const mark_index = (mark_coverage.index(data, mark_glyph) catch return error.InvalidGpos) orelse return null;
1542     const base_index = (base_coverage.index(data, base_glyph) catch return error.InvalidGpos) orelse return null;
1543 
1544     const mark_count = try readGposU16(data, mark_array_offset);
1545     if (mark_index >= mark_count) return error.InvalidGpos;
1546     const mark_record_offset = mark_array_offset + 2 + @as(usize, mark_index) * 4;
1547     if (!hasGposBytes(data, mark_record_offset, 4)) return error.InvalidGpos;
1548     const mark_class = try readGposU16(data, mark_record_offset);
1549     if (mark_class >= mark_class_count) return error.InvalidGpos;
1550     const mark_anchor_offset = try gposChildOffset(data, mark_array_offset, try readGposU16(data, mark_record_offset + 2), 6);
1551     const mark_anchor = try readGposAnchor(data, mark_anchor_offset);
1552 
1553     const base_count = try readGposU16(data, base_array_offset);
1554     if (base_index >= base_count) return error.InvalidGpos;
1555     const base_record_size = @as(usize, mark_class_count) * 2;
1556     const base_records_offset = base_array_offset + 2;
1557     if (base_record_size != 0 and @as(usize, base_count) > (std.math.maxInt(usize) - 2) / base_record_size) return error.InvalidGpos;
1558     if (!hasGposBytes(data, base_records_offset, @as(usize, base_count) * base_record_size)) return error.InvalidGpos;
1559     const base_anchor_record = base_records_offset + @as(usize, base_index) * base_record_size + @as(usize, mark_class) * 2;
1560     const base_anchor_relative = try readGposU16(data, base_anchor_record);
1561     if (base_anchor_relative == 0) return null;
1562     const base_anchor_offset = try gposChildOffset(data, base_array_offset, base_anchor_relative, 6);
1563     const base_anchor = try readGposAnchor(data, base_anchor_offset);
1564 
1565     return .{
1566         .x_placement = @as(i32, base_anchor.x) - @as(i32, mark_anchor.x),
1567         .y_placement = @as(i32, base_anchor.y) - @as(i32, mark_anchor.y),
1568     };
1569 }
1570 
1571 fn markToLigatureAdjustmentSubtable(data: []const u8, subtable_offset: usize, ligature_glyph: u16, mark_glyph: u16, component_index: u16) FontError!?GposValueAdjustment {
1572     if (!hasGposBytes(data, subtable_offset, 12)) return error.InvalidGpos;
1573     if (try readGposU16(data, subtable_offset) != 1) return error.InvalidGpos;
1574 
1575     const mark_coverage_offset = try gposChildOffset(data, subtable_offset, try readGposU16(data, subtable_offset + 2), 2);
1576     const ligature_coverage_offset = try gposChildOffset(data, subtable_offset, try readGposU16(data, subtable_offset + 4), 2);
1577     const mark_class_count = try readGposU16(data, subtable_offset + 6);
1578     const mark_array_offset = try gposChildOffset(data, subtable_offset, try readGposU16(data, subtable_offset + 8), 2);
1579     const ligature_array_offset = try gposChildOffset(data, subtable_offset, try readGposU16(data, subtable_offset + 10), 2);
1580 
1581     const mark_coverage = layout.Coverage.init(data, mark_coverage_offset) catch return error.InvalidGpos;
1582     const ligature_coverage = layout.Coverage.init(data, ligature_coverage_offset) catch return error.InvalidGpos;
1583     const mark_index = (mark_coverage.index(data, mark_glyph) catch return error.InvalidGpos) orelse return null;
1584     const ligature_index = (ligature_coverage.index(data, ligature_glyph) catch return error.InvalidGpos) orelse return null;
1585 
1586     const mark_count = try readGposU16(data, mark_array_offset);
1587     if (mark_index >= mark_count) return error.InvalidGpos;
1588     const mark_record_offset = mark_array_offset + 2 + @as(usize, mark_index) * 4;
1589     if (!hasGposBytes(data, mark_record_offset, 4)) return error.InvalidGpos;
1590     const mark_class = try readGposU16(data, mark_record_offset);
1591     if (mark_class >= mark_class_count) return error.InvalidGpos;
1592     const mark_anchor_offset = try gposChildOffset(data, mark_array_offset, try readGposU16(data, mark_record_offset + 2), 6);
1593     const mark_anchor = try readGposAnchor(data, mark_anchor_offset);
1594 
1595     const ligature_count = try readGposU16(data, ligature_array_offset);
1596     if (ligature_index >= ligature_count) return error.InvalidGpos;
1597     const ligature_offsets_offset = ligature_array_offset + 2;
1598     if (!hasGposBytes(data, ligature_offsets_offset, @as(usize, ligature_count) * 2)) return error.InvalidGpos;
1599     const ligature_attach_offset = try gposChildOffset(data, ligature_array_offset, try readGposU16(data, ligature_offsets_offset + @as(usize, ligature_index) * 2), 2);
1600 
1601     const component_count = try readGposU16(data, ligature_attach_offset);
1602     if (component_index >= component_count) return null;
1603     const component_record_size = @as(usize, mark_class_count) * 2;
1604     const component_records_offset = ligature_attach_offset + 2;
1605     if (component_record_size != 0 and @as(usize, component_count) > (std.math.maxInt(usize) - 2) / component_record_size) return error.InvalidGpos;
1606     if (!hasGposBytes(data, component_records_offset, @as(usize, component_count) * component_record_size)) return error.InvalidGpos;
1607     const ligature_anchor_record = component_records_offset + @as(usize, component_index) * component_record_size + @as(usize, mark_class) * 2;
1608     const ligature_anchor_relative = try readGposU16(data, ligature_anchor_record);
1609     if (ligature_anchor_relative == 0) return null;
1610     const ligature_anchor_offset = try gposChildOffset(data, ligature_attach_offset, ligature_anchor_relative, 6);
1611     const ligature_anchor = try readGposAnchor(data, ligature_anchor_offset);
1612 
1613     return .{
1614         .x_placement = @as(i32, ligature_anchor.x) - @as(i32, mark_anchor.x),
1615         .y_placement = @as(i32, ligature_anchor.y) - @as(i32, mark_anchor.y),
1616     };
1617 }
1618 
1619 fn markToMarkAdjustmentSubtable(data: []const u8, subtable_offset: usize, base_mark_glyph: u16, mark_glyph: u16) FontError!?GposValueAdjustment {
1620     if (!hasGposBytes(data, subtable_offset, 12)) return error.InvalidGpos;
1621     if (try readGposU16(data, subtable_offset) != 1) return error.InvalidGpos;
1622 
1623     const mark_coverage_offset = try gposChildOffset(data, subtable_offset, try readGposU16(data, subtable_offset + 2), 2);
1624     const base_mark_coverage_offset = try gposChildOffset(data, subtable_offset, try readGposU16(data, subtable_offset + 4), 2);
1625     const mark_class_count = try readGposU16(data, subtable_offset + 6);
1626     const mark_array_offset = try gposChildOffset(data, subtable_offset, try readGposU16(data, subtable_offset + 8), 2);
1627     const base_mark_array_offset = try gposChildOffset(data, subtable_offset, try readGposU16(data, subtable_offset + 10), 2);
1628 
1629     const mark_coverage = layout.Coverage.init(data, mark_coverage_offset) catch return error.InvalidGpos;
1630     const base_mark_coverage = layout.Coverage.init(data, base_mark_coverage_offset) catch return error.InvalidGpos;
1631     const mark_index = (mark_coverage.index(data, mark_glyph) catch return error.InvalidGpos) orelse return null;
1632     const base_mark_index = (base_mark_coverage.index(data, base_mark_glyph) catch return error.InvalidGpos) orelse return null;
1633 
1634     const mark_count = try readGposU16(data, mark_array_offset);
1635     if (mark_index >= mark_count) return error.InvalidGpos;
1636     const mark_record_offset = mark_array_offset + 2 + @as(usize, mark_index) * 4;
1637     if (!hasGposBytes(data, mark_record_offset, 4)) return error.InvalidGpos;
1638     const mark_class = try readGposU16(data, mark_record_offset);
1639     if (mark_class >= mark_class_count) return error.InvalidGpos;
1640     const mark_anchor_offset = try gposChildOffset(data, mark_array_offset, try readGposU16(data, mark_record_offset + 2), 6);
1641     const mark_anchor = try readGposAnchor(data, mark_anchor_offset);
1642 
1643     const base_mark_count = try readGposU16(data, base_mark_array_offset);
1644     if (base_mark_index >= base_mark_count) return error.InvalidGpos;
1645     const base_mark_record_size = @as(usize, mark_class_count) * 2;
1646     const base_mark_records_offset = base_mark_array_offset + 2;
1647     if (base_mark_record_size != 0 and @as(usize, base_mark_count) > (std.math.maxInt(usize) - 2) / base_mark_record_size) return error.InvalidGpos;
1648     if (!hasGposBytes(data, base_mark_records_offset, @as(usize, base_mark_count) * base_mark_record_size)) return error.InvalidGpos;
1649     const base_mark_anchor_record = base_mark_records_offset + @as(usize, base_mark_index) * base_mark_record_size + @as(usize, mark_class) * 2;
1650     const base_mark_anchor_relative = try readGposU16(data, base_mark_anchor_record);
1651     if (base_mark_anchor_relative == 0) return null;
1652     const base_mark_anchor_offset = try gposChildOffset(data, base_mark_array_offset, base_mark_anchor_relative, 6);
1653     const base_mark_anchor = try readGposAnchor(data, base_mark_anchor_offset);
1654 
1655     return .{
1656         .x_placement = @as(i32, base_mark_anchor.x) - @as(i32, mark_anchor.x),
1657         .y_placement = @as(i32, base_mark_anchor.y) - @as(i32, mark_anchor.y),
1658     };
1659 }
1660 
1661 const GposAnchor = struct {
1662     x: i16,
1663     y: i16,
1664 };
1665 
1666 fn readGposAnchor(data: []const u8, anchor_offset: usize) FontError!GposAnchor {
1667     const format = try readGposU16(data, anchor_offset);
1668     return switch (format) {
1669         1 => blk: {
1670             if (!hasGposBytes(data, anchor_offset, 6)) return error.InvalidGpos;
1671             break :blk .{
1672                 .x = try readGposI16(data, anchor_offset + 2),
1673                 .y = try readGposI16(data, anchor_offset + 4),
1674             };
1675         },
1676         2 => blk: {
1677             if (!hasGposBytes(data, anchor_offset, 8)) return error.InvalidGpos;
1678             break :blk .{
1679                 .x = try readGposI16(data, anchor_offset + 2),
1680                 .y = try readGposI16(data, anchor_offset + 4),
1681             };
1682         },
1683         3 => blk: {
1684             if (!hasGposBytes(data, anchor_offset, 10)) return error.InvalidGpos;
1685             const x_device_offset = try readGposU16(data, anchor_offset + 6);
1686             const y_device_offset = try readGposU16(data, anchor_offset + 8);
1687             if (x_device_offset != 0) _ = try gposChildOffset(data, anchor_offset, x_device_offset, 2);
1688             if (y_device_offset != 0) _ = try gposChildOffset(data, anchor_offset, y_device_offset, 2);
1689             break :blk .{
1690                 .x = try readGposI16(data, anchor_offset + 2),
1691                 .y = try readGposI16(data, anchor_offset + 4),
1692             };
1693         },
1694         else => error.InvalidGpos,
1695     };
1696 }
1697 
1698 fn gposValueRecordSize(value_format: u16) FontError!usize {
1699     if ((value_format & 0xff00) != 0) return error.InvalidGpos;
1700     var count: usize = 0;
1701     if ((value_format & 0x0001) != 0) count += 1;
1702     if ((value_format & 0x0002) != 0) count += 1;
1703     if ((value_format & 0x0004) != 0) count += 1;
1704     if ((value_format & 0x0008) != 0) count += 1;
1705     if ((value_format & 0x0010) != 0) count += 1;
1706     if ((value_format & 0x0020) != 0) count += 1;
1707     if ((value_format & 0x0040) != 0) count += 1;
1708     if ((value_format & 0x0080) != 0) count += 1;
1709     return count * 2;
1710 }
1711 
1712 fn readGposValueRecord(data: []const u8, value_base_offset: usize, offset: usize, value_format: u16) FontError!GposValueAdjustment {
1713     const size = try gposValueRecordSize(value_format);
1714     if (!hasGposBytes(data, offset, size)) return error.InvalidGpos;
1715     var cursor = offset;
1716     var value = GposValueAdjustment{};
1717     if ((value_format & 0x0001) != 0) {
1718         value.x_placement = try readGposI16(data, cursor);
1719         cursor += 2;
1720     }
1721     if ((value_format & 0x0002) != 0) {
1722         value.y_placement = try readGposI16(data, cursor);
1723         cursor += 2;
1724     }
1725     if ((value_format & 0x0004) != 0) {
1726         value.x_advance = try readGposI16(data, cursor);
1727         cursor += 2;
1728     }
1729     if ((value_format & 0x0008) != 0) {
1730         value.y_advance = try readGposI16(data, cursor);
1731         cursor += 2;
1732     }
1733     if ((value_format & 0x0010) != 0) {
1734         value.x_placement_variation = try readGposDeviceOrVariationIndex(data, value_base_offset, try readGposU16(data, cursor));
1735         cursor += 2;
1736     }
1737     if ((value_format & 0x0020) != 0) {
1738         value.y_placement_variation = try readGposDeviceOrVariationIndex(data, value_base_offset, try readGposU16(data, cursor));
1739         cursor += 2;
1740     }
1741     if ((value_format & 0x0040) != 0) {
1742         value.x_advance_variation = try readGposDeviceOrVariationIndex(data, value_base_offset, try readGposU16(data, cursor));
1743         cursor += 2;
1744     }
1745     if ((value_format & 0x0080) != 0) {
1746         value.y_advance_variation = try readGposDeviceOrVariationIndex(data, value_base_offset, try readGposU16(data, cursor));
1747     }
1748     return value;
1749 }
1750 
1751 fn readGposDeviceOrVariationIndex(data: []const u8, value_base_offset: usize, relative_offset: u16) FontError!u32 {
1752     if (relative_offset == 0) return noGposVariationIndex;
1753     const offset = try gposChildOffset(data, value_base_offset, relative_offset, 6);
1754     const delta_format = try readGposU16(data, offset + 4);
1755     if (delta_format == 0x8000) {
1756         return packGposVariationIndex(
1757             try readGposU16(data, offset),
1758             try readGposU16(data, offset + 2),
1759         );
1760     }
1761     const start_size = try readGposU16(data, offset);
1762     const end_size = try readGposU16(data, offset + 2);
1763     if (end_size < start_size) return error.InvalidGpos;
1764     const bits_per_value: usize = switch (delta_format) {
1765         1 => 2,
1766         2 => 4,
1767         3 => 8,
1768         else => return error.InvalidGpos,
1769     };
1770     const value_count = @as(usize, end_size - start_size) + 1;
1771     const bit_count = value_count * bits_per_value;
1772     const word_count = (bit_count + 15) / 16;
1773     if (word_count > std.math.maxInt(usize) / 2) return error.InvalidGpos;
1774     if (!hasGposBytes(data, offset + 6, word_count * 2)) return error.InvalidGpos;
1775     return noGposVariationIndex;
1776 }
1777 
1778 fn packGposVariationIndex(outer: u16, inner: u16) u32 {
1779     return (@as(u32, outer) << 16) | @as(u32, inner);
1780 }
1781 test "GPOS extension positioning dispatches wrapped single adjustments" {
1782     const data = [_]u8{
1783         0, 1,  0,   0,   0,    10,   0, 30, 0, 44,
1784         0, 1,  'D', 'F', 'L',  'T',  0, 8,  0, 4,
1785         0, 0,  0,   0,   0xff, 0xff, 0, 1,  0, 0,
1786         0, 1,  'd', 'i', 's',  't',  0, 8,  0, 0,
1787         0, 1,  0,   0,   0,    1,    0, 4,  0, 9,
1788         0, 0,  0,   1,   0,    8,    0, 1,  0, 1,
1789         0, 0,  0,   8,   0,    1,    0, 8,  0, 1,
1790         0, 50, 0,   1,   0,    1,    0, 1,
1791     };
1792     const gpos = try Gpos.init(&data, .{ .offset = 0, .len = data.len });
1793     const adjustment = try gpos.singleAdjustment(&data, .{}, null, 1);
1794 
1795     try std.testing.expect(try gpos.hasActiveLookupType(&data, null, .{}, 1));
1796     try std.testing.expectEqual(@as(i32, 50), adjustment.x_placement);
1797     try std.testing.expectEqual(@as(i32, 0), adjustment.x_advance);
1798 }
1799 
1800 test "GPOS offsets stay inside table data" {
1801     const data = @as([10]u8, @splat(0));
1802     try std.testing.expectEqual(@as(usize, 8), try gposOffset(&data, 8));
1803     try std.testing.expectError(error.InvalidGpos, gposOffset(&data, 0));
1804     try std.testing.expectError(error.InvalidGpos, gposOffset(&data, 9));
1805     try std.testing.expectError(error.InvalidGpos, gposOffset(&data, 10));
1806 }
1807 
1808 test "GPOS child offsets stay inside table data" {
1809     const data = @as([10]u8, @splat(0));
1810     try std.testing.expectEqual(@as(usize, 6), try gposChildOffset(&data, 2, 4, 2));
1811     try std.testing.expectError(error.InvalidGpos, gposChildOffset(&data, 2, 0, 2));
1812     try std.testing.expectError(error.InvalidGpos, gposChildOffset(&data, 10, 1, 2));
1813     try std.testing.expectError(error.InvalidGpos, gposChildOffset(&data, 8, 1, 2));
1814 }
1815 
1816 test "GPOS layout selection resolves language system tags" {
1817     const data = [_]u8{
1818         0,    1,
1819         'l',  'a',
1820         't',  'n',
1821         0,    8,
1822         0,    18,
1823         0,    1,
1824         'T',  'R',
1825         'K',  ' ',
1826         0,    10,
1827         0,    0,
1828         0xff, 0xff,
1829         0,    1,
1830         0,    7,
1831         0,    0,
1832         0xff, 0xff,
1833         0,    1,
1834         0,    3,
1835     };
1836 
1837     try std.testing.expectEqual(@as(?usize, 18), try gposLangSysOffset(&data, 0, .{ .script = tag("latn"), .language = tag("TRK ") }));
1838     try std.testing.expectEqual(@as(?usize, 26), try gposLangSysOffset(&data, 0, .{ .script = tag("latn"), .language = tag("DEU ") }));
1839     try std.testing.expectEqual(@as(?usize, null), try gposLangSysOffset(&data, 0, .{ .script = tag("cyrl"), .language = tag("TRK ") }));
1840 }
1841 
1842 test "GPOS value records read placement and advance fields" {
1843     const data = [_]u8{
1844         0xff, 0xfe,
1845         0,    3,
1846         0xff, 0xb0,
1847         0,    4,
1848     };
1849     const value = try readGposValueRecord(&data, 0, 0, 0x000f);
1850     try std.testing.expectEqual(@as(i32, -2), value.x_placement);
1851     try std.testing.expectEqual(@as(i32, 3), value.y_placement);
1852     try std.testing.expectEqual(@as(i32, -80), value.x_advance);
1853     try std.testing.expectEqual(@as(i32, 4), value.y_advance);
1854     try std.testing.expectEqual(@as(usize, 16), try gposValueRecordSize(0x00ff));
1855     try std.testing.expectError(error.InvalidGpos, gposValueRecordSize(0x0100));
1856 }
1857 
1858 test "GPOS value records validate device and variation offsets" {
1859     const data = [_]u8{
1860         0,    10,
1861         0,    6,
1862         0,    14,
1863         0,    10,
1864         0,    13,
1865         0,    1,
1866         0,    0,
1867         0,    2,
1868         0,    3,
1869         0x80, 0,
1870     };
1871     const value = try readGposValueRecord(&data, 0, 0, 0x0091);
1872 
1873     try std.testing.expectEqual(@as(i32, 10), value.x_placement);
1874     try std.testing.expectEqual(@as(i32, 0), value.y_placement);
1875     try std.testing.expectEqual(@as(i32, 0), value.x_advance);
1876     try std.testing.expectEqual(@as(i32, 0), value.y_advance);
1877 
1878     const malformed = [_]u8{
1879         0, 4,
1880         0, 0,
1881         0, 13,
1882         0, 10,
1883         0, 1,
1884     };
1885     try std.testing.expectError(error.InvalidGpos, readGposValueRecord(&malformed, 0, 0, 0x0010));
1886 }
1887 
1888 test "GPOS single adjustment format 1 uses one value record" {
1889     const data = [_]u8{
1890         0,    1,
1891         0,    10,
1892         0,    3,
1893         0,    7,
1894         0xff, 0xf8,
1895         0,    1,
1896         0,    1,
1897         0,    'A' - 31,
1898     };
1899     const adjustment = (try singleAdjustmentSubtable(&data, 0, 'A' - 31)).?;
1900     try std.testing.expectEqual(@as(i32, 7), adjustment.x_placement);
1901     try std.testing.expectEqual(@as(i32, -8), adjustment.y_placement);
1902     try std.testing.expectEqual(@as(?GposValueAdjustment, null), try singleAdjustmentSubtable(&data, 0, 'B' - 31));
1903 }
1904 
1905 test "GPOS single adjustment format 2 uses coverage index records" {
1906     const data = [_]u8{
1907         0,    2,
1908         0,    20,
1909         0,    7,
1910         0,    2,
1911         0,    20,
1912         0,    30,
1913         0xff, 0xd8,
1914         0xff, 0xf6,
1915         0,    0,
1916         0,    0,
1917         0,    1,
1918         0,    2,
1919         0,    'A' - 31,
1920         0,    'B' - 31,
1921     };
1922     const a_adjustment = (try singleAdjustmentSubtable(&data, 0, 'A' - 31)).?;
1923     try std.testing.expectEqual(@as(i32, 20), a_adjustment.x_placement);
1924     try std.testing.expectEqual(@as(i32, 30), a_adjustment.y_placement);
1925     try std.testing.expectEqual(@as(i32, -40), a_adjustment.x_advance);
1926     const b_adjustment = (try singleAdjustmentSubtable(&data, 0, 'B' - 31)).?;
1927     try std.testing.expectEqual(@as(i32, -10), b_adjustment.x_placement);
1928     try std.testing.expectEqual(@as(i32, 0), b_adjustment.y_placement);
1929     try std.testing.expectEqual(@as(i32, 0), b_adjustment.x_advance);
1930     try std.testing.expectEqual(@as(?GposValueAdjustment, null), try singleAdjustmentSubtable(&data, 0, '!' - 31));
1931 }
1932 
1933 test "GPOS cursive attachment format 1 uses entry and exit anchors" {
1934     const data = [_]u8{
1935         0, 1,
1936         0, 14,
1937         0, 2,
1938         0, 0,
1939         0, 22,
1940         0, 28,
1941         0, 0,
1942         0, 1,
1943         0, 2,
1944         0, 'A' - 31,
1945         0, 'B' - 31,
1946         0, 1,
1947         1, 224,
1948         1, 44,
1949         0, 1,
1950         0, 20,
1951         0, 200,
1952     };
1953     const attachment = (try cursiveAttachmentSubtable(&data, 0, 'A' - 31, 'B' - 31)).?;
1954     try std.testing.expectEqual(@as(i32, 460), attachment.x_anchor_delta);
1955     try std.testing.expectEqual(@as(i32, 100), attachment.y_anchor_delta);
1956     try std.testing.expectEqual(@as(?GposCursiveAttachment, null), try cursiveAttachmentSubtable(&data, 0, 'B' - 31, 'A' - 31));
1957     try std.testing.expectEqual(@as(?GposCursiveAttachment, null), try cursiveAttachmentSubtable(&data, 0, 'A' - 31, '!' - 31));
1958 }
1959 
1960 test "GPOS pair adjustment format 1 uses coverage and second glyph" {
1961     const data = [_]u8{
1962         0,    1,
1963         0,    18,
1964         0,    4,
1965         0,    0,
1966         0,    1,
1967         0,    12,
1968         0,    1,
1969         0,    'V' - 31,
1970         0xff, 0xb0,
1971         0,    1,
1972         0,    1,
1973         0,    'A' - 31,
1974     };
1975     const adjustment = (try pairAdjustmentSubtable(&data, 0, 'A' - 31, 'V' - 31)).?;
1976     try std.testing.expectEqual(@as(i32, -80), adjustment.left.x_advance);
1977     try std.testing.expectEqual(@as(?GposPairAdjustment, null), try pairAdjustmentSubtable(&data, 0, 'A' - 31, 'A' - 31));
1978 }
1979 
1980 test "GPOS pair adjustment format 1 resolves device offsets from PairSet" {
1981     const data = [_]u8{
1982         0,    1,
1983         0,    28,
1984         0,    0x44,
1985         0,    0,
1986         0,    1,
1987         0,    12,
1988         0,    1,
1989         0,    'V' - 31,
1990         0xff, 0xb0,
1991         0,    8,
1992         0,    10,
1993         0,    13,
1994         0,    1,
1995         0,    0,
1996         0,    1,
1997         0,    1,
1998         0,    'A' - 31,
1999     };
2000     const adjustment = (try pairAdjustmentSubtable(&data, 0, 'A' - 31, 'V' - 31)).?;
2001 
2002     try std.testing.expectEqual(@as(i32, -80), adjustment.left.x_advance);
2003     try std.testing.expectEqual(@as(i32, 0), adjustment.right.x_advance);
2004 }
2005 
2006 test "GPOS pair adjustment format 2 uses glyph classes" {
2007     const data = [_]u8{
2008         0,    2,
2009         0,    40,
2010         0,    4,
2011         0,    0,
2012         0,    24,
2013         0,    32,
2014         0,    2,
2015         0,    2,
2016         0,    0,
2017         0,    0,
2018         0,    0,
2019         0xff, 0xb0,
2020         0,    1,
2021         0,    'A' - 31,
2022         0,    1,
2023         0,    1,
2024         0,    1,
2025         0,    'V' - 31,
2026         0,    1,
2027         0,    1,
2028         0,    1,
2029         0,    1,
2030         0,    'A' - 31,
2031     };
2032     const adjustment = (try pairAdjustmentSubtable(&data, 0, 'A' - 31, 'V' - 31)).?;
2033     try std.testing.expectEqual(@as(i32, -80), adjustment.left.x_advance);
2034     try std.testing.expect((try pairAdjustmentSubtable(&data, 0, 'A' - 31, 'A' - 31)).?.isZero());
2035     try std.testing.expectEqual(@as(?GposPairAdjustment, null), try pairAdjustmentSubtable(&data, 0, 'B' - 31, 'V' - 31));
2036 }
2037 
2038 test "GPOS mark-to-base attachment format 1 uses anchors" {
2039     const data = [_]u8{
2040         0, 1,
2041         0, 34,
2042         0, 40,
2043         0, 1,
2044         0, 12,
2045         0, 24,
2046         0, 1,
2047         0, 0,
2048         0, 6,
2049         0, 1,
2050         0, 100,
2051         0, 0,
2052         0, 1,
2053         0, 4,
2054         0, 1,
2055         0, 250,
2056         2, 188,
2057         0, 1,
2058         0, 1,
2059         0, '^' - 31,
2060         0, 1,
2061         0, 1,
2062         0, 'A' - 31,
2063     };
2064     const adjustment = (try markToBaseAdjustmentSubtable(&data, 0, 'A' - 31, '^' - 31)).?;
2065     try std.testing.expectEqual(@as(i32, 150), adjustment.x_placement);
2066     try std.testing.expectEqual(@as(i32, 700), adjustment.y_placement);
2067     try std.testing.expect((try markToBaseAdjustmentSubtable(&data, 0, 'B' - 31, '^' - 31)) == null);
2068     try std.testing.expect((try markToBaseAdjustmentSubtable(&data, 0, 'A' - 31, '_' - 31)) == null);
2069 }
2070 
2071 test "GPOS mark-to-ligature attachment format 1 uses component anchors" {
2072     const data = [_]u8{
2073         0, 1,
2074         0, 46,
2075         0, 52,
2076         0, 1,
2077         0, 12,
2078         0, 24,
2079         0, 1,
2080         0, 0,
2081         0, 6,
2082         0, 1,
2083         0, 100,
2084         0, 0,
2085         0, 1,
2086         0, 4,
2087         0, 2,
2088         0, 6,
2089         0, 12,
2090         0, 1,
2091         0, 180,
2092         2, 188,
2093         0, 1,
2094         1, 74,
2095         2, 138,
2096         0, 1,
2097         0, 1,
2098         0, '^' - 31,
2099         0, 1,
2100         0, 1,
2101         0, 96,
2102     };
2103     const first_component = (try markToLigatureAdjustmentSubtable(&data, 0, 96, '^' - 31, 0)).?;
2104     try std.testing.expectEqual(@as(i32, 80), first_component.x_placement);
2105     try std.testing.expectEqual(@as(i32, 700), first_component.y_placement);
2106     const second_component = (try markToLigatureAdjustmentSubtable(&data, 0, 96, '^' - 31, 1)).?;
2107     try std.testing.expectEqual(@as(i32, 230), second_component.x_placement);
2108     try std.testing.expectEqual(@as(i32, 650), second_component.y_placement);
2109     try std.testing.expect((try markToLigatureAdjustmentSubtable(&data, 0, 96, '^' - 31, 2)) == null);
2110     try std.testing.expect((try markToLigatureAdjustmentSubtable(&data, 0, 'A' - 31, '^' - 31, 0)) == null);
2111     try std.testing.expect((try markToLigatureAdjustmentSubtable(&data, 0, 96, '!' - 31, 0)) == null);
2112 }
2113 
2114 test "GPOS mark-to-mark attachment format 1 uses anchors" {
2115     const data = [_]u8{
2116         0, 1,
2117         0, 34,
2118         0, 40,
2119         0, 1,
2120         0, 12,
2121         0, 24,
2122         0, 1,
2123         0, 0,
2124         0, 6,
2125         0, 1,
2126         0, 80,
2127         0, 100,
2128         0, 1,
2129         0, 4,
2130         0, 1,
2131         0, 100,
2132         1, 144,
2133         0, 1,
2134         0, 1,
2135         0, '_' - 31,
2136         0, 1,
2137         0, 1,
2138         0, '^' - 31,
2139     };
2140     const adjustment = (try markToMarkAdjustmentSubtable(&data, 0, '^' - 31, '_' - 31)).?;
2141     try std.testing.expectEqual(@as(i32, 20), adjustment.x_placement);
2142     try std.testing.expectEqual(@as(i32, 300), adjustment.y_placement);
2143     try std.testing.expect((try markToMarkAdjustmentSubtable(&data, 0, '_' - 31, '^' - 31)) == null);
2144     try std.testing.expect((try markToMarkAdjustmentSubtable(&data, 0, '^' - 31, '!' - 31)) == null);
2145 }
2146 
2147 test "GPOS anchor coordinates read supported formats" {
2148     const format_1 = [_]u8{
2149         0,    1,
2150         0xff, 0xfe,
2151         0,    3,
2152     };
2153     const format_2 = [_]u8{
2154         0, 2,
2155         0, 4,
2156         0, 5,
2157         0, 9,
2158     };
2159     const format_3 = [_]u8{
2160         0,    3,
2161         0xff, 0x9c,
2162         0,    200,
2163         0,    0,
2164         0,    0,
2165     };
2166 
2167     const anchor_1 = try readGposAnchor(&format_1, 0);
2168     const anchor_2 = try readGposAnchor(&format_2, 0);
2169     const anchor_3 = try readGposAnchor(&format_3, 0);
2170     try std.testing.expectEqual(@as(i16, -2), anchor_1.x);
2171     try std.testing.expectEqual(@as(i16, 3), anchor_1.y);
2172     try std.testing.expectEqual(@as(i16, 4), anchor_2.x);
2173     try std.testing.expectEqual(@as(i16, 5), anchor_2.y);
2174     try std.testing.expectEqual(@as(i16, -100), anchor_3.x);
2175     try std.testing.expectEqual(@as(i16, 200), anchor_3.y);
2176     try std.testing.expectError(error.InvalidGpos, readGposAnchor(&[_]u8{ 0, 4 }, 0));
2177 }