Skip to documentation
SLOP

tiny.filigree.font.gsub

Reference tiny.filigree font gsub

Defined in font.

API (8)

Types and contracts

Public types and contracts.

Values and defaults

Public values and defaults.

No direct callersNo direct callsfontgsub
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Source: lib/filigree/src/font/gsub.zig

zig
const std = @import("std");const binary = @import("binary.zig");const gdef_table = @import("gdef.zig");const layout = @import("../layout/root.zig");const lookups = @import("lookups.zig");const model = @import("model.zig");const FontError = model.FontError;const FeatureRange = model.FeatureRange;const Gdef = gdef_table.Gdef;const GsubSubstitution = model.GsubSubstitution;const LayoutSelection = model.LayoutSelection;const Table = binary.Table;const Tag = model.Tag;const defaultScriptTag = model.defaultScriptTag;const featureEnabled = model.featureEnabled;const tagLiteral = model.tag;const featureTableContainsLookup = lookups.featureTableContainsLookup;const gsubChildOffset = lookups.gsubChildOffset;const gsubChildOffset32 = lookups.gsubChildOffset32;const gsubLookupFilter = lookups.gsubLookupFilter;const gsubOffset = lookups.gsubOffset;const hasGsubBytes = lookups.gsubHasBytes;const lookupGlyphIgnored = lookups.lookupGlyphIgnored;const lookupFilterSkipsGlyphs = lookups.lookupFilterSkipsGlyphs;const matchingFeatureSubstitutionOffset = lookups.matchingFeatureSubstitutionOffset;const readGsubI16 = lookups.gsubReadI16;const readGsubU16 = lookups.gsubReadU16;const readGsubU32 = lookups.gsubReadU32;const scriptTagListContains = lookups.scriptTagListContains;const substitutedFeatureOffset = lookups.substitutedFeatureOffset;const tag = model.tag;pub const Gsub = struct {    table: Table,    script_list_offset: usize,    feature_list_offset: usize,    lookup_list_offset: usize,    feature_variations_offset: ?usize,    pub const init = initImpl;    pub const lookupCount = lookupCountImpl;    pub const substitutionAt = substitutionAtImpl;    pub const substitutionAtWithGdef = substitutionAtWithGdefImpl;    const substitutionAtLookup = substitutionAtLookupImpl;    const contextualSubstitution = contextualSubstitutionImpl;    const contextualSubstitutionFormat1 = contextualSubstitutionFormat1Impl;    const contextualSubstitutionFormat2 = contextualSubstitutionFormat2Impl;    const contextualSubstitutionFormat3 = contextualSubstitutionFormat3Impl;    const contextualSubstitutionRuleSet = contextualSubstitutionRuleSetImpl;    const contextualSubstitutionClassSet = contextualSubstitutionClassSetImpl;    const contextualSubstitutionClassRule = contextualSubstitutionClassRuleImpl;    const contextualSubstitutionRule = contextualSubstitutionRuleImpl;    const chainedContextualSubstitution = chainedContextualSubstitutionImpl;    const chainedContextualSubstitutionFormat1 = chainedContextualSubstitutionFormat1Impl;    const chainedContextualSubstitutionFormat2 = chainedContextualSubstitutionFormat2Impl;    const chainedContextualSubstitutionFormat3 = chainedContextualSubstitutionFormat3Impl;    const chainedContextualSubstitutionRuleSet = chainedContextualSubstitutionRuleSetImpl;    const chainedContextualSubstitutionRule = chainedContextualSubstitutionRuleImpl;    const chainedContextualSubstitutionClassSet = chainedContextualSubstitutionClassSetImpl;    const chainedContextualSubstitutionClassRule = chainedContextualSubstitutionClassRuleImpl;    const contextualLookupRecordSubstitution = contextualLookupRecordSubstitutionImpl;    pub const lookupIsReverse = lookupIsReverseImpl;    const lookupIsActive = lookupIsActiveImpl;    const featureDefaultOn = featureDefaultOnImpl;    const featureAllowsLookup = featureAllowsLookupImpl;    const lookupResolvesOnlyToType = lookupResolvesOnlyToTypeImpl;    const langSysHasFeature = langSysHasFeatureImpl;    pub const usesAutomaticFractions = usesAutomaticFractionsImpl;    pub const usesArabicJoiningForms = usesArabicJoiningFormsImpl;    const featureTag = featureTagImpl;    const featureContainsLookup = featureContainsLookupImpl;    const featureOffset = featureOffsetImpl;    const featureRecord = featureRecordImpl;    const lookupOffset = lookupOffsetImpl;};fn initImpl(data: []const u8, gsub_table: Table) FontError!Gsub {    const bytes = data[gsub_table.offset..][0..gsub_table.len];    if (bytes.len < 10) return error.InvalidGsub;    const major = try readGsubU16(bytes, 0);    const minor = try readGsubU16(bytes, 2);    if (major != 1 or (minor != 0 and minor != 1)) return error.InvalidGsub;    if (minor == 1 and bytes.len < 14) return error.InvalidGsub;    const script_list_offset = try gsubOffset(bytes, try readGsubU16(bytes, 4));    const feature_list_offset = try gsubOffset(bytes, try readGsubU16(bytes, 6));    const lookup_list_offset = try gsubOffset(bytes, try readGsubU16(bytes, 8));    const feature_variations_offset = if (minor == 1) blk: {        const relative = try readGsubU32(bytes, 10);        if (relative == 0) break :blk null;        break :blk try gsubChildOffset32(bytes, 0, relative, 8);    } else null;    return .{        .table = gsub_table,        .script_list_offset = script_list_offset,        .feature_list_offset = feature_list_offset,        .lookup_list_offset = lookup_list_offset,        .feature_variations_offset = feature_variations_offset,    };}fn lookupCountImpl(self: Gsub, data: []const u8) FontError!u16 {    const bytes = data[self.table.offset..][0..self.table.len];    return try readGsubU16(bytes, self.lookup_list_offset);}fn substitutionAtImpl(self: Gsub, data: []const u8, selection: LayoutSelection, lookup_index: u16, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {    return try self.substitutionAtWithGdef(data, null, selection, null, lookup_index, glyph_ids, glyph_index, null);}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 {    if (glyph_index >= glyph_ids.len) return null;    return try self.substitutionAtLookup(data, face, selection, gdef, lookup_index, glyph_ids, glyph_index, source, true, true);}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 {    if (glyph_index >= glyph_ids.len) return null;    const bytes = data[self.table.offset..][0..self.table.len];    if (require_active and !try self.lookupIsActive(bytes, face, selection, lookup_index, source)) return null;    const lookup_offset = try self.lookupOffset(bytes, lookup_index);    const lookup_type = try readGsubU16(bytes, lookup_offset);    const filter = try gsubLookupFilter(bytes, lookup_offset);    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;    if (glyph_ids[glyph_index] <= std.math.maxInt(u16)) {        if (try lookupGlyphIgnored(data, gdef, filter, @intCast(glyph_ids[glyph_index]))) return null;    } else if (lookupFilterSkipsGlyphs(filter)) {        return null;    }    const subtable_count = try readGsubU16(bytes, lookup_offset + 4);    if (!hasGsubBytes(bytes, lookup_offset + 6, @as(usize, subtable_count) * 2)) return error.InvalidGsub;    for (0..subtable_count) |i| {        const subtable_offset = try gsubChildOffset(bytes, lookup_offset, try readGsubU16(bytes, lookup_offset + 6 + i * 2), 2);        const resolved = try resolveGsubSubtable(bytes, lookup_type, subtable_offset);        switch (resolved.lookup_type) {            1 => {                if (try singleSubstitution(bytes, resolved.subtable_offset, glyph_ids[glyph_index])) |substitute| {                    return .{ .glyph_id = substitute, .component_count = 1 };                }            },            2 => {                if (try multipleSubstitution(bytes, self.table.offset, resolved.subtable_offset, glyph_ids[glyph_index])) |substitute| return substitute;            },            3 => {                if (try alternateSubstitution(bytes, resolved.subtable_offset, glyph_ids[glyph_index])) |substitute| {                    return .{ .glyph_id = substitute, .component_count = 1 };                }            },            4 => {                if (try ligatureSubstitution(bytes, resolved.subtable_offset, glyph_ids, glyph_index)) |substitute| return substitute;            },            5 => {                if (!allow_context) return null;                if (lookupFilterSkipsGlyphs(filter)) return null;                if (try self.contextualSubstitution(data, bytes, selection, resolved.subtable_offset, glyph_ids, glyph_index)) |substitute| return substitute;            },            6 => {                if (!allow_context) return null;                if (lookupFilterSkipsGlyphs(filter)) return null;                if (try self.chainedContextualSubstitution(data, bytes, selection, resolved.subtable_offset, glyph_ids, glyph_index)) |substitute| return substitute;            },            8 => {                if (lookupFilterSkipsGlyphs(filter)) return null;                if (try reverseChainingSingleSubstitution(bytes, resolved.subtable_offset, glyph_ids, glyph_index)) |substitute| {                    return .{ .glyph_id = substitute, .component_count = 1 };                }            },            else => return null,        }    }    return null;}fn contextualSubstitutionImpl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {    if (glyph_index >= glyph_ids.len or glyph_ids[glyph_index] > std.math.maxInt(u16)) return null;    if (!hasGsubBytes(bytes, subtable_offset, 2)) return error.InvalidGsub;    return switch (try readGsubU16(bytes, subtable_offset)) {        1 => self.contextualSubstitutionFormat1(data, bytes, selection, subtable_offset, glyph_ids, glyph_index),        2 => self.contextualSubstitutionFormat2(data, bytes, selection, subtable_offset, glyph_ids, glyph_index),        3 => self.contextualSubstitutionFormat3(data, bytes, selection, subtable_offset, glyph_ids, glyph_index),        else => error.InvalidGsub,    };}fn contextualSubstitutionFormat1Impl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {    if (!hasGsubBytes(bytes, subtable_offset, 8)) return error.InvalidGsub;    const coverage_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, subtable_offset + 2), 2);    const coverage = layout.Coverage.init(bytes, coverage_offset) catch return error.InvalidGsub;    const coverage_index = (coverage.index(bytes, glyph_ids[glyph_index]) catch return error.InvalidGsub) orelse return null;    const rule_set_count = try readGsubU16(bytes, subtable_offset + 4);    if (coverage_index >= rule_set_count) return error.InvalidGsub;    const rule_set_offsets = subtable_offset + 6;    if (!hasGsubBytes(bytes, rule_set_offsets, @as(usize, rule_set_count) * 2)) return error.InvalidGsub;    const rule_set_relative = try readGsubU16(bytes, rule_set_offsets + @as(usize, coverage_index) * 2);    if (rule_set_relative == 0) return null;    const rule_set_offset = try gsubChildOffset(bytes, subtable_offset, rule_set_relative, 2);    return try self.contextualSubstitutionRuleSet(data, bytes, selection, rule_set_offset, glyph_ids, glyph_index);}fn contextualSubstitutionFormat2Impl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {    if (!hasGsubBytes(bytes, subtable_offset, 8)) return error.InvalidGsub;    const coverage_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, subtable_offset + 2), 2);    const coverage = layout.Coverage.init(bytes, coverage_offset) catch return error.InvalidGsub;    if ((coverage.index(bytes, glyph_ids[glyph_index]) catch return error.InvalidGsub) == null) return null;    const class_def_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, subtable_offset + 4), 2);    const class_def = layout.ClassDef.init(bytes, class_def_offset) catch return error.InvalidGsub;    const rule_set_count = try readGsubU16(bytes, subtable_offset + 6);    const rule_set_offsets = subtable_offset + 8;    if (!hasGsubBytes(bytes, rule_set_offsets, @as(usize, rule_set_count) * 2)) return error.InvalidGsub;    const first_class = class_def.class(bytes, glyph_ids[glyph_index]) catch return error.InvalidGsub;    if (first_class >= rule_set_count) return null;    const rule_set_relative = try readGsubU16(bytes, rule_set_offsets + @as(usize, first_class) * 2);    if (rule_set_relative == 0) return null;    const rule_set_offset = try gsubChildOffset(bytes, subtable_offset, rule_set_relative, 2);    return try self.contextualSubstitutionClassSet(data, bytes, selection, class_def, rule_set_offset, glyph_ids, glyph_index);}fn contextualSubstitutionFormat3Impl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {    if (!hasGsubBytes(bytes, subtable_offset, 6)) return error.InvalidGsub;    const glyph_count = try readGsubU16(bytes, subtable_offset + 2);    const lookup_count = try readGsubU16(bytes, subtable_offset + 4);    if (glyph_count == 0) return error.InvalidGsub;    if (@as(usize, glyph_count) > glyph_ids.len - glyph_index) return null;    const coverage_offsets = subtable_offset + 6;    if (!hasGsubBytes(bytes, coverage_offsets, @as(usize, glyph_count) * 2)) return error.InvalidGsub;    for (0..glyph_count) |sequence_index| {        const coverage_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, coverage_offsets + sequence_index * 2), 2);        if (!try gsubGlyphInCoverage(bytes, coverage_offset, glyph_ids[glyph_index + sequence_index])) return null;    }    const lookup_records_offset = coverage_offsets + @as(usize, glyph_count) * 2;    if (!hasGsubBytes(bytes, lookup_records_offset, @as(usize, lookup_count) * 4)) return error.InvalidGsub;    return try self.contextualLookupRecordSubstitution(data, bytes, selection, lookup_records_offset, lookup_count, glyph_ids, glyph_index, glyph_count);}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 {    const rule_count = try readGsubU16(bytes, rule_set_offset);    const rule_offsets = rule_set_offset + 2;    if (!hasGsubBytes(bytes, rule_offsets, @as(usize, rule_count) * 2)) return error.InvalidGsub;    for (0..rule_count) |rule_index| {        const rule_relative = try readGsubU16(bytes, rule_offsets + rule_index * 2);        if (rule_relative == 0) continue;        const rule_offset = try gsubChildOffset(bytes, rule_set_offset, rule_relative, 4);        if (try self.contextualSubstitutionRule(data, bytes, selection, rule_offset, glyph_ids, glyph_index)) |substitute| return substitute;    }    return null;}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 {    const rule_count = try readGsubU16(bytes, rule_set_offset);    const rule_offsets = rule_set_offset + 2;    if (!hasGsubBytes(bytes, rule_offsets, @as(usize, rule_count) * 2)) return error.InvalidGsub;    for (0..rule_count) |rule_index| {        const rule_relative = try readGsubU16(bytes, rule_offsets + rule_index * 2);        if (rule_relative == 0) continue;        const rule_offset = try gsubChildOffset(bytes, rule_set_offset, rule_relative, 4);        if (try self.contextualSubstitutionClassRule(data, bytes, selection, class_def, rule_offset, glyph_ids, glyph_index)) |substitute| return substitute;    }    return null;}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 {    const glyph_count = try readGsubU16(bytes, rule_offset);    const lookup_count = try readGsubU16(bytes, rule_offset + 2);    if (glyph_count == 0) return error.InvalidGsub;    if (@as(usize, glyph_count) > glyph_ids.len - glyph_index) return null;    const input_sequence_offset = rule_offset + 4;    const input_sequence_len = @as(usize, glyph_count - 1) * 2;    if (!hasGsubBytes(bytes, input_sequence_offset, input_sequence_len)) return error.InvalidGsub;    for (1..glyph_count) |sequence_index| {        const glyph = glyph_ids[glyph_index + sequence_index];        if (glyph > std.math.maxInt(u16)) return null;        const expected_class = try readGsubU16(bytes, input_sequence_offset + (sequence_index - 1) * 2);        const actual_class = class_def.class(bytes, glyph) catch return error.InvalidGsub;        if (actual_class != expected_class) return null;    }    const lookup_records_offset = input_sequence_offset + input_sequence_len;    if (!hasGsubBytes(bytes, lookup_records_offset, @as(usize, lookup_count) * 4)) return error.InvalidGsub;    return try self.contextualLookupRecordSubstitution(data, bytes, selection, lookup_records_offset, lookup_count, glyph_ids, glyph_index, glyph_count);}fn contextualSubstitutionRuleImpl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, rule_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {    const glyph_count = try readGsubU16(bytes, rule_offset);    const lookup_count = try readGsubU16(bytes, rule_offset + 2);    if (glyph_count == 0) return error.InvalidGsub;    if (@as(usize, glyph_count) > glyph_ids.len - glyph_index) return null;    const input_sequence_offset = rule_offset + 4;    const input_sequence_len = @as(usize, glyph_count - 1) * 2;    if (!hasGsubBytes(bytes, input_sequence_offset, input_sequence_len)) return error.InvalidGsub;    for (1..glyph_count) |sequence_index| {        const glyph = glyph_ids[glyph_index + sequence_index];        if (glyph > std.math.maxInt(u16) or glyph != try readGsubU16(bytes, input_sequence_offset + (sequence_index - 1) * 2)) return null;    }    const lookup_records_offset = input_sequence_offset + input_sequence_len;    if (!hasGsubBytes(bytes, lookup_records_offset, @as(usize, lookup_count) * 4)) return error.InvalidGsub;    return try self.contextualLookupRecordSubstitution(data, bytes, selection, lookup_records_offset, lookup_count, glyph_ids, glyph_index, glyph_count);}fn chainedContextualSubstitutionImpl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {    if (glyph_index >= glyph_ids.len) return null;    if (!hasGsubBytes(bytes, subtable_offset, 2)) return error.InvalidGsub;    return switch (try readGsubU16(bytes, subtable_offset)) {        1 => self.chainedContextualSubstitutionFormat1(data, bytes, selection, subtable_offset, glyph_ids, glyph_index),        2 => self.chainedContextualSubstitutionFormat2(data, bytes, selection, subtable_offset, glyph_ids, glyph_index),        3 => self.chainedContextualSubstitutionFormat3(data, bytes, selection, subtable_offset, glyph_ids, glyph_index),        else => error.InvalidGsub,    };}fn chainedContextualSubstitutionFormat1Impl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {    if (!hasGsubBytes(bytes, subtable_offset, 8)) return error.InvalidGsub;    const coverage_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, subtable_offset + 2), 2);    const coverage = layout.Coverage.init(bytes, coverage_offset) catch return error.InvalidGsub;    const coverage_index = (coverage.index(bytes, glyph_ids[glyph_index]) catch return error.InvalidGsub) orelse return null;    const rule_set_count = try readGsubU16(bytes, subtable_offset + 4);    if (coverage_index >= rule_set_count) return error.InvalidGsub;    const rule_set_offsets = subtable_offset + 6;    if (!hasGsubBytes(bytes, rule_set_offsets, @as(usize, rule_set_count) * 2)) return error.InvalidGsub;    const rule_set_relative = try readGsubU16(bytes, rule_set_offsets + @as(usize, coverage_index) * 2);    if (rule_set_relative == 0) return null;    const rule_set_offset = try gsubChildOffset(bytes, subtable_offset, rule_set_relative, 2);    return try self.chainedContextualSubstitutionRuleSet(data, bytes, selection, rule_set_offset, glyph_ids, glyph_index);}fn chainedContextualSubstitutionFormat2Impl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {    if (!hasGsubBytes(bytes, subtable_offset, 12)) return error.InvalidGsub;    const coverage_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, subtable_offset + 2), 2);    const coverage = layout.Coverage.init(bytes, coverage_offset) catch return error.InvalidGsub;    if ((coverage.index(bytes, glyph_ids[glyph_index]) catch return error.InvalidGsub) == null) return null;    const backtrack_class_def_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, subtable_offset + 4), 2);    const input_class_def_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, subtable_offset + 6), 2);    const lookahead_class_def_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, subtable_offset + 8), 2);    const backtrack_class_def = layout.ClassDef.init(bytes, backtrack_class_def_offset) catch return error.InvalidGsub;    const input_class_def = layout.ClassDef.init(bytes, input_class_def_offset) catch return error.InvalidGsub;    const lookahead_class_def = layout.ClassDef.init(bytes, lookahead_class_def_offset) catch return error.InvalidGsub;    const rule_set_count = try readGsubU16(bytes, subtable_offset + 10);    const rule_set_offsets = subtable_offset + 12;    if (!hasGsubBytes(bytes, rule_set_offsets, @as(usize, rule_set_count) * 2)) return error.InvalidGsub;    const first_class = input_class_def.class(bytes, glyph_ids[glyph_index]) catch return error.InvalidGsub;    if (first_class >= rule_set_count) return null;    const rule_set_relative = try readGsubU16(bytes, rule_set_offsets + @as(usize, first_class) * 2);    if (rule_set_relative == 0) return null;    const rule_set_offset = try gsubChildOffset(bytes, subtable_offset, rule_set_relative, 2);    return try self.chainedContextualSubstitutionClassSet(        data,        bytes,        selection,        backtrack_class_def,        input_class_def,        lookahead_class_def,        rule_set_offset,        glyph_ids,        glyph_index,    );}fn chainedContextualSubstitutionFormat3Impl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {    if (!hasGsubBytes(bytes, subtable_offset, 4)) return error.InvalidGsub;    const backtrack_count = try readGsubU16(bytes, subtable_offset + 2);    const backtrack_offsets = subtable_offset + 4;    if (!hasGsubBytes(bytes, backtrack_offsets, @as(usize, backtrack_count) * 2)) return error.InvalidGsub;    if (@as(usize, backtrack_count) > glyph_index) return null;    for (0..backtrack_count) |backtrack_index| {        const coverage_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, backtrack_offsets + backtrack_index * 2), 2);        const match_index = glyph_index - backtrack_index - 1;        if (!try gsubGlyphInCoverage(bytes, coverage_offset, glyph_ids[match_index])) return null;    }    const input_count_offset = backtrack_offsets + @as(usize, backtrack_count) * 2;    const input_count = try readGsubU16(bytes, input_count_offset);    if (input_count == 0) return error.InvalidGsub;    const input_offsets = input_count_offset + 2;    if (!hasGsubBytes(bytes, input_offsets, @as(usize, input_count) * 2)) return error.InvalidGsub;    if (@as(usize, input_count) > glyph_ids.len - glyph_index) return null;    for (0..input_count) |input_index| {        const coverage_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, input_offsets + input_index * 2), 2);        if (!try gsubGlyphInCoverage(bytes, coverage_offset, glyph_ids[glyph_index + input_index])) return null;    }    const lookahead_count_offset = input_offsets + @as(usize, input_count) * 2;    const lookahead_count = try readGsubU16(bytes, lookahead_count_offset);    const lookahead_offsets = lookahead_count_offset + 2;    if (!hasGsubBytes(bytes, lookahead_offsets, @as(usize, lookahead_count) * 2)) return error.InvalidGsub;    const lookahead_start = glyph_index + @as(usize, input_count);    if (@as(usize, lookahead_count) > glyph_ids.len - lookahead_start) return null;    for (0..lookahead_count) |lookahead_index| {        const coverage_offset = try gsubChildOffset(bytes, subtable_offset, try readGsubU16(bytes, lookahead_offsets + lookahead_index * 2), 2);        if (!try gsubGlyphInCoverage(bytes, coverage_offset, glyph_ids[lookahead_start + lookahead_index])) return null;    }    const lookup_count_offset = lookahead_offsets + @as(usize, lookahead_count) * 2;    const lookup_count = try readGsubU16(bytes, lookup_count_offset);    const lookup_records_offset = lookup_count_offset + 2;    if (!hasGsubBytes(bytes, lookup_records_offset, @as(usize, lookup_count) * 4)) return error.InvalidGsub;    return try self.contextualLookupRecordSubstitution(data, bytes, selection, lookup_records_offset, lookup_count, glyph_ids, glyph_index, input_count);}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 {    const rule_count = try readGsubU16(bytes, rule_set_offset);    const rule_offsets = rule_set_offset + 2;    if (!hasGsubBytes(bytes, rule_offsets, @as(usize, rule_count) * 2)) return error.InvalidGsub;    for (0..rule_count) |rule_index| {        const rule_relative = try readGsubU16(bytes, rule_offsets + rule_index * 2);        if (rule_relative == 0) continue;        const rule_offset = try gsubChildOffset(bytes, rule_set_offset, rule_relative, 2);        if (try self.chainedContextualSubstitutionRule(data, bytes, selection, rule_offset, glyph_ids, glyph_index)) |substitute| return substitute;    }    return null;}fn chainedContextualSubstitutionRuleImpl(self: Gsub, data: []const u8, bytes: []const u8, selection: LayoutSelection, rule_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {    const backtrack_count = try readGsubU16(bytes, rule_offset);    const backtrack_sequence_offset = rule_offset + 2;    if (!hasGsubBytes(bytes, backtrack_sequence_offset, @as(usize, backtrack_count) * 2)) return error.InvalidGsub;    if (@as(usize, backtrack_count) > glyph_index) return null;    for (0..backtrack_count) |backtrack_index| {        const glyph = glyph_ids[glyph_index - backtrack_index - 1];        if (glyph > std.math.maxInt(u16) or glyph != try readGsubU16(bytes, backtrack_sequence_offset + backtrack_index * 2)) return null;    }    const input_count_offset = backtrack_sequence_offset + @as(usize, backtrack_count) * 2;    const input_count = try readGsubU16(bytes, input_count_offset);    if (input_count == 0) return error.InvalidGsub;    if (@as(usize, input_count) > glyph_ids.len - glyph_index) return null;    const input_sequence_offset = input_count_offset + 2;    const input_sequence_len = @as(usize, input_count - 1) * 2;    if (!hasGsubBytes(bytes, input_sequence_offset, input_sequence_len)) return error.InvalidGsub;    for (1..input_count) |input_index| {        const glyph = glyph_ids[glyph_index + input_index];        if (glyph > std.math.maxInt(u16) or glyph != try readGsubU16(bytes, input_sequence_offset + (input_index - 1) * 2)) return null;    }    const lookahead_count_offset = input_sequence_offset + input_sequence_len;    const lookahead_count = try readGsubU16(bytes, lookahead_count_offset);    const lookahead_sequence_offset = lookahead_count_offset + 2;    if (!hasGsubBytes(bytes, lookahead_sequence_offset, @as(usize, lookahead_count) * 2)) return error.InvalidGsub;    const lookahead_start = glyph_index + @as(usize, input_count);    if (@as(usize, lookahead_count) > glyph_ids.len - lookahead_start) return null;    for (0..lookahead_count) |lookahead_index| {        const glyph = glyph_ids[lookahead_start + lookahead_index];        if (glyph > std.math.maxInt(u16) or glyph != try readGsubU16(bytes, lookahead_sequence_offset + lookahead_index * 2)) return null;    }    const lookup_count_offset = lookahead_sequence_offset + @as(usize, lookahead_count) * 2;    const lookup_count = try readGsubU16(bytes, lookup_count_offset);    const lookup_records_offset = lookup_count_offset + 2;    if (!hasGsubBytes(bytes, lookup_records_offset, @as(usize, lookup_count) * 4)) return error.InvalidGsub;    return try self.contextualLookupRecordSubstitution(data, bytes, selection, lookup_records_offset, lookup_count, glyph_ids, glyph_index, input_count);}fn chainedContextualSubstitutionClassSetImpl(    self: Gsub,    data: []const u8,    bytes: []const u8,    selection: LayoutSelection,    backtrack_class_def: layout.ClassDef,    input_class_def: layout.ClassDef,    lookahead_class_def: layout.ClassDef,    rule_set_offset: usize,    glyph_ids: []const u32,    glyph_index: usize,) FontError!?GsubSubstitution {    const rule_count = try readGsubU16(bytes, rule_set_offset);    const rule_offsets = rule_set_offset + 2;    if (!hasGsubBytes(bytes, rule_offsets, @as(usize, rule_count) * 2)) return error.InvalidGsub;    for (0..rule_count) |rule_index| {        const rule_relative = try readGsubU16(bytes, rule_offsets + rule_index * 2);        if (rule_relative == 0) continue;        const rule_offset = try gsubChildOffset(bytes, rule_set_offset, rule_relative, 2);        if (try self.chainedContextualSubstitutionClassRule(            data,            bytes,            selection,            backtrack_class_def,            input_class_def,            lookahead_class_def,            rule_offset,            glyph_ids,            glyph_index,        )) |substitute| return substitute;    }    return null;}fn chainedContextualSubstitutionClassRuleImpl(    self: Gsub,    data: []const u8,    bytes: []const u8,    selection: LayoutSelection,    backtrack_class_def: layout.ClassDef,    input_class_def: layout.ClassDef,    lookahead_class_def: layout.ClassDef,    rule_offset: usize,    glyph_ids: []const u32,    glyph_index: usize,) FontError!?GsubSubstitution {    const backtrack_count = try readGsubU16(bytes, rule_offset);    const backtrack_sequence_offset = rule_offset + 2;    if (!hasGsubBytes(bytes, backtrack_sequence_offset, @as(usize, backtrack_count) * 2)) return error.InvalidGsub;    if (@as(usize, backtrack_count) > glyph_index) return null;    for (0..backtrack_count) |backtrack_index| {        const glyph = glyph_ids[glyph_index - backtrack_index - 1];        if (glyph > std.math.maxInt(u16)) return null;        const expected_class = try readGsubU16(bytes, backtrack_sequence_offset + backtrack_index * 2);        const actual_class = backtrack_class_def.class(bytes, glyph) catch return error.InvalidGsub;        if (actual_class != expected_class) return null;    }    const input_count_offset = backtrack_sequence_offset + @as(usize, backtrack_count) * 2;    const input_count = try readGsubU16(bytes, input_count_offset);    if (input_count == 0) return error.InvalidGsub;    if (@as(usize, input_count) > glyph_ids.len - glyph_index) return null;    const input_sequence_offset = input_count_offset + 2;    const input_sequence_len = @as(usize, input_count - 1) * 2;    if (!hasGsubBytes(bytes, input_sequence_offset, input_sequence_len)) return error.InvalidGsub;    for (1..input_count) |input_index| {        const glyph = glyph_ids[glyph_index + input_index];        if (glyph > std.math.maxInt(u16)) return null;        const expected_class = try readGsubU16(bytes, input_sequence_offset + (input_index - 1) * 2);        const actual_class = input_class_def.class(bytes, glyph) catch return error.InvalidGsub;        if (actual_class != expected_class) return null;    }    const lookahead_count_offset = input_sequence_offset + input_sequence_len;    const lookahead_count = try readGsubU16(bytes, lookahead_count_offset);    const lookahead_sequence_offset = lookahead_count_offset + 2;    if (!hasGsubBytes(bytes, lookahead_sequence_offset, @as(usize, lookahead_count) * 2)) return error.InvalidGsub;    const lookahead_start = glyph_index + @as(usize, input_count);    if (@as(usize, lookahead_count) > glyph_ids.len - lookahead_start) return null;    for (0..lookahead_count) |lookahead_index| {        const glyph = glyph_ids[lookahead_start + lookahead_index];        if (glyph > std.math.maxInt(u16)) return null;        const expected_class = try readGsubU16(bytes, lookahead_sequence_offset + lookahead_index * 2);        const actual_class = lookahead_class_def.class(bytes, glyph) catch return error.InvalidGsub;        if (actual_class != expected_class) return null;    }    const lookup_count_offset = lookahead_sequence_offset + @as(usize, lookahead_count) * 2;    const lookup_count = try readGsubU16(bytes, lookup_count_offset);    const lookup_records_offset = lookup_count_offset + 2;    if (!hasGsubBytes(bytes, lookup_records_offset, @as(usize, lookup_count) * 4)) return error.InvalidGsub;    return try self.contextualLookupRecordSubstitution(data, bytes, selection, lookup_records_offset, lookup_count, glyph_ids, glyph_index, input_count);}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 {    for (0..lookup_count) |lookup_record_index| {        const record_offset = lookup_records_offset + lookup_record_index * 4;        const sequence_index = try readGsubU16(bytes, record_offset);        const nested_lookup_index = try readGsubU16(bytes, record_offset + 2);        if (sequence_index >= input_count) return error.InvalidGsub;        const nested_glyph_index = glyph_index + @as(usize, sequence_index);        var substitution = (try self.substitutionAtLookup(data, null, selection, null, nested_lookup_index, glyph_ids, nested_glyph_index, null, false, false)) orelse continue;        if (@as(usize, substitution.component_count) > @as(usize, input_count) - @as(usize, sequence_index)) return error.InvalidGsub;        substitution.target_offset = @intCast(sequence_index);        substitution.skip_count = @as(usize, input_count) - @as(usize, substitution.component_count) + @as(usize, substitution.replacement_count);        return substitution;    }    return null;}fn lookupIsReverseImpl(self: Gsub, data: []const u8, selection: LayoutSelection, lookup_index: u16) FontError!bool {    _ = selection;    const bytes = data[self.table.offset..][0..self.table.len];    const lookup_offset = try self.lookupOffset(bytes, lookup_index);    const lookup_type = try readGsubU16(bytes, lookup_offset);    if (lookup_type == 8) return true;    if (lookup_type != 7) return false;    const subtable_count = try readGsubU16(bytes, lookup_offset + 4);    if (!hasGsubBytes(bytes, lookup_offset + 6, @as(usize, subtable_count) * 2)) return error.InvalidGsub;    for (0..subtable_count) |i| {        const subtable_offset = try gsubChildOffset(bytes, lookup_offset, try readGsubU16(bytes, lookup_offset + 6 + i * 2), 2);        const resolved = try resolveGsubSubtable(bytes, lookup_type, subtable_offset);        if (resolved.lookup_type == 8) return true;    }    return false;}fn lookupIsActiveImpl(self: Gsub, bytes: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, lookup_index: u16, source: ?FeatureRange) FontError!bool {    const langsys_offset = (try gsubLangSysOffset(bytes, self.script_list_offset, selection)) orelse return false;    const required_feature_index = try readGsubU16(bytes, langsys_offset + 2);    if (required_feature_index != 0xffff) {        const feature_index = required_feature_index;        const feature_tag = try self.featureTag(bytes, feature_index);        if (try self.featureContainsLookup(bytes, face, selection, feature_index, lookup_index)) {            if (try self.featureAllowsLookup(bytes, feature_tag, lookup_index)) return true;        }    }    const feature_count = try readGsubU16(bytes, langsys_offset + 4);    if (!hasGsubBytes(bytes, langsys_offset + 6, @as(usize, feature_count) * 2)) return error.InvalidGsub;    for (0..feature_count) |i| {        const feature_index = try readGsubU16(bytes, langsys_offset + 6 + i * 2);        const feature_tag = try self.featureTag(bytes, feature_index);        if (!gsubFeatureEnabled(selection, feature_tag, try self.featureDefaultOn(bytes, selection, feature_tag), source)) continue;        if (try self.featureContainsLookup(bytes, face, selection, feature_index, lookup_index)) {            if (try self.featureAllowsLookup(bytes, feature_tag, lookup_index)) return true;        }    }    return false;}fn featureDefaultOnImpl(self: Gsub, bytes: []const u8, selection: LayoutSelection, feature_tag: Tag) FontError!bool {    return switch (feature_tag) {        tagLiteral("rlig"), tagLiteral("ccmp"), tagLiteral("rvrn"), tagLiteral("locl") => true,        tagLiteral("liga"), tagLiteral("clig"), tagLiteral("calt"), tagLiteral("rclt") => !selection.vertical,        tagLiteral("ltra"), tagLiteral("ltrm") => !selection.right_to_left,        tagLiteral("rtla"), tagLiteral("rtlm") => selection.right_to_left,        tagLiteral("vrt2") => selection.vertical,        tagLiteral("vert") => selection.vertical and !(try self.langSysHasFeature(bytes, selection, tagLiteral("vrt2"))),        else => false,    };}fn featureAllowsLookupImpl(self: Gsub, bytes: []const u8, feature_tag: Tag, lookup_index: u16) FontError!bool {    if (feature_tag != tagLiteral("rvrn")) return true;    return try self.lookupResolvesOnlyToType(bytes, lookup_index, 1);}fn lookupResolvesOnlyToTypeImpl(self: Gsub, bytes: []const u8, lookup_index: u16, wanted_lookup_type: u16) FontError!bool {    const lookup_offset = try self.lookupOffset(bytes, lookup_index);    const lookup_type = try readGsubU16(bytes, lookup_offset);    if (lookup_type == wanted_lookup_type) return true;    if (lookup_type != 7) return false;    const subtable_count = try readGsubU16(bytes, lookup_offset + 4);    if (!hasGsubBytes(bytes, lookup_offset + 6, @as(usize, subtable_count) * 2)) return error.InvalidGsub;    var saw_subtable = false;    for (0..subtable_count) |i| {        const subtable_offset = try gsubChildOffset(bytes, lookup_offset, try readGsubU16(bytes, lookup_offset + 6 + i * 2), 2);        const resolved = try resolveGsubSubtable(bytes, lookup_type, subtable_offset);        if (resolved.lookup_type != wanted_lookup_type) return false;        saw_subtable = true;    }    return saw_subtable;}fn langSysHasFeatureImpl(self: Gsub, bytes: []const u8, selection: LayoutSelection, wanted_tag: Tag) FontError!bool {    const langsys_offset = (try gsubLangSysOffset(bytes, self.script_list_offset, selection)) orelse return false;    const required_feature_index = try readGsubU16(bytes, langsys_offset + 2);    if (required_feature_index != 0xffff and (try self.featureTag(bytes, required_feature_index)) == wanted_tag) return true;    const feature_count = try readGsubU16(bytes, langsys_offset + 4);    if (!hasGsubBytes(bytes, langsys_offset + 6, @as(usize, feature_count) * 2)) return error.InvalidGsub;    for (0..feature_count) |i| {        const feature_index = try readGsubU16(bytes, langsys_offset + 6 + i * 2);        if ((try self.featureTag(bytes, feature_index)) == wanted_tag) return true;    }    return false;}fn usesAutomaticFractionsImpl(self: Gsub, data: []const u8, selection: LayoutSelection) FontError!bool {    const table_bytes = data[self.table.offset..][0..self.table.len];    if (try self.langSysHasFeature(table_bytes, selection, tagLiteral("frac"))) return true;    return (try self.langSysHasFeature(table_bytes, selection, tagLiteral("numr"))) and        (try self.langSysHasFeature(table_bytes, selection, tagLiteral("dnom")));}fn usesArabicJoiningFormsImpl(self: Gsub, data: []const u8, selection: LayoutSelection) FontError!bool {    const table_bytes = data[self.table.offset..][0..self.table.len];    const feature_tags = [_]Tag{        tagLiteral("isol"),        tagLiteral("fina"),        tagLiteral("medi"),        tagLiteral("init"),    };    for (feature_tags) |feature_tag| {        if (try self.langSysHasFeature(table_bytes, selection, feature_tag)) return true;    }    return false;}fn featureTagImpl(self: Gsub, bytes: []const u8, feature_index: u16) FontError!Tag {    const feature_record = try self.featureRecord(bytes, feature_index);    return try readGsubU32(bytes, feature_record);}fn featureContainsLookupImpl(self: Gsub, bytes: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, feature_index: u16, lookup_index: u16) FontError!bool {    const feature_offset = try self.featureOffset(bytes, face, selection, feature_index);    return try featureTableContainsLookup(.gsub, bytes, feature_offset, lookup_index);}fn featureOffsetImpl(self: Gsub, bytes: []const u8, face: ?lookups.VariationContext, selection: LayoutSelection, feature_index: u16) FontError!usize {    if (try matchingFeatureSubstitutionOffset(.gsub, bytes, self.feature_variations_offset, face, selection)) |substitution_offset| {        if (try substitutedFeatureOffset(.gsub, bytes, substitution_offset, feature_index)) |alternate_offset| return alternate_offset;    }    const feature_record = try self.featureRecord(bytes, feature_index);    return try gsubChildOffset(bytes, self.feature_list_offset, try readGsubU16(bytes, feature_record + 4), 4);}fn featureRecordImpl(self: Gsub, bytes: []const u8, feature_index: u16) FontError!usize {    const feature_count = try readGsubU16(bytes, self.feature_list_offset);    if (feature_index >= feature_count) return error.InvalidGsub;    const feature_record = self.feature_list_offset + 2 + @as(usize, feature_index) * 6;    if (!hasGsubBytes(bytes, feature_record, 6)) return error.InvalidGsub;    return feature_record;}fn lookupOffsetImpl(self: Gsub, bytes: []const u8, lookup_index: u16) FontError!usize {    const lookup_count = try readGsubU16(bytes, self.lookup_list_offset);    if (lookup_index >= lookup_count) return error.InvalidGsub;    const lookup_record = self.lookup_list_offset + 2 + @as(usize, lookup_index) * 2;    return try gsubChildOffset(bytes, self.lookup_list_offset, try readGsubU16(bytes, lookup_record), 6);}fn gsubFeatureEnabled(selection: LayoutSelection, feature_tag: Tag, default_on: bool, source: ?FeatureRange) bool {    if (feature_tag == tagLiteral("rclt") and default_on) return true;    return featureEnabled(selection, feature_tag, default_on, source);}fn gsubLangSysOffset(data: []const u8, script_list_offset: usize, selection: LayoutSelection) FontError!?usize {    if (selection.script_tags.len != 0) {        for (selection.script_tags) |script_tag| {            if (try gsubLangSysOffsetForScript(data, script_list_offset, script_tag, selection.language)) |langsys_offset| return langsys_offset;        }        if (!scriptTagListContains(selection.script_tags, defaultScriptTag)) {            return try gsubLangSysOffsetForScript(data, script_list_offset, defaultScriptTag, selection.language);        }        return null;    }    if (try gsubScriptOffset(data, script_list_offset, selection.script)) |script_offset| {        return try gsubLangSysOffsetInScript(data, script_offset, selection.language);    }    if (selection.script != defaultScriptTag) {        if (try gsubScriptOffset(data, script_list_offset, defaultScriptTag)) |script_offset| {            return try gsubLangSysOffsetInScript(data, script_offset, selection.language);        }    }    return null;}fn gsubLangSysOffsetForScript(data: []const u8, script_list_offset: usize, script_tag: Tag, language: ?Tag) FontError!?usize {    const script_offset = (try gsubScriptOffset(data, script_list_offset, script_tag)) orelse return null;    return try gsubLangSysOffsetInScript(data, script_offset, language);}fn gsubScriptOffset(data: []const u8, script_list_offset: usize, script_tag: Tag) FontError!?usize {    const script_count = try readGsubU16(data, script_list_offset);    if (!hasGsubBytes(data, script_list_offset + 2, @as(usize, script_count) * 6)) return error.InvalidGsub;    for (0..script_count) |i| {        const record = script_list_offset + 2 + i * 6;        if (try readGsubU32(data, record) != script_tag) continue;        return try gsubChildOffset(data, script_list_offset, try readGsubU16(data, record + 4), 4);    }    return null;}fn gsubLangSysOffsetInScript(data: []const u8, script_offset: usize, language: ?Tag) FontError!?usize {    if (language) |language_tag| {        const langsys_count = try readGsubU16(data, script_offset + 2);        const records_offset = script_offset + 4;        if (!hasGsubBytes(data, records_offset, @as(usize, langsys_count) * 6)) return error.InvalidGsub;        for (0..langsys_count) |i| {            const record = records_offset + i * 6;            if (try readGsubU32(data, record) != language_tag) continue;            return try gsubChildOffset(data, script_offset, try readGsubU16(data, record + 4), 6);        }    }    const default_langsys_offset = try readGsubU16(data, script_offset);    if (default_langsys_offset == 0) return null;    return try gsubChildOffset(data, script_offset, default_langsys_offset, 6);}fn gsubGlyphInCoverage(data: []const u8, coverage_offset: usize, glyph_id: u32) FontError!bool {    const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGsub;    return (coverage.index(data, glyph_id) catch return error.InvalidGsub) != null;}const ResolvedGsubSubtable = struct {    lookup_type: u16,    subtable_offset: usize,};fn resolveGsubSubtable(data: []const u8, lookup_type: u16, subtable_offset: usize) FontError!ResolvedGsubSubtable {    if (lookup_type != 7) return .{ .lookup_type = lookup_type, .subtable_offset = subtable_offset };    if (!hasGsubBytes(data, subtable_offset, 8)) return error.InvalidGsub;    if (try readGsubU16(data, subtable_offset) != 1) return error.InvalidGsub;    const extension_lookup_type = try readGsubU16(data, subtable_offset + 2);    if (extension_lookup_type == 7) return error.InvalidGsub;    const extension_offset = try readGsubU32(data, subtable_offset + 4);    return .{        .lookup_type = extension_lookup_type,        .subtable_offset = try gsubChildOffset32(data, subtable_offset, extension_offset, 2),    };}fn singleSubstitution(data: []const u8, subtable_offset: usize, glyph_id: u32) FontError!?u32 {    if (glyph_id > std.math.maxInt(u16)) return null;    if (!hasGsubBytes(data, subtable_offset, 2)) return error.InvalidGsub;    return switch (try readGsubU16(data, subtable_offset)) {        1 => singleSubstitutionFormat1(data, subtable_offset, @intCast(glyph_id)),        2 => singleSubstitutionFormat2(data, subtable_offset, @intCast(glyph_id)),        else => error.InvalidGsub,    };}fn singleSubstitutionFormat1(data: []const u8, subtable_offset: usize, glyph_id: u16) FontError!?u32 {    if (!hasGsubBytes(data, subtable_offset, 6)) return error.InvalidGsub;    const coverage_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, subtable_offset + 2), 2);    const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGsub;    if ((coverage.index(data, glyph_id) catch return error.InvalidGsub) == null) return null;    const delta = try readGsubI16(data, subtable_offset + 4);    return @intCast(@mod(@as(i32, glyph_id) + @as(i32, delta), 65536));}fn singleSubstitutionFormat2(data: []const u8, subtable_offset: usize, glyph_id: u16) FontError!?u32 {    if (!hasGsubBytes(data, subtable_offset, 6)) return error.InvalidGsub;    const coverage_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, subtable_offset + 2), 2);    const glyph_count = try readGsubU16(data, subtable_offset + 4);    const substitute_offset = subtable_offset + 6;    if (!hasGsubBytes(data, substitute_offset, @as(usize, glyph_count) * 2)) return error.InvalidGsub;    const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGsub;    const coverage_index = (coverage.index(data, glyph_id) catch return error.InvalidGsub) orelse return null;    if (coverage_index >= glyph_count) return error.InvalidGsub;    return try readGsubU16(data, substitute_offset + @as(usize, coverage_index) * 2);}fn multipleSubstitution(data: []const u8, table_offset: usize, subtable_offset: usize, glyph_id: u32) FontError!?GsubSubstitution {    if (glyph_id > std.math.maxInt(u16)) return null;    if (!hasGsubBytes(data, subtable_offset, 6)) return error.InvalidGsub;    if (try readGsubU16(data, subtable_offset) != 1) return error.InvalidGsub;    const coverage_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, subtable_offset + 2), 2);    const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGsub;    const coverage_index = (coverage.index(data, glyph_id) catch return error.InvalidGsub) orelse return null;    const sequence_count = try readGsubU16(data, subtable_offset + 4);    if (coverage_index >= sequence_count) return error.InvalidGsub;    const sequence_offsets = subtable_offset + 6;    if (!hasGsubBytes(data, sequence_offsets, @as(usize, sequence_count) * 2)) return error.InvalidGsub;    const sequence_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, sequence_offsets + @as(usize, coverage_index) * 2), 2);    const glyph_count = try readGsubU16(data, sequence_offset);    if (glyph_count == 0) return error.InvalidGsub;    const substitute_offset = sequence_offset + 2;    if (!hasGsubBytes(data, substitute_offset, @as(usize, glyph_count) * 2)) return error.InvalidGsub;    if (table_offset > std.math.maxInt(usize) - substitute_offset) return error.InvalidGsub;    return .{        .glyph_id = try readGsubU16(data, substitute_offset),        .component_count = 1,        .replacement_count = glyph_count,        .replacement_glyphs_offset = table_offset + substitute_offset,    };}fn alternateSubstitution(data: []const u8, subtable_offset: usize, glyph_id: u32) FontError!?u32 {    if (glyph_id > std.math.maxInt(u16)) return null;    if (!hasGsubBytes(data, subtable_offset, 6)) return error.InvalidGsub;    if (try readGsubU16(data, subtable_offset) != 1) return error.InvalidGsub;    const coverage_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, subtable_offset + 2), 2);    const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGsub;    const coverage_index = (coverage.index(data, glyph_id) catch return error.InvalidGsub) orelse return null;    const alternate_set_count = try readGsubU16(data, subtable_offset + 4);    if (coverage_index >= alternate_set_count) return error.InvalidGsub;    const alternate_set_offsets = subtable_offset + 6;    if (!hasGsubBytes(data, alternate_set_offsets, @as(usize, alternate_set_count) * 2)) return error.InvalidGsub;    const alternate_set_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, alternate_set_offsets + @as(usize, coverage_index) * 2), 2);    const glyph_count = try readGsubU16(data, alternate_set_offset);    if (glyph_count == 0) return error.InvalidGsub;    if (!hasGsubBytes(data, alternate_set_offset + 2, @as(usize, glyph_count) * 2)) return error.InvalidGsub;    return try readGsubU16(data, alternate_set_offset + 2);}fn ligatureSubstitution(data: []const u8, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {    if (glyph_index >= glyph_ids.len or glyph_ids[glyph_index] > std.math.maxInt(u16)) return null;    if (!hasGsubBytes(data, subtable_offset, 8)) return error.InvalidGsub;    if (try readGsubU16(data, subtable_offset) != 1) return error.InvalidGsub;    const coverage_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, subtable_offset + 2), 2);    const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGsub;    const coverage_index = (coverage.index(data, glyph_ids[glyph_index]) catch return error.InvalidGsub) orelse return null;    const ligature_set_count = try readGsubU16(data, subtable_offset + 4);    if (coverage_index >= ligature_set_count) return error.InvalidGsub;    const ligature_set_offsets = subtable_offset + 6;    if (!hasGsubBytes(data, ligature_set_offsets, @as(usize, ligature_set_count) * 2)) return error.InvalidGsub;    const ligature_set_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, ligature_set_offsets + @as(usize, coverage_index) * 2), 2);    const ligature_count = try readGsubU16(data, ligature_set_offset);    if (!hasGsubBytes(data, ligature_set_offset + 2, @as(usize, ligature_count) * 2)) return error.InvalidGsub;    for (0..ligature_count) |i| {        const ligature_offset = try gsubChildOffset(data, ligature_set_offset, try readGsubU16(data, ligature_set_offset + 2 + i * 2), 4);        const ligature_glyph = try readGsubU16(data, ligature_offset);        const component_count = try readGsubU16(data, ligature_offset + 2);        if (component_count < 2) return error.InvalidGsub;        if (glyph_index + @as(usize, component_count) > glyph_ids.len) continue;        const component_offset = ligature_offset + 4;        if (!hasGsubBytes(data, component_offset, @as(usize, component_count - 1) * 2)) return error.InvalidGsub;        var matched = true;        for (1..component_count) |component_index| {            const glyph = glyph_ids[glyph_index + component_index];            if (glyph > std.math.maxInt(u16) or glyph != try readGsubU16(data, component_offset + (component_index - 1) * 2)) {                matched = false;                break;            }        }        if (matched) {            return .{ .glyph_id = ligature_glyph, .component_count = component_count };        }    }    return null;}fn reverseChainingSingleSubstitution(data: []const u8, subtable_offset: usize, glyph_ids: []const u32, glyph_index: usize) FontError!?u32 {    if (glyph_index >= glyph_ids.len or glyph_ids[glyph_index] > std.math.maxInt(u16)) return null;    if (!hasGsubBytes(data, subtable_offset, 6)) return error.InvalidGsub;    if (try readGsubU16(data, subtable_offset) != 1) return error.InvalidGsub;    const coverage_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, subtable_offset + 2), 2);    const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGsub;    const coverage_index = (coverage.index(data, glyph_ids[glyph_index]) catch return error.InvalidGsub) orelse return null;    const backtrack_count = try readGsubU16(data, subtable_offset + 4);    const backtrack_offsets = subtable_offset + 6;    if (!hasGsubBytes(data, backtrack_offsets, @as(usize, backtrack_count) * 2)) return error.InvalidGsub;    if (glyph_index < @as(usize, backtrack_count)) return null;    for (0..backtrack_count) |i| {        const backtrack_coverage_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, backtrack_offsets + i * 2), 2);        if (!try coverageContainsGlyph(data, backtrack_coverage_offset, glyph_ids[glyph_index - 1 - i])) return null;    }    const lookahead_count_offset = backtrack_offsets + @as(usize, backtrack_count) * 2;    if (!hasGsubBytes(data, lookahead_count_offset, 2)) return error.InvalidGsub;    const lookahead_count = try readGsubU16(data, lookahead_count_offset);    const lookahead_offsets = lookahead_count_offset + 2;    if (!hasGsubBytes(data, lookahead_offsets, @as(usize, lookahead_count) * 2)) return error.InvalidGsub;    if (@as(usize, lookahead_count) > glyph_ids.len - glyph_index - 1) return null;    for (0..lookahead_count) |i| {        const lookahead_coverage_offset = try gsubChildOffset(data, subtable_offset, try readGsubU16(data, lookahead_offsets + i * 2), 2);        if (!try coverageContainsGlyph(data, lookahead_coverage_offset, glyph_ids[glyph_index + 1 + i])) return null;    }    const glyph_count_offset = lookahead_offsets + @as(usize, lookahead_count) * 2;    if (!hasGsubBytes(data, glyph_count_offset, 2)) return error.InvalidGsub;    const glyph_count = try readGsubU16(data, glyph_count_offset);    if (coverage_index >= glyph_count) return error.InvalidGsub;    const substitute_offset = glyph_count_offset + 2;    if (!hasGsubBytes(data, substitute_offset, @as(usize, glyph_count) * 2)) return error.InvalidGsub;    return try readGsubU16(data, substitute_offset + @as(usize, coverage_index) * 2);}fn coverageContainsGlyph(data: []const u8, coverage_offset: usize, glyph_id: u32) FontError!bool {    if (glyph_id > std.math.maxInt(u16)) return false;    const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidGsub;    return (coverage.index(data, glyph_id) catch return error.InvalidGsub) != null;}test "GSUB extension substitution dispatches wrapped single substitutions" {    const data = [_]u8{        0, 1, 0,   0,   0,    10,   0, 30, 0, 44,        0, 1, 'D', 'F', 'L',  'T',  0, 8,  0, 4,        0, 0, 0,   0,   0xff, 0xff, 0, 1,  0, 0,        0, 1, 'l', 'i', 'g',  'a',  0, 8,  0, 0,        0, 1, 0,   0,   0,    1,    0, 4,  0, 7,        0, 0, 0,   1,   0,    8,    0, 1,  0, 1,        0, 0, 0,   8,   0,    1,    0, 6,  0, 4,        0, 1, 0,   1,   0,    1,    };    const gsub = try Gsub.init(&data, .{ .offset = 0, .len = data.len });    const glyphs = [_]u32{1};    const substitution = (try gsub.substitutionAt(&data, .{}, 0, &glyphs, 0)).?;    try std.testing.expectEqual(@as(u32, 5), substitution.glyph_id);    try std.testing.expectEqual(@as(u16, 1), substitution.component_count);}test "GSUB reverse chaining lookup dispatches contextual single substitutions" {    const data = [_]u8{        0, 1,        0,   0,   0,    10,       0, 30, 0, 44,        0, 1,        'D', 'F', 'L',  'T',      0, 8,  0, 4,        0, 0,        0,   0,   0xff, 0xff,     0, 1,  0, 0,        0, 1,        'r', 'l', 'i',  'g',      0, 8,  0, 0,        0, 1,        0,   0,   0,    1,        0, 4,  0, 8,        0, 0,        0,   1,   0,    8,        0, 1,  0, 14,        0, 0,        0,   1,   0,    20,       0, 1,  0, 'B' - 31,        0, 1,        0,   1,   0,    'A' - 31, 0, 1,  0, 1,        0, 'A' - 31,    };    const gsub = try Gsub.init(&data, .{ .offset = 0, .len = data.len });    const glyphs = [_]u32{ 'A' - 31, 'A' - 31, 'A' - 31 };    const substitution = (try gsub.substitutionAt(&data, .{}, 0, &glyphs, 1)).?;    try std.testing.expect(try gsub.lookupIsReverse(&data, .{}, 0));    try std.testing.expectEqual(@as(u32, 'B' - 31), substitution.glyph_id);    try std.testing.expectEqual(@as(u16, 1), substitution.component_count);    try std.testing.expectEqual(@as(?GsubSubstitution, null), try gsub.substitutionAt(&data, .{}, 0, &glyphs, 2));}test "GSUB offsets stay inside table data" {    const data = @as([10]u8, @splat(0));    try std.testing.expectEqual(@as(usize, 8), try gsubOffset(&data, 8));    try std.testing.expectError(error.InvalidGsub, gsubOffset(&data, 0));    try std.testing.expectError(error.InvalidGsub, gsubOffset(&data, 9));    try std.testing.expectError(error.InvalidGsub, gsubOffset(&data, 10));}test "GSUB child offsets stay inside table data" {    const data = @as([10]u8, @splat(0));    try std.testing.expectEqual(@as(usize, 6), try gsubChildOffset(&data, 2, 4, 2));    try std.testing.expectError(error.InvalidGsub, gsubChildOffset(&data, 2, 0, 2));    try std.testing.expectError(error.InvalidGsub, gsubChildOffset(&data, 10, 1, 2));    try std.testing.expectError(error.InvalidGsub, gsubChildOffset(&data, 8, 1, 2));}test "GSUB layout selection resolves language system tags" {    const data = [_]u8{        0,    1,        'l',  'a',        't',  'n',        0,    8,        0,    18,        0,    1,        'T',  'R',        'K',  ' ',        0,    10,        0,    0,        0xff, 0xff,        0,    1,        0,    7,        0,    0,        0xff, 0xff,        0,    1,        0,    3,    };    try std.testing.expectEqual(@as(?usize, 18), try gsubLangSysOffset(&data, 0, .{ .script = tag("latn"), .language = tag("TRK ") }));    try std.testing.expectEqual(@as(?usize, 26), try gsubLangSysOffset(&data, 0, .{ .script = tag("latn"), .language = tag("DEU ") }));    try std.testing.expectEqual(@as(?usize, null), try gsubLangSysOffset(&data, 0, .{ .script = tag("cyrl"), .language = tag("TRK ") }));}test "GSUB single substitution format 1 applies signed delta" {    const data = [_]u8{        0, 1,        0, 6,        0, 2,        0, 1,        0, 1,        0, 5,    };    try std.testing.expectEqual(@as(?u32, 7), try singleSubstitution(&data, 0, 5));    try std.testing.expectEqual(@as(?u32, null), try singleSubstitution(&data, 0, 4));}test "GSUB single substitution format 1 wraps negative delta" {    const data = [_]u8{        0,    1,        0,    6,        0xff, 0xfe,        0,    1,        0,    1,        0,    1,    };    try std.testing.expectEqual(@as(?u32, 65535), try singleSubstitution(&data, 0, 1));}test "GSUB single substitution format 2 uses coverage index" {    const data = [_]u8{        0, 2,        0, 10,        0, 2,        0, 70,        0, 80,        0, 1,        0, 2,        0, 5,        0, 9,    };    try std.testing.expectEqual(@as(?u32, 70), try singleSubstitution(&data, 0, 5));    try std.testing.expectEqual(@as(?u32, 80), try singleSubstitution(&data, 0, 9));    try std.testing.expectEqual(@as(?u32, null), try singleSubstitution(&data, 0, 6));}test "GSUB multiple substitution format 1 uses sequence table" {    const data = [_]u8{        0, 1,        0, 14,        0, 1,        0, 8,        0, 2,        0, 'X' - 31,        0, 'Y' - 31,        0, 1,        0, 1,        0, 'A' - 31,    };    const substitution = (try multipleSubstitution(&data, 0, 0, 'A' - 31)).?;    try std.testing.expectEqual(@as(u32, 'X' - 31), substitution.glyph_id);    try std.testing.expectEqual(@as(u16, 1), substitution.component_count);    try std.testing.expectEqual(@as(u16, 2), substitution.replacement_count);    try std.testing.expectEqual(@as(usize, 10), substitution.replacement_glyphs_offset);    try std.testing.expectEqual(@as(?GsubSubstitution, null), try multipleSubstitution(&data, 0, 0, 'B' - 31));}test "GSUB multiple substitution rejects deletion sequences" {    const data = [_]u8{        0, 1,        0, 10,        0, 1,        0, 8,        0, 0,        0, 1,        0, 1,        0, 'A' - 31,    };    try std.testing.expectError(error.InvalidGsub, multipleSubstitution(&data, 0, 0, 'A' - 31));}test "GSUB ligature substitution format 1 uses preferred ligature set order" {    const data = [_]u8{        0, 1,        0, 18,        0, 1,        0, 8,        0, 1,        0, 4,        0, 96,        0, 2,        0, 'i' - 31,        0, 1,        0, 1,        0, 'f' - 31,    };    const glyphs = [_]u32{ 'f' - 31, 'i' - 31, '!' - 31 };    const substitution = (try ligatureSubstitution(&data, 0, &glyphs, 0)).?;    try std.testing.expectEqual(@as(u32, 96), substitution.glyph_id);    try std.testing.expectEqual(@as(u16, 2), substitution.component_count);    try std.testing.expectEqual(@as(?GsubSubstitution, null), try ligatureSubstitution(&data, 0, &glyphs, 1));}test "GSUB reverse chaining single substitution format 1 uses context coverage" {    const data = [_]u8{        0, 1,        0, 16,        0, 1,        0, 22,        0, 1,        0, 28,        0, 1,        0, 20,        0, 1,        0, 1,        0, 2,        0, 1,        0, 1,        0, 1,        0, 1,        0, 1,        0, 3,    };    const glyphs = [_]u32{ 1, 2, 3 };    const mismatch = [_]u32{ 3, 2, 1 };    try std.testing.expectEqual(@as(?u32, 20), try reverseChainingSingleSubstitution(&data, 0, &glyphs, 1));    try std.testing.expectEqual(@as(?u32, null), try reverseChainingSingleSubstitution(&data, 0, &glyphs, 0));    try std.testing.expectEqual(@as(?u32, null), try reverseChainingSingleSubstitution(&data, 0, &mismatch, 1));}

Source: lib/filigree/src/font/root.zig:9

zig
pub const gsub = @import("gsub.zig");

Audit

Definitions9
Public names9
Members5
Version26.7.0
Revisiondaab053ee433