lib/filigree/src/font/gsub.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 GsubSubstitution = model.GsubSubstitution;
  12 const LayoutSelection = model.LayoutSelection;
  13 const Table = binary.Table;
  14 const Tag = model.Tag;
  15 const defaultScriptTag = model.defaultScriptTag;
  16 const featureEnabled = model.featureEnabled;
  17 const tagLiteral = model.tag;
  18 const featureTableContainsLookup = lookups.featureTableContainsLookup;
  19 const gsubChildOffset = lookups.gsubChildOffset;
  20 const gsubChildOffset32 = lookups.gsubChildOffset32;
  21 const gsubLookupFilter = lookups.gsubLookupFilter;
  22 const gsubOffset = lookups.gsubOffset;
  23 const hasGsubBytes = lookups.gsubHasBytes;
  24 const lookupGlyphIgnored = lookups.lookupGlyphIgnored;
  25 const lookupFilterSkipsGlyphs = lookups.lookupFilterSkipsGlyphs;
  26 const matchingFeatureSubstitutionOffset = lookups.matchingFeatureSubstitutionOffset;
  27 const readGsubI16 = lookups.gsubReadI16;
  28 const readGsubU16 = lookups.gsubReadU16;
  29 const readGsubU32 = lookups.gsubReadU32;
  30 const scriptTagListContains = lookups.scriptTagListContains;
  31 const substitutedFeatureOffset = lookups.substitutedFeatureOffset;
  32 const tag = model.tag;
  33 
  34 pub const Gsub = struct {
  35     table: Table,
  36     script_list_offset: usize,
  37     feature_list_offset: usize,
  38     lookup_list_offset: usize,
  39     feature_variations_offset: ?usize,
  40 
  41     pub const init = initImpl;
  42     pub const lookupCount = lookupCountImpl;
  43     pub const substitutionAt = substitutionAtImpl;
  44     pub const substitutionAtWithGdef = substitutionAtWithGdefImpl;
  45     const substitutionAtLookup = substitutionAtLookupImpl;
  46     const contextualSubstitution = contextualSubstitutionImpl;
  47     const contextualSubstitutionFormat1 = contextualSubstitutionFormat1Impl;
  48     const contextualSubstitutionFormat2 = contextualSubstitutionFormat2Impl;
  49     const contextualSubstitutionFormat3 = contextualSubstitutionFormat3Impl;
  50     const contextualSubstitutionRuleSet = contextualSubstitutionRuleSetImpl;
  51     const contextualSubstitutionClassSet = contextualSubstitutionClassSetImpl;
  52     const contextualSubstitutionClassRule = contextualSubstitutionClassRuleImpl;
  53     const contextualSubstitutionRule = contextualSubstitutionRuleImpl;
  54     const chainedContextualSubstitution = chainedContextualSubstitutionImpl;
  55     const chainedContextualSubstitutionFormat1 = chainedContextualSubstitutionFormat1Impl;
  56     const chainedContextualSubstitutionFormat2 = chainedContextualSubstitutionFormat2Impl;
  57     const chainedContextualSubstitutionFormat3 = chainedContextualSubstitutionFormat3Impl;
  58     const chainedContextualSubstitutionRuleSet = chainedContextualSubstitutionRuleSetImpl;
  59     const chainedContextualSubstitutionRule = chainedContextualSubstitutionRuleImpl;
  60     const chainedContextualSubstitutionClassSet = chainedContextualSubstitutionClassSetImpl;
  61     const chainedContextualSubstitutionClassRule = chainedContextualSubstitutionClassRuleImpl;
  62     const contextualLookupRecordSubstitution = contextualLookupRecordSubstitutionImpl;
  63     pub const lookupIsReverse = lookupIsReverseImpl;
  64     const lookupIsActive = lookupIsActiveImpl;
  65     const featureDefaultOn = featureDefaultOnImpl;
  66     const featureAllowsLookup = featureAllowsLookupImpl;
  67     const lookupResolvesOnlyToType = lookupResolvesOnlyToTypeImpl;
  68     const langSysHasFeature = langSysHasFeatureImpl;
  69     pub const usesAutomaticFractions = usesAutomaticFractionsImpl;
  70     pub const usesArabicJoiningForms = usesArabicJoiningFormsImpl;
  71     const featureTag = featureTagImpl;
  72     const featureContainsLookup = featureContainsLookupImpl;
  73     const featureOffset = featureOffsetImpl;
  74     const featureRecord = featureRecordImpl;
  75     const lookupOffset = lookupOffsetImpl;
  76 };
  77 
  78 fn initImpl(data: []const u8, gsub_table: Table) FontError!Gsub {
  79     const bytes = data[gsub_table.offset..][0..gsub_table.len];
  80     if (bytes.len < 10) return error.InvalidGsub;
  81     const major = try readGsubU16(bytes, 0);
  82     const minor = try readGsubU16(bytes, 2);
  83     if (major != 1 or (minor != 0 and minor != 1)) return error.InvalidGsub;
  84     if (minor == 1 and bytes.len < 14) return error.InvalidGsub;
  85 
  86     const script_list_offset = try gsubOffset(bytes, try readGsubU16(bytes, 4));
  87     const feature_list_offset = try gsubOffset(bytes, try readGsubU16(bytes, 6));
  88     const lookup_list_offset = try gsubOffset(bytes, try readGsubU16(bytes, 8));
  89     const feature_variations_offset = if (minor == 1) blk: {
  90         const relative = try readGsubU32(bytes, 10);
  91         if (relative == 0) break :blk null;
  92         break :blk try gsubChildOffset32(bytes, 0, relative, 8);
  93     } else null;
  94 
  95     return .{
  96         .table = gsub_table,
  97         .script_list_offset = script_list_offset,
  98         .feature_list_offset = feature_list_offset,
  99         .lookup_list_offset = lookup_list_offset,
 100         .feature_variations_offset = feature_variations_offset,
 101     };
 102 }
 103 
 104 fn lookupCountImpl(self: Gsub, data: []const u8) FontError!u16 {
 105     const bytes = data[self.table.offset..][0..self.table.len];
 106     return try readGsubU16(bytes, self.lookup_list_offset);
 107 }
 108 
 109 fn substitutionAtImpl(self: Gsub, data: []const u8, selection: LayoutSelection, lookup_index: u16, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {
 110     return try self.substitutionAtWithGdef(data, null, selection, null, lookup_index, glyph_ids, glyph_index, null);
 111 }
 112 
 113 fn substitutionAtWithGdefImpl(self: Gsub, data: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, gdef: ?Gdef, lookup_index: u16, glyph_ids: []const u32, glyph_index: usize, source: ?FeatureRange) FontError!?GsubSubstitution {
 114     if (glyph_index >= glyph_ids.len) return null;
 115     return try self.substitutionAtLookup(data, face, selection, gdef, lookup_index, glyph_ids, glyph_index, source, true, true);
 116 }
 117 
 118 fn substitutionAtLookupImpl(self: Gsub, data: []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!?GsubSubstitution {
 119     if (glyph_index >= glyph_ids.len) return null;
 120     const bytes = data[self.table.offset..][0..self.table.len];
 121     if (require_active and !try self.lookupIsActive(bytes, face, selection, lookup_index, source)) return null;
 122     const lookup_offset = try self.lookupOffset(bytes, lookup_index);
 123 
 124     const lookup_type = try readGsubU16(bytes, lookup_offset);
 125     const filter = try gsubLookupFilter(bytes, lookup_offset);
 126     if (lookup_type != 1 and lookup_type != 2 and lookup_type != 3 and lookup_type != 4 and lookup_type != 5 and lookup_type != 6 and lookup_type != 7 and lookup_type != 8) return null;
 127     if (glyph_ids[glyph_index] <= std.math.maxInt(u16)) {
 128         if (try lookupGlyphIgnored(data, gdef, filter, @intCast(glyph_ids[glyph_index]))) return null;
 129     } else if (lookupFilterSkipsGlyphs(filter)) {
 130         return null;
 131     }
 132     const subtable_count = try readGsubU16(bytes, lookup_offset + 4);
 133     if (!hasGsubBytes(bytes, lookup_offset + 6, @as(usize, subtable_count) * 2)) return error.InvalidGsub;
 134 
 135     for (0..subtable_count) |i| {
 136         const subtable_offset = try gsubChildOffset(bytes, lookup_offset, try readGsubU16(bytes, lookup_offset + 6 + i * 2), 2);
 137         const resolved = try resolveGsubSubtable(bytes, lookup_type, subtable_offset);
 138         switch (resolved.lookup_type) {
 139             1 => {
 140                 if (try singleSubstitution(bytes, resolved.subtable_offset, glyph_ids[glyph_index])) |substitute| {
 141                     return .{ .glyph_id = substitute, .component_count = 1 };
 142                 }
 143             },
 144             2 => {
 145                 if (try multipleSubstitution(bytes, self.table.offset, resolved.subtable_offset, glyph_ids[glyph_index])) |substitute| return substitute;
 146             },
 147             3 => {
 148                 if (try alternateSubstitution(bytes, resolved.subtable_offset, glyph_ids[glyph_index])) |substitute| {
 149                     return .{ .glyph_id = substitute, .component_count = 1 };
 150                 }
 151             },
 152             4 => {
 153                 if (try ligatureSubstitution(bytes, resolved.subtable_offset, glyph_ids, glyph_index)) |substitute| return substitute;
 154             },
 155             5 => {
 156                 if (!allow_context) return null;
 157                 if (lookupFilterSkipsGlyphs(filter)) return null;
 158                 if (try self.contextualSubstitution(data, bytes, selection, resolved.subtable_offset, glyph_ids, glyph_index)) |substitute| return substitute;
 159             },
 160             6 => {
 161                 if (!allow_context) return null;
 162                 if (lookupFilterSkipsGlyphs(filter)) return null;
 163                 if (try self.chainedContextualSubstitution(data, bytes, selection, resolved.subtable_offset, glyph_ids, glyph_index)) |substitute| return substitute;
 164             },
 165             8 => {
 166                 if (lookupFilterSkipsGlyphs(filter)) return null;
 167                 if (try reverseChainingSingleSubstitution(bytes, resolved.subtable_offset, glyph_ids, glyph_index)) |substitute| {
 168                     return .{ .glyph_id = substitute, .component_count = 1 };
 169                 }
 170             },
 171             else => return null,
 172         }
 173     }
 174     return null;
 175 }
 176 
 177 fn contextualSubstitutionImpl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {
 178     if (glyph_index >= glyph_ids.len or glyph_ids[glyph_index] > std.math.maxInt(u16)) return null;
 179     if (!hasGsubBytes(bytes, subtable_offset, 2)) return error.InvalidGsub;
 180     return switch (try readGsubU16(bytes, subtable_offset)) {
 181         1 => self.contextualSubstitutionFormat1(data, bytes, selection, subtable_offset, glyph_ids, glyph_index),
 182         2 => self.contextualSubstitutionFormat2(data, bytes, selection, subtable_offset, glyph_ids, glyph_index),
 183         3 => self.contextualSubstitutionFormat3(data, bytes, selection, subtable_offset, glyph_ids, glyph_index),
 184         else => error.InvalidGsub,
 185     };
 186 }
 187 
 188 fn contextualSubstitutionFormat1Impl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {
 189     if (!hasGsubBytes(bytes, subtable_offset, 8)) return error.InvalidGsub;
 190     const coverage_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, subtable_offset + 2), 2);
 191     const coverage = layout.Coverage.init(bytes, coverage_offset) catch return error.InvalidGsub;
 192     const coverage_index = (coverage.index(bytes, glyph_ids[glyph_index]) catch return error.InvalidGsub) orelse return null;
 193     const rule_set_count = try readGsubU16(bytes, subtable_offset + 4);
 194     if (coverage_index >= rule_set_count) return error.InvalidGsub;
 195     const rule_set_offsets = subtable_offset + 6;
 196     if (!hasGsubBytes(bytes, rule_set_offsets, @as(usize, rule_set_count) * 2)) return error.InvalidGsub;
 197     const rule_set_relative = try readGsubU16(bytes, rule_set_offsets + @as(usize, coverage_index) * 2);
 198     if (rule_set_relative == 0) return null;
 199     const rule_set_offset = try gsubChildOffset(bytes, subtable_offset, rule_set_relative, 2);
 200     return try self.contextualSubstitutionRuleSet(data, bytes, selection, rule_set_offset, glyph_ids, glyph_index);
 201 }
 202 
 203 fn contextualSubstitutionFormat2Impl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {
 204     if (!hasGsubBytes(bytes, subtable_offset, 8)) return error.InvalidGsub;
 205     const coverage_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, subtable_offset + 2), 2);
 206     const coverage = layout.Coverage.init(bytes, coverage_offset) catch return error.InvalidGsub;
 207     if ((coverage.index(bytes, glyph_ids[glyph_index]) catch return error.InvalidGsub) == null) return null;
 208     const class_def_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, subtable_offset + 4), 2);
 209     const class_def = layout.ClassDef.init(bytes, class_def_offset) catch return error.InvalidGsub;
 210     const rule_set_count = try readGsubU16(bytes, subtable_offset + 6);
 211     const rule_set_offsets = subtable_offset + 8;
 212     if (!hasGsubBytes(bytes, rule_set_offsets, @as(usize, rule_set_count) * 2)) return error.InvalidGsub;
 213     const first_class = class_def.class(bytes, glyph_ids[glyph_index]) catch return error.InvalidGsub;
 214     if (first_class >= rule_set_count) return null;
 215     const rule_set_relative = try readGsubU16(bytes, rule_set_offsets + @as(usize, first_class) * 2);
 216     if (rule_set_relative == 0) return null;
 217     const rule_set_offset = try gsubChildOffset(bytes, subtable_offset, rule_set_relative, 2);
 218     return try self.contextualSubstitutionClassSet(data, bytes, selection, class_def, rule_set_offset, glyph_ids, glyph_index);
 219 }
 220 
 221 fn contextualSubstitutionFormat3Impl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {
 222     if (!hasGsubBytes(bytes, subtable_offset, 6)) return error.InvalidGsub;
 223     const glyph_count = try readGsubU16(bytes, subtable_offset + 2);
 224     const lookup_count = try readGsubU16(bytes, subtable_offset + 4);
 225     if (glyph_count == 0) return error.InvalidGsub;
 226     if (@as(usize, glyph_count) > glyph_ids.len - glyph_index) return null;
 227     const coverage_offsets = subtable_offset + 6;
 228     if (!hasGsubBytes(bytes, coverage_offsets, @as(usize, glyph_count) * 2)) return error.InvalidGsub;
 229     for (0..glyph_count) |sequence_index| {
 230         const coverage_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, coverage_offsets + sequence_index * 2), 2);
 231         if (!try gsubGlyphInCoverage(bytes, coverage_offset, glyph_ids[glyph_index + sequence_index])) return null;
 232     }
 233 
 234     const lookup_records_offset = coverage_offsets + @as(usize, glyph_count) * 2;
 235     if (!hasGsubBytes(bytes, lookup_records_offset, @as(usize, lookup_count) * 4)) return error.InvalidGsub;
 236     return try self.contextualLookupRecordSubstitution(data, bytes, selection, lookup_records_offset, lookup_count, glyph_ids, glyph_index, glyph_count);
 237 }
 238 
 239 fn contextualSubstitutionRuleSetImpl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, rule_set_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {
 240     const rule_count = try readGsubU16(bytes, rule_set_offset);
 241     const rule_offsets = rule_set_offset + 2;
 242     if (!hasGsubBytes(bytes, rule_offsets, @as(usize, rule_count) * 2)) return error.InvalidGsub;
 243     for (0..rule_count) |rule_index| {
 244         const rule_relative = try readGsubU16(bytes, rule_offsets + rule_index * 2);
 245         if (rule_relative == 0) continue;
 246         const rule_offset = try gsubChildOffset(bytes, rule_set_offset, rule_relative, 4);
 247         if (try self.contextualSubstitutionRule(data, bytes, selection, rule_offset, glyph_ids, glyph_index)) |substitute| return substitute;
 248     }
 249     return null;
 250 }
 251 
 252 fn contextualSubstitutionClassSetImpl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, class_def: layout.ClassDef, rule_set_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {
 253     const rule_count = try readGsubU16(bytes, rule_set_offset);
 254     const rule_offsets = rule_set_offset + 2;
 255     if (!hasGsubBytes(bytes, rule_offsets, @as(usize, rule_count) * 2)) return error.InvalidGsub;
 256     for (0..rule_count) |rule_index| {
 257         const rule_relative = try readGsubU16(bytes, rule_offsets + rule_index * 2);
 258         if (rule_relative == 0) continue;
 259         const rule_offset = try gsubChildOffset(bytes, rule_set_offset, rule_relative, 4);
 260         if (try self.contextualSubstitutionClassRule(data, bytes, selection, class_def, rule_offset, glyph_ids, glyph_index)) |substitute| return substitute;
 261     }
 262     return null;
 263 }
 264 
 265 fn contextualSubstitutionClassRuleImpl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, class_def: layout.ClassDef, rule_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {
 266     const glyph_count = try readGsubU16(bytes, rule_offset);
 267     const lookup_count = try readGsubU16(bytes, rule_offset + 2);
 268     if (glyph_count == 0) return error.InvalidGsub;
 269     if (@as(usize, glyph_count) > glyph_ids.len - glyph_index) return null;
 270     const input_sequence_offset = rule_offset + 4;
 271     const input_sequence_len = @as(usize, glyph_count - 1) * 2;
 272     if (!hasGsubBytes(bytes, input_sequence_offset, input_sequence_len)) return error.InvalidGsub;
 273     for (1..glyph_count) |sequence_index| {
 274         const glyph = glyph_ids[glyph_index + sequence_index];
 275         if (glyph > std.math.maxInt(u16)) return null;
 276         const expected_class = try readGsubU16(bytes, input_sequence_offset + (sequence_index - 1) * 2);
 277         const actual_class = class_def.class(bytes, glyph) catch return error.InvalidGsub;
 278         if (actual_class != expected_class) return null;
 279     }
 280 
 281     const lookup_records_offset = input_sequence_offset + input_sequence_len;
 282     if (!hasGsubBytes(bytes, lookup_records_offset, @as(usize, lookup_count) * 4)) return error.InvalidGsub;
 283     return try self.contextualLookupRecordSubstitution(data, bytes, selection, lookup_records_offset, lookup_count, glyph_ids, glyph_index, glyph_count);
 284 }
 285 
 286 fn contextualSubstitutionRuleImpl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, rule_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {
 287     const glyph_count = try readGsubU16(bytes, rule_offset);
 288     const lookup_count = try readGsubU16(bytes, rule_offset + 2);
 289     if (glyph_count == 0) return error.InvalidGsub;
 290     if (@as(usize, glyph_count) > glyph_ids.len - glyph_index) return null;
 291     const input_sequence_offset = rule_offset + 4;
 292     const input_sequence_len = @as(usize, glyph_count - 1) * 2;
 293     if (!hasGsubBytes(bytes, input_sequence_offset, input_sequence_len)) return error.InvalidGsub;
 294     for (1..glyph_count) |sequence_index| {
 295         const glyph = glyph_ids[glyph_index + sequence_index];
 296         if (glyph > std.math.maxInt(u16) or glyph != try readGsubU16(bytes, input_sequence_offset + (sequence_index - 1) * 2)) return null;
 297     }
 298 
 299     const lookup_records_offset = input_sequence_offset + input_sequence_len;
 300     if (!hasGsubBytes(bytes, lookup_records_offset, @as(usize, lookup_count) * 4)) return error.InvalidGsub;
 301     return try self.contextualLookupRecordSubstitution(data, bytes, selection, lookup_records_offset, lookup_count, glyph_ids, glyph_index, glyph_count);
 302 }
 303 
 304 fn chainedContextualSubstitutionImpl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {
 305     if (glyph_index >= glyph_ids.len) return null;
 306     if (!hasGsubBytes(bytes, subtable_offset, 2)) return error.InvalidGsub;
 307     return switch (try readGsubU16(bytes, subtable_offset)) {
 308         1 => self.chainedContextualSubstitutionFormat1(data, bytes, selection, subtable_offset, glyph_ids, glyph_index),
 309         2 => self.chainedContextualSubstitutionFormat2(data, bytes, selection, subtable_offset, glyph_ids, glyph_index),
 310         3 => self.chainedContextualSubstitutionFormat3(data, bytes, selection, subtable_offset, glyph_ids, glyph_index),
 311         else => error.InvalidGsub,
 312     };
 313 }
 314 
 315 fn chainedContextualSubstitutionFormat1Impl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {
 316     if (!hasGsubBytes(bytes, subtable_offset, 8)) return error.InvalidGsub;
 317     const coverage_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, subtable_offset + 2), 2);
 318     const coverage = layout.Coverage.init(bytes, coverage_offset) catch return error.InvalidGsub;
 319     const coverage_index = (coverage.index(bytes, glyph_ids[glyph_index]) catch return error.InvalidGsub) orelse return null;
 320     const rule_set_count = try readGsubU16(bytes, subtable_offset + 4);
 321     if (coverage_index >= rule_set_count) return error.InvalidGsub;
 322     const rule_set_offsets = subtable_offset + 6;
 323     if (!hasGsubBytes(bytes, rule_set_offsets, @as(usize, rule_set_count) * 2)) return error.InvalidGsub;
 324     const rule_set_relative = try readGsubU16(bytes, rule_set_offsets + @as(usize, coverage_index) * 2);
 325     if (rule_set_relative == 0) return null;
 326     const rule_set_offset = try gsubChildOffset(bytes, subtable_offset, rule_set_relative, 2);
 327     return try self.chainedContextualSubstitutionRuleSet(data, bytes, selection, rule_set_offset, glyph_ids, glyph_index);
 328 }
 329 
 330 fn chainedContextualSubstitutionFormat2Impl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {
 331     if (!hasGsubBytes(bytes, subtable_offset, 12)) return error.InvalidGsub;
 332     const coverage_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, subtable_offset + 2), 2);
 333     const coverage = layout.Coverage.init(bytes, coverage_offset) catch return error.InvalidGsub;
 334     if ((coverage.index(bytes, glyph_ids[glyph_index]) catch return error.InvalidGsub) == null) return null;
 335 
 336     const backtrack_class_def_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, subtable_offset + 4), 2);
 337     const input_class_def_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, subtable_offset + 6), 2);
 338     const lookahead_class_def_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, subtable_offset + 8), 2);
 339     const backtrack_class_def = layout.ClassDef.init(bytes, backtrack_class_def_offset) catch return error.InvalidGsub;
 340     const input_class_def = layout.ClassDef.init(bytes, input_class_def_offset) catch return error.InvalidGsub;
 341     const lookahead_class_def = layout.ClassDef.init(bytes, lookahead_class_def_offset) catch return error.InvalidGsub;
 342 
 343     const rule_set_count = try readGsubU16(bytes, subtable_offset + 10);
 344     const rule_set_offsets = subtable_offset + 12;
 345     if (!hasGsubBytes(bytes, rule_set_offsets, @as(usize, rule_set_count) * 2)) return error.InvalidGsub;
 346     const first_class = input_class_def.class(bytes, glyph_ids[glyph_index]) catch return error.InvalidGsub;
 347     if (first_class >= rule_set_count) return null;
 348     const rule_set_relative = try readGsubU16(bytes, rule_set_offsets + @as(usize, first_class) * 2);
 349     if (rule_set_relative == 0) return null;
 350     const rule_set_offset = try gsubChildOffset(bytes, subtable_offset, rule_set_relative, 2);
 351     return try self.chainedContextualSubstitutionClassSet(
 352         data,
 353         bytes,
 354         selection,
 355         backtrack_class_def,
 356         input_class_def,
 357         lookahead_class_def,
 358         rule_set_offset,
 359         glyph_ids,
 360         glyph_index,
 361     );
 362 }
 363 
 364 fn chainedContextualSubstitutionFormat3Impl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {
 365     if (!hasGsubBytes(bytes, subtable_offset, 4)) return error.InvalidGsub;
 366     const backtrack_count = try readGsubU16(bytes, subtable_offset + 2);
 367     const backtrack_offsets = subtable_offset + 4;
 368     if (!hasGsubBytes(bytes, backtrack_offsets, @as(usize, backtrack_count) * 2)) return error.InvalidGsub;
 369     if (@as(usize, backtrack_count) > glyph_index) return null;
 370     for (0..backtrack_count) |backtrack_index| {
 371         const coverage_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, backtrack_offsets + backtrack_index * 2), 2);
 372         const match_index = glyph_index - backtrack_index - 1;
 373         if (!try gsubGlyphInCoverage(bytes, coverage_offset, glyph_ids[match_index])) return null;
 374     }
 375 
 376     const input_count_offset = backtrack_offsets + @as(usize, backtrack_count) * 2;
 377     const input_count = try readGsubU16(bytes, input_count_offset);
 378     if (input_count == 0) return error.InvalidGsub;
 379     const input_offsets = input_count_offset + 2;
 380     if (!hasGsubBytes(bytes, input_offsets, @as(usize, input_count) * 2)) return error.InvalidGsub;
 381     if (@as(usize, input_count) > glyph_ids.len - glyph_index) return null;
 382     for (0..input_count) |input_index| {
 383         const coverage_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, input_offsets + input_index * 2), 2);
 384         if (!try gsubGlyphInCoverage(bytes, coverage_offset, glyph_ids[glyph_index + input_index])) return null;
 385     }
 386 
 387     const lookahead_count_offset = input_offsets + @as(usize, input_count) * 2;
 388     const lookahead_count = try readGsubU16(bytes, lookahead_count_offset);
 389     const lookahead_offsets = lookahead_count_offset + 2;
 390     if (!hasGsubBytes(bytes, lookahead_offsets, @as(usize, lookahead_count) * 2)) return error.InvalidGsub;
 391     const lookahead_start = glyph_index + @as(usize, input_count);
 392     if (@as(usize, lookahead_count) > glyph_ids.len - lookahead_start) return null;
 393     for (0..lookahead_count) |lookahead_index| {
 394         const coverage_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, lookahead_offsets + lookahead_index * 2), 2);
 395         if (!try gsubGlyphInCoverage(bytes, coverage_offset, glyph_ids[lookahead_start + lookahead_index])) return null;
 396     }
 397 
 398     const lookup_count_offset = lookahead_offsets + @as(usize, lookahead_count) * 2;
 399     const lookup_count = try readGsubU16(bytes, lookup_count_offset);
 400     const lookup_records_offset = lookup_count_offset + 2;
 401     if (!hasGsubBytes(bytes, lookup_records_offset, @as(usize, lookup_count) * 4)) return error.InvalidGsub;
 402     return try self.contextualLookupRecordSubstitution(data, bytes, selection, lookup_records_offset, lookup_count, glyph_ids, glyph_index, input_count);
 403 }
 404 
 405 fn chainedContextualSubstitutionRuleSetImpl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, rule_set_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {
 406     const rule_count = try readGsubU16(bytes, rule_set_offset);
 407     const rule_offsets = rule_set_offset + 2;
 408     if (!hasGsubBytes(bytes, rule_offsets, @as(usize, rule_count) * 2)) return error.InvalidGsub;
 409     for (0..rule_count) |rule_index| {
 410         const rule_relative = try readGsubU16(bytes, rule_offsets + rule_index * 2);
 411         if (rule_relative == 0) continue;
 412         const rule_offset = try gsubChildOffset(bytes, rule_set_offset, rule_relative, 2);
 413         if (try self.chainedContextualSubstitutionRule(data, bytes, selection, rule_offset, glyph_ids, glyph_index)) |substitute| return substitute;
 414     }
 415     return null;
 416 }
 417 
 418 fn chainedContextualSubstitutionRuleImpl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, rule_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {
 419     const backtrack_count = try readGsubU16(bytes, rule_offset);
 420     const backtrack_sequence_offset = rule_offset + 2;
 421     if (!hasGsubBytes(bytes, backtrack_sequence_offset, @as(usize, backtrack_count) * 2)) return error.InvalidGsub;
 422     if (@as(usize, backtrack_count) > glyph_index) return null;
 423     for (0..backtrack_count) |backtrack_index| {
 424         const glyph = glyph_ids[glyph_index - backtrack_index - 1];
 425         if (glyph > std.math.maxInt(u16) or glyph != try readGsubU16(bytes, backtrack_sequence_offset + backtrack_index * 2)) return null;
 426     }
 427 
 428     const input_count_offset = backtrack_sequence_offset + @as(usize, backtrack_count) * 2;
 429     const input_count = try readGsubU16(bytes, input_count_offset);
 430     if (input_count == 0) return error.InvalidGsub;
 431     if (@as(usize, input_count) > glyph_ids.len - glyph_index) return null;
 432     const input_sequence_offset = input_count_offset + 2;
 433     const input_sequence_len = @as(usize, input_count - 1) * 2;
 434     if (!hasGsubBytes(bytes, input_sequence_offset, input_sequence_len)) return error.InvalidGsub;
 435     for (1..input_count) |input_index| {
 436         const glyph = glyph_ids[glyph_index + input_index];
 437         if (glyph > std.math.maxInt(u16) or glyph != try readGsubU16(bytes, input_sequence_offset + (input_index - 1) * 2)) return null;
 438     }
 439 
 440     const lookahead_count_offset = input_sequence_offset + input_sequence_len;
 441     const lookahead_count = try readGsubU16(bytes, lookahead_count_offset);
 442     const lookahead_sequence_offset = lookahead_count_offset + 2;
 443     if (!hasGsubBytes(bytes, lookahead_sequence_offset, @as(usize, lookahead_count) * 2)) return error.InvalidGsub;
 444     const lookahead_start = glyph_index + @as(usize, input_count);
 445     if (@as(usize, lookahead_count) > glyph_ids.len - lookahead_start) return null;
 446     for (0..lookahead_count) |lookahead_index| {
 447         const glyph = glyph_ids[lookahead_start + lookahead_index];
 448         if (glyph > std.math.maxInt(u16) or glyph != try readGsubU16(bytes, lookahead_sequence_offset + lookahead_index * 2)) return null;
 449     }
 450 
 451     const lookup_count_offset = lookahead_sequence_offset + @as(usize, lookahead_count) * 2;
 452     const lookup_count = try readGsubU16(bytes, lookup_count_offset);
 453     const lookup_records_offset = lookup_count_offset + 2;
 454     if (!hasGsubBytes(bytes, lookup_records_offset, @as(usize, lookup_count) * 4)) return error.InvalidGsub;
 455     return try self.contextualLookupRecordSubstitution(data, bytes, selection, lookup_records_offset, lookup_count, glyph_ids, glyph_index, input_count);
 456 }
 457 
 458 fn chainedContextualSubstitutionClassSetImpl(
 459     self: Gsub,
 460     data: []const u8,
 461     bytes: []const u8,
 462     selection: LayoutSelection,
 463     backtrack_class_def: layout.ClassDef,
 464     input_class_def: layout.ClassDef,
 465     lookahead_class_def: layout.ClassDef,
 466     rule_set_offset: usize,
 467     glyph_ids: []const u32,
 468     glyph_index: usize,
 469 ) FontError!?GsubSubstitution {
 470     const rule_count = try readGsubU16(bytes, rule_set_offset);
 471     const rule_offsets = rule_set_offset + 2;
 472     if (!hasGsubBytes(bytes, rule_offsets, @as(usize, rule_count) * 2)) return error.InvalidGsub;
 473     for (0..rule_count) |rule_index| {
 474         const rule_relative = try readGsubU16(bytes, rule_offsets + rule_index * 2);
 475         if (rule_relative == 0) continue;
 476         const rule_offset = try gsubChildOffset(bytes, rule_set_offset, rule_relative, 2);
 477         if (try self.chainedContextualSubstitutionClassRule(
 478             data,
 479             bytes,
 480             selection,
 481             backtrack_class_def,
 482             input_class_def,
 483             lookahead_class_def,
 484             rule_offset,
 485             glyph_ids,
 486             glyph_index,
 487         )) |substitute| return substitute;
 488     }
 489     return null;
 490 }
 491 
 492 fn chainedContextualSubstitutionClassRuleImpl(
 493     self: Gsub,
 494     data: []const u8,
 495     bytes: []const u8,
 496     selection: LayoutSelection,
 497     backtrack_class_def: layout.ClassDef,
 498     input_class_def: layout.ClassDef,
 499     lookahead_class_def: layout.ClassDef,
 500     rule_offset: usize,
 501     glyph_ids: []const u32,
 502     glyph_index: usize,
 503 ) FontError!?GsubSubstitution {
 504     const backtrack_count = try readGsubU16(bytes, rule_offset);
 505     const backtrack_sequence_offset = rule_offset + 2;
 506     if (!hasGsubBytes(bytes, backtrack_sequence_offset, @as(usize, backtrack_count) * 2)) return error.InvalidGsub;
 507     if (@as(usize, backtrack_count) > glyph_index) return null;
 508     for (0..backtrack_count) |backtrack_index| {
 509         const glyph = glyph_ids[glyph_index - backtrack_index - 1];
 510         if (glyph > std.math.maxInt(u16)) return null;
 511         const expected_class = try readGsubU16(bytes, backtrack_sequence_offset + backtrack_index * 2);
 512         const actual_class = backtrack_class_def.class(bytes, glyph) catch return error.InvalidGsub;
 513         if (actual_class != expected_class) return null;
 514     }
 515 
 516     const input_count_offset = backtrack_sequence_offset + @as(usize, backtrack_count) * 2;
 517     const input_count = try readGsubU16(bytes, input_count_offset);
 518     if (input_count == 0) return error.InvalidGsub;
 519     if (@as(usize, input_count) > glyph_ids.len - glyph_index) return null;
 520     const input_sequence_offset = input_count_offset + 2;
 521     const input_sequence_len = @as(usize, input_count - 1) * 2;
 522     if (!hasGsubBytes(bytes, input_sequence_offset, input_sequence_len)) return error.InvalidGsub;
 523     for (1..input_count) |input_index| {
 524         const glyph = glyph_ids[glyph_index + input_index];
 525         if (glyph > std.math.maxInt(u16)) return null;
 526         const expected_class = try readGsubU16(bytes, input_sequence_offset + (input_index - 1) * 2);
 527         const actual_class = input_class_def.class(bytes, glyph) catch return error.InvalidGsub;
 528         if (actual_class != expected_class) return null;
 529     }
 530 
 531     const lookahead_count_offset = input_sequence_offset + input_sequence_len;
 532     const lookahead_count = try readGsubU16(bytes, lookahead_count_offset);
 533     const lookahead_sequence_offset = lookahead_count_offset + 2;
 534     if (!hasGsubBytes(bytes, lookahead_sequence_offset, @as(usize, lookahead_count) * 2)) return error.InvalidGsub;
 535     const lookahead_start = glyph_index + @as(usize, input_count);
 536     if (@as(usize, lookahead_count) > glyph_ids.len - lookahead_start) return null;
 537     for (0..lookahead_count) |lookahead_index| {
 538         const glyph = glyph_ids[lookahead_start + lookahead_index];
 539         if (glyph > std.math.maxInt(u16)) return null;
 540         const expected_class = try readGsubU16(bytes, lookahead_sequence_offset + lookahead_index * 2);
 541         const actual_class = lookahead_class_def.class(bytes, glyph) catch return error.InvalidGsub;
 542         if (actual_class != expected_class) return null;
 543     }
 544 
 545     const lookup_count_offset = lookahead_sequence_offset + @as(usize, lookahead_count) * 2;
 546     const lookup_count = try readGsubU16(bytes, lookup_count_offset);
 547     const lookup_records_offset = lookup_count_offset + 2;
 548     if (!hasGsubBytes(bytes, lookup_records_offset, @as(usize, lookup_count) * 4)) return error.InvalidGsub;
 549     return try self.contextualLookupRecordSubstitution(data, bytes, selection, lookup_records_offset, lookup_count, glyph_ids, glyph_index, input_count);
 550 }
 551 
 552 fn contextualLookupRecordSubstitutionImpl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, lookup_records_offset: usize, lookup_count: u16, glyph_ids: []const u32, glyph_index: usize, input_count: u16) FontError!?GsubSubstitution {
 553     for (0..lookup_count) |lookup_record_index| {
 554         const record_offset = lookup_records_offset + lookup_record_index * 4;
 555         const sequence_index = try readGsubU16(bytes, record_offset);
 556         const nested_lookup_index = try readGsubU16(bytes, record_offset + 2);
 557         if (sequence_index >= input_count) return error.InvalidGsub;
 558         const nested_glyph_index = glyph_index + @as(usize, sequence_index);
 559         var substitution = (try self.substitutionAtLookup(data, null, selection, null, nested_lookup_index, glyph_ids, nested_glyph_index, null, false, false)) orelse continue;
 560         if (@as(usize, substitution.component_count) > @as(usize, input_count) - @as(usize, sequence_index)) return error.InvalidGsub;
 561         substitution.target_offset = @intCast(sequence_index);
 562         substitution.skip_count = @as(usize, input_count) - @as(usize, substitution.component_count) + @as(usize, substitution.replacement_count);
 563         return substitution;
 564     }
 565     return null;
 566 }
 567 
 568 fn lookupIsReverseImpl(self: Gsub, data: []const u8, selection: LayoutSelection, lookup_index: u16) FontError!bool {
 569     _ = selection;
 570     const bytes = data[self.table.offset..][0..self.table.len];
 571     const lookup_offset = try self.lookupOffset(bytes, lookup_index);
 572     const lookup_type = try readGsubU16(bytes, lookup_offset);
 573     if (lookup_type == 8) return true;
 574     if (lookup_type != 7) return false;
 575     const subtable_count = try readGsubU16(bytes, lookup_offset + 4);
 576     if (!hasGsubBytes(bytes, lookup_offset + 6, @as(usize, subtable_count) * 2)) return error.InvalidGsub;
 577     for (0..subtable_count) |i| {
 578         const subtable_offset = try gsubChildOffset(bytes, lookup_offset, try readGsubU16(bytes, lookup_offset + 6 + i * 2), 2);
 579         const resolved = try resolveGsubSubtable(bytes, lookup_type, subtable_offset);
 580         if (resolved.lookup_type == 8) return true;
 581     }
 582     return false;
 583 }
 584 
 585 fn lookupIsActiveImpl(self: Gsub, bytes: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, lookup_index: u16, source: ?FeatureRange) FontError!bool {
 586     const langsys_offset = (try gsubLangSysOffset(bytes, self.script_list_offset, selection)) orelse return false;
 587     const required_feature_index = try readGsubU16(bytes, langsys_offset + 2);
 588     if (required_feature_index != 0xffff) {
 589         const feature_index = required_feature_index;
 590         const feature_tag = try self.featureTag(bytes, feature_index);
 591         if (try self.featureContainsLookup(bytes, face, selection, feature_index, lookup_index)) {
 592             if (try self.featureAllowsLookup(bytes, feature_tag, lookup_index)) return true;
 593         }
 594     }
 595 
 596     const feature_count = try readGsubU16(bytes, langsys_offset + 4);
 597     if (!hasGsubBytes(bytes, langsys_offset + 6, @as(usize, feature_count) * 2)) return error.InvalidGsub;
 598     for (0..feature_count) |i| {
 599         const feature_index = try readGsubU16(bytes, langsys_offset + 6 + i * 2);
 600         const feature_tag = try self.featureTag(bytes, feature_index);
 601         if (!gsubFeatureEnabled(selection, feature_tag, try self.featureDefaultOn(bytes, selection, feature_tag), source)) continue;
 602         if (try self.featureContainsLookup(bytes, face, selection, feature_index, lookup_index)) {
 603             if (try self.featureAllowsLookup(bytes, feature_tag, lookup_index)) return true;
 604         }
 605     }
 606     return false;
 607 }
 608 
 609 fn featureDefaultOnImpl(self: Gsub, bytes: []const u8, selection: LayoutSelection, feature_tag: Tag) FontError!bool {
 610     return switch (feature_tag) {
 611         tagLiteral("rlig"), tagLiteral("ccmp"), tagLiteral("rvrn"), tagLiteral("locl") => true,
 612         tagLiteral("liga"), tagLiteral("clig"), tagLiteral("calt"), tagLiteral("rclt") => !selection.vertical,
 613         tagLiteral("ltra"), tagLiteral("ltrm") => !selection.right_to_left,
 614         tagLiteral("rtla"), tagLiteral("rtlm") => selection.right_to_left,
 615         tagLiteral("vrt2") => selection.vertical,
 616         tagLiteral("vert") => selection.vertical and !(try self.langSysHasFeature(bytes, selection, tagLiteral("vrt2"))),
 617         else => false,
 618     };
 619 }
 620 
 621 fn featureAllowsLookupImpl(self: Gsub, bytes: []const u8, feature_tag: Tag, lookup_index: u16) FontError!bool {
 622     if (feature_tag != tagLiteral("rvrn")) return true;
 623     return try self.lookupResolvesOnlyToType(bytes, lookup_index, 1);
 624 }
 625 
 626 fn lookupResolvesOnlyToTypeImpl(self: Gsub, bytes: []const u8, lookup_index: u16, wanted_lookup_type: u16) FontError!bool {
 627     const lookup_offset = try self.lookupOffset(bytes, lookup_index);
 628     const lookup_type = try readGsubU16(bytes, lookup_offset);
 629     if (lookup_type == wanted_lookup_type) return true;
 630     if (lookup_type != 7) return false;
 631     const subtable_count = try readGsubU16(bytes, lookup_offset + 4);
 632     if (!hasGsubBytes(bytes, lookup_offset + 6, @as(usize, subtable_count) * 2)) return error.InvalidGsub;
 633     var saw_subtable = false;
 634     for (0..subtable_count) |i| {
 635         const subtable_offset = try gsubChildOffset(bytes, lookup_offset, try readGsubU16(bytes, lookup_offset + 6 + i * 2), 2);
 636         const resolved = try resolveGsubSubtable(bytes, lookup_type, subtable_offset);
 637         if (resolved.lookup_type != wanted_lookup_type) return false;
 638         saw_subtable = true;
 639     }
 640     return saw_subtable;
 641 }
 642 
 643 fn langSysHasFeatureImpl(self: Gsub, bytes: []const u8, selection: LayoutSelection, wanted_tag: Tag) FontError!bool {
 644     const langsys_offset = (try gsubLangSysOffset(bytes, self.script_list_offset, selection)) orelse return false;
 645     const required_feature_index = try readGsubU16(bytes, langsys_offset + 2);
 646     if (required_feature_index != 0xffff and (try self.featureTag(bytes, required_feature_index)) == wanted_tag) return true;
 647 
 648     const feature_count = try readGsubU16(bytes, langsys_offset + 4);
 649     if (!hasGsubBytes(bytes, langsys_offset + 6, @as(usize, feature_count) * 2)) return error.InvalidGsub;
 650     for (0..feature_count) |i| {
 651         const feature_index = try readGsubU16(bytes, langsys_offset + 6 + i * 2);
 652         if ((try self.featureTag(bytes, feature_index)) == wanted_tag) return true;
 653     }
 654     return false;
 655 }
 656 
 657 fn usesAutomaticFractionsImpl(self: Gsub, data: []const u8, selection: LayoutSelection) FontError!bool {
 658     const table_bytes = data[self.table.offset..][0..self.table.len];
 659     if (try self.langSysHasFeature(table_bytes, selection, tagLiteral("frac"))) return true;
 660     return (try self.langSysHasFeature(table_bytes, selection, tagLiteral("numr"))) and
 661         (try self.langSysHasFeature(table_bytes, selection, tagLiteral("dnom")));
 662 }
 663 
 664 fn usesArabicJoiningFormsImpl(self: Gsub, data: []const u8, selection: LayoutSelection) FontError!bool {
 665     const table_bytes = data[self.table.offset..][0..self.table.len];
 666     const feature_tags = [_]Tag{
 667         tagLiteral("isol"),
 668         tagLiteral("fina"),
 669         tagLiteral("medi"),
 670         tagLiteral("init"),
 671     };
 672     for (feature_tags) |feature_tag| {
 673         if (try self.langSysHasFeature(table_bytes, selection, feature_tag)) return true;
 674     }
 675     return false;
 676 }
 677 
 678 fn featureTagImpl(self: Gsub, bytes: []const u8, feature_index: u16) FontError!Tag {
 679     const feature_record = try self.featureRecord(bytes, feature_index);
 680     return try readGsubU32(bytes, feature_record);
 681 }
 682 
 683 fn featureContainsLookupImpl(self: Gsub, bytes: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, feature_index: u16, lookup_index: u16) FontError!bool {
 684     const feature_offset = try self.featureOffset(bytes, face, selection, feature_index);
 685     return try featureTableContainsLookup(.gsub, bytes, feature_offset, lookup_index);
 686 }
 687 
 688 fn featureOffsetImpl(self: Gsub, bytes: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, feature_index: u16) FontError!usize {
 689     if (try matchingFeatureSubstitutionOffset(.gsub, bytes, self.feature_variations_offset, face, selection)) |substitution_offset| {
 690         if (try substitutedFeatureOffset(.gsub, bytes, substitution_offset, feature_index)) |alternate_offset| return alternate_offset;
 691     }
 692     const feature_record = try self.featureRecord(bytes, feature_index);
 693     return try gsubChildOffset(bytes, self.feature_list_offset, try readGsubU16(bytes, feature_record + 4), 4);
 694 }
 695 
 696 fn featureRecordImpl(self: Gsub, bytes: []const u8, feature_index: u16) FontError!usize {
 697     const feature_count = try readGsubU16(bytes, self.feature_list_offset);
 698     if (feature_index >= feature_count) return error.InvalidGsub;
 699     const feature_record = self.feature_list_offset + 2 + @as(usize, feature_index) * 6;
 700     if (!hasGsubBytes(bytes, feature_record, 6)) return error.InvalidGsub;
 701     return feature_record;
 702 }
 703 
 704 fn lookupOffsetImpl(self: Gsub, bytes: []const u8, lookup_index: u16) FontError!usize {
 705     const lookup_count = try readGsubU16(bytes, self.lookup_list_offset);
 706     if (lookup_index >= lookup_count) return error.InvalidGsub;
 707     const lookup_record = self.lookup_list_offset + 2 + @as(usize, lookup_index) * 2;
 708     return try gsubChildOffset(bytes, self.lookup_list_offset, try readGsubU16(bytes, lookup_record), 6);
 709 }
 710 
 711 fn gsubFeatureEnabled(selection: LayoutSelection, feature_tag: Tag, default_on: bool, source: ?FeatureRange) bool {
 712     if (feature_tag == tagLiteral("rclt") and default_on) return true;
 713     return featureEnabled(selection, feature_tag, default_on, source);
 714 }
 715 
 716 fn gsubLangSysOffset(data: []const u8, script_list_offset: usize, selection: LayoutSelection) FontError!?usize {
 717     if (selection.script_tags.len != 0) {
 718         for (selection.script_tags) |script_tag| {
 719             if (try gsubLangSysOffsetForScript(data, script_list_offset, script_tag, selection.language)) |langsys_offset| return langsys_offset;
 720         }
 721         if (!scriptTagListContains(selection.script_tags, defaultScriptTag)) {
 722             return try gsubLangSysOffsetForScript(data, script_list_offset, defaultScriptTag, selection.language);
 723         }
 724         return null;
 725     }
 726     if (try gsubScriptOffset(data, script_list_offset, selection.script)) |script_offset| {
 727         return try gsubLangSysOffsetInScript(data, script_offset, selection.language);
 728     }
 729     if (selection.script != defaultScriptTag) {
 730         if (try gsubScriptOffset(data, script_list_offset, defaultScriptTag)) |script_offset| {
 731             return try gsubLangSysOffsetInScript(data, script_offset, selection.language);
 732         }
 733     }
 734     return null;
 735 }
 736 
 737 fn gsubLangSysOffsetForScript(data: []const u8, script_list_offset: usize, script_tag: Tag, language: ?Tag) FontError!?usize {
 738     const script_offset = (try gsubScriptOffset(data, script_list_offset, script_tag)) orelse return null;
 739     return try gsubLangSysOffsetInScript(data, script_offset, language);
 740 }
 741 
 742 fn gsubScriptOffset(data: []const u8, script_list_offset: usize, script_tag: Tag) FontError!?usize {
 743     const script_count = try readGsubU16(data, script_list_offset);
 744     if (!hasGsubBytes(data, script_list_offset + 2, @as(usize, script_count) * 6)) return error.InvalidGsub;
 745     for (0..script_count) |i| {
 746         const record = script_list_offset + 2 + i * 6;
 747         if (try readGsubU32(data, record) != script_tag) continue;
 748         return try gsubChildOffset(data, script_list_offset, try readGsubU16(data, record + 4), 4);
 749     }
 750     return null;
 751 }
 752 
 753 fn gsubLangSysOffsetInScript(data: []const u8, script_offset: usize, language: ?Tag) FontError!?usize {
 754     if (language) |language_tag| {
 755         const langsys_count = try readGsubU16(data, script_offset + 2);
 756         const records_offset = script_offset + 4;
 757         if (!hasGsubBytes(data, records_offset, @as(usize, langsys_count) * 6)) return error.InvalidGsub;
 758         for (0..langsys_count) |i| {
 759             const record = records_offset + i * 6;
 760             if (try readGsubU32(data, record) != language_tag) continue;
 761             return try gsubChildOffset(data, script_offset, try readGsubU16(data, record + 4), 6);
 762         }
 763     }
 764 
 765     const default_langsys_offset = try readGsubU16(data, script_offset);
 766     if (default_langsys_offset == 0) return null;
 767     return try gsubChildOffset(data, script_offset, default_langsys_offset, 6);
 768 }
 769 
 770 fn gsubGlyphInCoverage(data: []const u8, coverage_offset: usize, glyph_id: u32) FontError!bool {
 771     const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGsub;
 772     return (coverage.index(data, glyph_id) catch return error.InvalidGsub) != null;
 773 }
 774 
 775 const ResolvedGsubSubtable = struct {
 776     lookup_type: u16,
 777     subtable_offset: usize,
 778 };
 779 
 780 fn resolveGsubSubtable(data: []const u8, lookup_type: u16, subtable_offset: usize) FontError!ResolvedGsubSubtable {
 781     if (lookup_type != 7) return .{ .lookup_type = lookup_type, .subtable_offset = subtable_offset };
 782     if (!hasGsubBytes(data, subtable_offset, 8)) return error.InvalidGsub;
 783     if (try readGsubU16(data, subtable_offset) != 1) return error.InvalidGsub;
 784     const extension_lookup_type = try readGsubU16(data, subtable_offset + 2);
 785     if (extension_lookup_type == 7) return error.InvalidGsub;
 786     const extension_offset = try readGsubU32(data, subtable_offset + 4);
 787     return .{
 788         .lookup_type = extension_lookup_type,
 789         .subtable_offset = try gsubChildOffset32(data, subtable_offset, extension_offset, 2),
 790     };
 791 }
 792 
 793 fn singleSubstitution(data: []const u8, subtable_offset: usize, glyph_id: u32) FontError!?u32 {
 794     if (glyph_id > std.math.maxInt(u16)) return null;
 795     if (!hasGsubBytes(data, subtable_offset, 2)) return error.InvalidGsub;
 796     return switch (try readGsubU16(data, subtable_offset)) {
 797         1 => singleSubstitutionFormat1(data, subtable_offset, @intCast(glyph_id)),
 798         2 => singleSubstitutionFormat2(data, subtable_offset, @intCast(glyph_id)),
 799         else => error.InvalidGsub,
 800     };
 801 }
 802 
 803 fn singleSubstitutionFormat1(data: []const u8, subtable_offset: usize, glyph_id: u16) FontError!?u32 {
 804     if (!hasGsubBytes(data, subtable_offset, 6)) return error.InvalidGsub;
 805     const coverage_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, subtable_offset + 2), 2);
 806     const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGsub;
 807     if ((coverage.index(data, glyph_id) catch return error.InvalidGsub) == null) return null;
 808     const delta = try readGsubI16(data, subtable_offset + 4);
 809     return @intCast(@mod(@as(i32, glyph_id) + @as(i32, delta), 65536));
 810 }
 811 
 812 fn singleSubstitutionFormat2(data: []const u8, subtable_offset: usize, glyph_id: u16) FontError!?u32 {
 813     if (!hasGsubBytes(data, subtable_offset, 6)) return error.InvalidGsub;
 814     const coverage_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, subtable_offset + 2), 2);
 815     const glyph_count = try readGsubU16(data, subtable_offset + 4);
 816     const substitute_offset = subtable_offset + 6;
 817     if (!hasGsubBytes(data, substitute_offset, @as(usize, glyph_count) * 2)) return error.InvalidGsub;
 818     const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGsub;
 819     const coverage_index = (coverage.index(data, glyph_id) catch return error.InvalidGsub) orelse return null;
 820     if (coverage_index >= glyph_count) return error.InvalidGsub;
 821     return try readGsubU16(data, substitute_offset + @as(usize, coverage_index) * 2);
 822 }
 823 
 824 fn multipleSubstitution(data: []const u8, table_offset: usize, subtable_offset: usize, glyph_id: u32) FontError!?GsubSubstitution {
 825     if (glyph_id > std.math.maxInt(u16)) return null;
 826     if (!hasGsubBytes(data, subtable_offset, 6)) return error.InvalidGsub;
 827     if (try readGsubU16(data, subtable_offset) != 1) return error.InvalidGsub;
 828     const coverage_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, subtable_offset + 2), 2);
 829     const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGsub;
 830     const coverage_index = (coverage.index(data, glyph_id) catch return error.InvalidGsub) orelse return null;
 831     const sequence_count = try readGsubU16(data, subtable_offset + 4);
 832     if (coverage_index >= sequence_count) return error.InvalidGsub;
 833     const sequence_offsets = subtable_offset + 6;
 834     if (!hasGsubBytes(data, sequence_offsets, @as(usize, sequence_count) * 2)) return error.InvalidGsub;
 835     const sequence_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, sequence_offsets + @as(usize, coverage_index) * 2), 2);
 836     const glyph_count = try readGsubU16(data, sequence_offset);
 837     if (glyph_count == 0) return error.InvalidGsub;
 838     const substitute_offset = sequence_offset + 2;
 839     if (!hasGsubBytes(data, substitute_offset, @as(usize, glyph_count) * 2)) return error.InvalidGsub;
 840     if (table_offset > std.math.maxInt(usize) - substitute_offset) return error.InvalidGsub;
 841     return .{
 842         .glyph_id = try readGsubU16(data, substitute_offset),
 843         .component_count = 1,
 844         .replacement_count = glyph_count,
 845         .replacement_glyphs_offset = table_offset + substitute_offset,
 846     };
 847 }
 848 
 849 fn alternateSubstitution(data: []const u8, subtable_offset: usize, glyph_id: u32) FontError!?u32 {
 850     if (glyph_id > std.math.maxInt(u16)) return null;
 851     if (!hasGsubBytes(data, subtable_offset, 6)) return error.InvalidGsub;
 852     if (try readGsubU16(data, subtable_offset) != 1) return error.InvalidGsub;
 853     const coverage_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, subtable_offset + 2), 2);
 854     const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGsub;
 855     const coverage_index = (coverage.index(data, glyph_id) catch return error.InvalidGsub) orelse return null;
 856     const alternate_set_count = try readGsubU16(data, subtable_offset + 4);
 857     if (coverage_index >= alternate_set_count) return error.InvalidGsub;
 858     const alternate_set_offsets = subtable_offset + 6;
 859     if (!hasGsubBytes(data, alternate_set_offsets, @as(usize, alternate_set_count) * 2)) return error.InvalidGsub;
 860     const alternate_set_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, alternate_set_offsets + @as(usize, coverage_index) * 2), 2);
 861     const glyph_count = try readGsubU16(data, alternate_set_offset);
 862     if (glyph_count == 0) return error.InvalidGsub;
 863     if (!hasGsubBytes(data, alternate_set_offset + 2, @as(usize, glyph_count) * 2)) return error.InvalidGsub;
 864     return try readGsubU16(data, alternate_set_offset + 2);
 865 }
 866 
 867 fn ligatureSubstitution(data: []const u8, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {
 868     if (glyph_index >= glyph_ids.len or glyph_ids[glyph_index] > std.math.maxInt(u16)) return null;
 869     if (!hasGsubBytes(data, subtable_offset, 8)) return error.InvalidGsub;
 870     if (try readGsubU16(data, subtable_offset) != 1) return error.InvalidGsub;
 871     const coverage_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, subtable_offset + 2), 2);
 872     const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGsub;
 873     const coverage_index = (coverage.index(data, glyph_ids[glyph_index]) catch return error.InvalidGsub) orelse return null;
 874     const ligature_set_count = try readGsubU16(data, subtable_offset + 4);
 875     if (coverage_index >= ligature_set_count) return error.InvalidGsub;
 876     const ligature_set_offsets = subtable_offset + 6;
 877     if (!hasGsubBytes(data, ligature_set_offsets, @as(usize, ligature_set_count) * 2)) return error.InvalidGsub;
 878     const ligature_set_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, ligature_set_offsets + @as(usize, coverage_index) * 2), 2);
 879     const ligature_count = try readGsubU16(data, ligature_set_offset);
 880     if (!hasGsubBytes(data, ligature_set_offset + 2, @as(usize, ligature_count) * 2)) return error.InvalidGsub;
 881 
 882     for (0..ligature_count) |i| {
 883         const ligature_offset = try gsubChildOffset(data, ligature_set_offset, try readGsubU16(data, ligature_set_offset + 2 + i * 2), 4);
 884         const ligature_glyph = try readGsubU16(data, ligature_offset);
 885         const component_count = try readGsubU16(data, ligature_offset + 2);
 886         if (component_count < 2) return error.InvalidGsub;
 887         if (glyph_index + @as(usize, component_count) > glyph_ids.len) continue;
 888         const component_offset = ligature_offset + 4;
 889         if (!hasGsubBytes(data, component_offset, @as(usize, component_count - 1) * 2)) return error.InvalidGsub;
 890         var matched = true;
 891         for (1..component_count) |component_index| {
 892             const glyph = glyph_ids[glyph_index + component_index];
 893             if (glyph > std.math.maxInt(u16) or glyph != try readGsubU16(data, component_offset + (component_index - 1) * 2)) {
 894                 matched = false;
 895                 break;
 896             }
 897         }
 898         if (matched) {
 899             return .{ .glyph_id = ligature_glyph, .component_count = component_count };
 900         }
 901     }
 902     return null;
 903 }
 904 
 905 fn reverseChainingSingleSubstitution(data: []const u8, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?u32 {
 906     if (glyph_index >= glyph_ids.len or glyph_ids[glyph_index] > std.math.maxInt(u16)) return null;
 907     if (!hasGsubBytes(data, subtable_offset, 6)) return error.InvalidGsub;
 908     if (try readGsubU16(data, subtable_offset) != 1) return error.InvalidGsub;
 909 
 910     const coverage_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, subtable_offset + 2), 2);
 911     const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGsub;
 912     const coverage_index = (coverage.index(data, glyph_ids[glyph_index]) catch return error.InvalidGsub) orelse return null;
 913 
 914     const backtrack_count = try readGsubU16(data, subtable_offset + 4);
 915     const backtrack_offsets = subtable_offset + 6;
 916     if (!hasGsubBytes(data, backtrack_offsets, @as(usize, backtrack_count) * 2)) return error.InvalidGsub;
 917     if (glyph_index < @as(usize, backtrack_count)) return null;
 918     for (0..backtrack_count) |i| {
 919         const backtrack_coverage_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, backtrack_offsets + i * 2), 2);
 920         if (!try coverageContainsGlyph(data, backtrack_coverage_offset, glyph_ids[glyph_index - 1 - i])) return null;
 921     }
 922 
 923     const lookahead_count_offset = backtrack_offsets + @as(usize, backtrack_count) * 2;
 924     if (!hasGsubBytes(data, lookahead_count_offset, 2)) return error.InvalidGsub;
 925     const lookahead_count = try readGsubU16(data, lookahead_count_offset);
 926     const lookahead_offsets = lookahead_count_offset + 2;
 927     if (!hasGsubBytes(data, lookahead_offsets, @as(usize, lookahead_count) * 2)) return error.InvalidGsub;
 928     if (@as(usize, lookahead_count) > glyph_ids.len - glyph_index - 1) return null;
 929     for (0..lookahead_count) |i| {
 930         const lookahead_coverage_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, lookahead_offsets + i * 2), 2);
 931         if (!try coverageContainsGlyph(data, lookahead_coverage_offset, glyph_ids[glyph_index + 1 + i])) return null;
 932     }
 933 
 934     const glyph_count_offset = lookahead_offsets + @as(usize, lookahead_count) * 2;
 935     if (!hasGsubBytes(data, glyph_count_offset, 2)) return error.InvalidGsub;
 936     const glyph_count = try readGsubU16(data, glyph_count_offset);
 937     if (coverage_index >= glyph_count) return error.InvalidGsub;
 938     const substitute_offset = glyph_count_offset + 2;
 939     if (!hasGsubBytes(data, substitute_offset, @as(usize, glyph_count) * 2)) return error.InvalidGsub;
 940     return try readGsubU16(data, substitute_offset + @as(usize, coverage_index) * 2);
 941 }
 942 
 943 fn coverageContainsGlyph(data: []const u8, coverage_offset: usize, glyph_id: u32) FontError!bool {
 944     if (glyph_id > std.math.maxInt(u16)) return false;
 945     const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGsub;
 946     return (coverage.index(data, glyph_id) catch return error.InvalidGsub) != null;
 947 }
 948 test "GSUB extension substitution dispatches wrapped single substitutions" {
 949     const data = [_]u8{
 950         0, 1, 0,   0,   0,    10,   0, 30, 0, 44,
 951         0, 1, 'D', 'F', 'L',  'T',  0, 8,  0, 4,
 952         0, 0, 0,   0,   0xff, 0xff, 0, 1,  0, 0,
 953         0, 1, 'l', 'i', 'g',  'a',  0, 8,  0, 0,
 954         0, 1, 0,   0,   0,    1,    0, 4,  0, 7,
 955         0, 0, 0,   1,   0,    8,    0, 1,  0, 1,
 956         0, 0, 0,   8,   0,    1,    0, 6,  0, 4,
 957         0, 1, 0,   1,   0,    1,
 958     };
 959     const gsub = try Gsub.init(&data, .{ .offset = 0, .len = data.len });
 960     const glyphs = [_]u32{1};
 961     const substitution = (try gsub.substitutionAt(&data, .{}, 0, &glyphs, 0)).?;
 962 
 963     try std.testing.expectEqual(@as(u32, 5), substitution.glyph_id);
 964     try std.testing.expectEqual(@as(u16, 1), substitution.component_count);
 965 }
 966 
 967 test "GSUB reverse chaining lookup dispatches contextual single substitutions" {
 968     const data = [_]u8{
 969         0, 1,        0,   0,   0,    10,       0, 30, 0, 44,
 970         0, 1,        'D', 'F', 'L',  'T',      0, 8,  0, 4,
 971         0, 0,        0,   0,   0xff, 0xff,     0, 1,  0, 0,
 972         0, 1,        'r', 'l', 'i',  'g',      0, 8,  0, 0,
 973         0, 1,        0,   0,   0,    1,        0, 4,  0, 8,
 974         0, 0,        0,   1,   0,    8,        0, 1,  0, 14,
 975         0, 0,        0,   1,   0,    20,       0, 1,  0, 'B' - 31,
 976         0, 1,        0,   1,   0,    'A' - 31, 0, 1,  0, 1,
 977         0, 'A' - 31,
 978     };
 979     const gsub = try Gsub.init(&data, .{ .offset = 0, .len = data.len });
 980     const glyphs = [_]u32{ 'A' - 31, 'A' - 31, 'A' - 31 };
 981     const substitution = (try gsub.substitutionAt(&data, .{}, 0, &glyphs, 1)).?;
 982 
 983     try std.testing.expect(try gsub.lookupIsReverse(&data, .{}, 0));
 984     try std.testing.expectEqual(@as(u32, 'B' - 31), substitution.glyph_id);
 985     try std.testing.expectEqual(@as(u16, 1), substitution.component_count);
 986     try std.testing.expectEqual(@as(?GsubSubstitution, null), try gsub.substitutionAt(&data, .{}, 0, &glyphs, 2));
 987 }
 988 
 989 test "GSUB offsets stay inside table data" {
 990     const data = @as([10]u8, @splat(0));
 991     try std.testing.expectEqual(@as(usize, 8), try gsubOffset(&data, 8));
 992     try std.testing.expectError(error.InvalidGsub, gsubOffset(&data, 0));
 993     try std.testing.expectError(error.InvalidGsub, gsubOffset(&data, 9));
 994     try std.testing.expectError(error.InvalidGsub, gsubOffset(&data, 10));
 995 }
 996 
 997 test "GSUB child offsets stay inside table data" {
 998     const data = @as([10]u8, @splat(0));
 999     try std.testing.expectEqual(@as(usize, 6), try gsubChildOffset(&data, 2, 4, 2));
1000     try std.testing.expectError(error.InvalidGsub, gsubChildOffset(&data, 2, 0, 2));
1001     try std.testing.expectError(error.InvalidGsub, gsubChildOffset(&data, 10, 1, 2));
1002     try std.testing.expectError(error.InvalidGsub, gsubChildOffset(&data, 8, 1, 2));
1003 }
1004 
1005 test "GSUB layout selection resolves language system tags" {
1006     const data = [_]u8{
1007         0,    1,
1008         'l',  'a',
1009         't',  'n',
1010         0,    8,
1011         0,    18,
1012         0,    1,
1013         'T',  'R',
1014         'K',  ' ',
1015         0,    10,
1016         0,    0,
1017         0xff, 0xff,
1018         0,    1,
1019         0,    7,
1020         0,    0,
1021         0xff, 0xff,
1022         0,    1,
1023         0,    3,
1024     };
1025 
1026     try std.testing.expectEqual(@as(?usize, 18), try gsubLangSysOffset(&data, 0, .{ .script = tag("latn"), .language = tag("TRK ") }));
1027     try std.testing.expectEqual(@as(?usize, 26), try gsubLangSysOffset(&data, 0, .{ .script = tag("latn"), .language = tag("DEU ") }));
1028     try std.testing.expectEqual(@as(?usize, null), try gsubLangSysOffset(&data, 0, .{ .script = tag("cyrl"), .language = tag("TRK ") }));
1029 }
1030 
1031 test "GSUB single substitution format 1 applies signed delta" {
1032     const data = [_]u8{
1033         0, 1,
1034         0, 6,
1035         0, 2,
1036         0, 1,
1037         0, 1,
1038         0, 5,
1039     };
1040     try std.testing.expectEqual(@as(?u32, 7), try singleSubstitution(&data, 0, 5));
1041     try std.testing.expectEqual(@as(?u32, null), try singleSubstitution(&data, 0, 4));
1042 }
1043 
1044 test "GSUB single substitution format 1 wraps negative delta" {
1045     const data = [_]u8{
1046         0,    1,
1047         0,    6,
1048         0xff, 0xfe,
1049         0,    1,
1050         0,    1,
1051         0,    1,
1052     };
1053     try std.testing.expectEqual(@as(?u32, 65535), try singleSubstitution(&data, 0, 1));
1054 }
1055 
1056 test "GSUB single substitution format 2 uses coverage index" {
1057     const data = [_]u8{
1058         0, 2,
1059         0, 10,
1060         0, 2,
1061         0, 70,
1062         0, 80,
1063         0, 1,
1064         0, 2,
1065         0, 5,
1066         0, 9,
1067     };
1068     try std.testing.expectEqual(@as(?u32, 70), try singleSubstitution(&data, 0, 5));
1069     try std.testing.expectEqual(@as(?u32, 80), try singleSubstitution(&data, 0, 9));
1070     try std.testing.expectEqual(@as(?u32, null), try singleSubstitution(&data, 0, 6));
1071 }
1072 
1073 test "GSUB multiple substitution format 1 uses sequence table" {
1074     const data = [_]u8{
1075         0, 1,
1076         0, 14,
1077         0, 1,
1078         0, 8,
1079         0, 2,
1080         0, 'X' - 31,
1081         0, 'Y' - 31,
1082         0, 1,
1083         0, 1,
1084         0, 'A' - 31,
1085     };
1086     const substitution = (try multipleSubstitution(&data, 0, 0, 'A' - 31)).?;
1087 
1088     try std.testing.expectEqual(@as(u32, 'X' - 31), substitution.glyph_id);
1089     try std.testing.expectEqual(@as(u16, 1), substitution.component_count);
1090     try std.testing.expectEqual(@as(u16, 2), substitution.replacement_count);
1091     try std.testing.expectEqual(@as(usize, 10), substitution.replacement_glyphs_offset);
1092     try std.testing.expectEqual(@as(?GsubSubstitution, null), try multipleSubstitution(&data, 0, 0, 'B' - 31));
1093 }
1094 
1095 test "GSUB multiple substitution rejects deletion sequences" {
1096     const data = [_]u8{
1097         0, 1,
1098         0, 10,
1099         0, 1,
1100         0, 8,
1101         0, 0,
1102         0, 1,
1103         0, 1,
1104         0, 'A' - 31,
1105     };
1106 
1107     try std.testing.expectError(error.InvalidGsub, multipleSubstitution(&data, 0, 0, 'A' - 31));
1108 }
1109 
1110 test "GSUB ligature substitution format 1 uses preferred ligature set order" {
1111     const data = [_]u8{
1112         0, 1,
1113         0, 18,
1114         0, 1,
1115         0, 8,
1116         0, 1,
1117         0, 4,
1118         0, 96,
1119         0, 2,
1120         0, 'i' - 31,
1121         0, 1,
1122         0, 1,
1123         0, 'f' - 31,
1124     };
1125     const glyphs = [_]u32{ 'f' - 31, 'i' - 31, '!' - 31 };
1126     const substitution = (try ligatureSubstitution(&data, 0, &glyphs, 0)).?;
1127     try std.testing.expectEqual(@as(u32, 96), substitution.glyph_id);
1128     try std.testing.expectEqual(@as(u16, 2), substitution.component_count);
1129     try std.testing.expectEqual(@as(?GsubSubstitution, null), try ligatureSubstitution(&data, 0, &glyphs, 1));
1130 }
1131 
1132 test "GSUB reverse chaining single substitution format 1 uses context coverage" {
1133     const data = [_]u8{
1134         0, 1,
1135         0, 16,
1136         0, 1,
1137         0, 22,
1138         0, 1,
1139         0, 28,
1140         0, 1,
1141         0, 20,
1142         0, 1,
1143         0, 1,
1144         0, 2,
1145         0, 1,
1146         0, 1,
1147         0, 1,
1148         0, 1,
1149         0, 1,
1150         0, 3,
1151     };
1152     const glyphs = [_]u32{ 1, 2, 3 };
1153     const mismatch = [_]u32{ 3, 2, 1 };
1154 
1155     try std.testing.expectEqual(@as(?u32, 20), try reverseChainingSingleSubstitution(&data, 0, &glyphs, 1));
1156     try std.testing.expectEqual(@as(?u32, null), try reverseChainingSingleSubstitution(&data, 0, &glyphs, 0));
1157     try std.testing.expectEqual(@as(?u32, null), try reverseChainingSingleSubstitution(&data, 0, &mismatch, 1));
1158 }