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