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 }