lib/filigree/src/font/lookups.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const gdef_table = @import("gdef.zig");
3 const model = @import("model.zig");
4
5 const FontError = model.FontError;
6 const Gdef = gdef_table.Gdef;
7 const LayoutSelection = model.LayoutSelection;
8 const Tag = model.Tag;
9 const VariationSetting = model.VariationSetting;
10
11 pub const Kind = enum {
12 gsub,
13 gpos,
14 };
15
16 pub const VariationContext = struct {
17 ptr: *const anyopaque,
18 variation_axis_count: *const fn (*const anyopaque) u16,
19 normalized_coordinate_at: *const fn (*const anyopaque, []const VariationSetting, usize) FontError!i16,
20
21 pub fn variationAxisCount(self: VariationContext) u16 {
22 return self.variation_axis_count(self.ptr);
23 }
24
25 pub fn normalizedCoordinateAt(self: VariationContext, settings: []const VariationSetting, axis_index: usize) FontError!i16 {
26 return self.normalized_coordinate_at(self.ptr, settings, axis_index);
27 }
28 };
29
30 pub const ignore_base_glyphs: u16 = 0x0002;
31 pub const ignore_ligatures: u16 = 0x0004;
32 pub const ignore_marks: u16 = 0x0008;
33 pub const use_mark_filtering_set: u16 = 0x0010;
34 pub const reserved_flags: u16 = 0x00e0;
35 pub const mark_attachment_type: u16 = 0xff00;
36
37 pub const LookupFilter = struct {
38 flags: u16,
39 mark_filtering_set: ?u16 = null,
40 };
41
42 pub fn matchingFeatureSubstitutionOffset(comptime kind: Kind, bytes: []const u8, feature_variations_offset: ?usize, face: anytype, selection: LayoutSelection) FontError!?usize {
43 const offset = feature_variations_offset orelse return null;
44 if (!hasBytes(kind, bytes, offset, 8)) return invalid(kind);
45 if (try readU16(kind, bytes, offset) != 1 or try readU16(kind, bytes, offset + 2) != 0) return null;
46 const record_count = try readU32(kind, bytes, offset + 4);
47 if (@as(u64, record_count) * 8 > @as(u64, bytes.len - offset - 8)) return invalid(kind);
48
49 for (0..record_count) |record_index| {
50 const record_offset = offset + 8 + @as(usize, @intCast(record_index)) * 8;
51 const condition_set_offset = try readU32(kind, bytes, record_offset);
52 if (!try featureConditionSetMatches(kind, bytes, offset, condition_set_offset, face, selection)) continue;
53
54 const substitution_relative = try readU32(kind, bytes, record_offset + 4);
55 if (substitution_relative == 0) return null;
56 const substitution_offset = try childOffset32(kind, bytes, offset, substitution_relative, 6);
57 if (try readU16(kind, bytes, substitution_offset) == 1 and try readU16(kind, bytes, substitution_offset + 2) == 0) {
58 return substitution_offset;
59 }
60 }
61 return null;
62 }
63
64 fn featureConditionSetMatches(comptime kind: Kind, bytes: []const u8, feature_variations_offset: usize, relative_offset: u32, face: anytype, selection: LayoutSelection) FontError!bool {
65 if (relative_offset == 0) return true;
66 const offset = try childOffset32(kind, bytes, feature_variations_offset, relative_offset, 2);
67 const condition_count = try readU16(kind, bytes, offset);
68 if (@as(u64, condition_count) * 4 > @as(u64, bytes.len - offset - 2)) return invalid(kind);
69 const condition_offsets = offset + 2;
70 for (0..condition_count) |condition_index| {
71 const condition_relative = try readU32(kind, bytes, condition_offsets + condition_index * 4);
72 const condition_offset = try childOffset32(kind, bytes, offset, condition_relative, 2);
73 if (!try featureConditionMatches(kind, bytes, condition_offset, face, selection)) return false;
74 }
75 return true;
76 }
77
78 fn featureConditionMatches(comptime kind: Kind, bytes: []const u8, condition_offset: usize, face: anytype, selection: LayoutSelection) FontError!bool {
79 const format = try readU16(kind, bytes, condition_offset);
80 if (format != 1) return false;
81 if (!hasBytes(kind, bytes, condition_offset, 8)) return invalid(kind);
82 const axis_index = try readU16(kind, bytes, condition_offset + 2);
83 const min_value = try readI16(kind, bytes, condition_offset + 4);
84 const max_value = try readI16(kind, bytes, condition_offset + 6);
85 if (min_value > max_value) return false;
86 const found_face = face orelse return false;
87 if (axis_index >= found_face.variationAxisCount()) return false;
88 const value = found_face.normalizedCoordinateAt(selection.variations, axis_index) catch return false;
89 return value >= min_value and value <= max_value;
90 }
91
92 pub fn substitutedFeatureOffset(comptime kind: Kind, bytes: []const u8, substitution_offset: usize, feature_index: u16) FontError!?usize {
93 if (try readU16(kind, bytes, substitution_offset) != 1 or try readU16(kind, bytes, substitution_offset + 2) != 0) return null;
94 const substitution_count = try readU16(kind, bytes, substitution_offset + 4);
95 if (@as(u64, substitution_count) * 6 > @as(u64, bytes.len - substitution_offset - 6)) return invalid(kind);
96 const records = substitution_offset + 6;
97 for (0..substitution_count) |record_index| {
98 const record_offset = records + record_index * 6;
99 const current_feature_index = try readU16(kind, bytes, record_offset);
100 if (current_feature_index > feature_index) return null;
101 if (current_feature_index != feature_index) continue;
102 const alternate_relative = try readU32(kind, bytes, record_offset + 2);
103 return try childOffset32(kind, bytes, substitution_offset, alternate_relative, 4);
104 }
105 return null;
106 }
107
108 pub fn featureTableContainsLookup(comptime kind: Kind, bytes: []const u8, feature_offset: usize, lookup_index: u16) FontError!bool {
109 if (!hasBytes(kind, bytes, feature_offset, 4)) return invalid(kind);
110 const lookup_index_count = try readU16(kind, bytes, feature_offset + 2);
111 if (@as(u64, lookup_index_count) * 2 > @as(u64, bytes.len - feature_offset - 4)) return invalid(kind);
112 for (0..lookup_index_count) |i| {
113 if (try readU16(kind, bytes, feature_offset + 4 + i * 2) == lookup_index) return true;
114 }
115 return false;
116 }
117
118 pub fn baseOffset(comptime kind: Kind, data: []const u8, relative_offset: u16) FontError!usize {
119 const relative: usize = relative_offset;
120 if (relative == 0 or relative >= data.len) return invalid(kind);
121 if (data.len - relative < 2) return invalid(kind);
122 return relative;
123 }
124
125 pub fn childOffset(comptime kind: Kind, data: []const u8, base: usize, relative_offset: u16, min_len: usize) FontError!usize {
126 const relative: usize = relative_offset;
127 if (relative == 0 or base > data.len or relative > data.len - base) return invalid(kind);
128 const child = base + relative;
129 if (!hasBytes(kind, data, child, min_len)) return invalid(kind);
130 return child;
131 }
132
133 pub fn childOffset32(comptime kind: Kind, data: []const u8, base: usize, relative_offset: u32, min_len: usize) FontError!usize {
134 if (relative_offset == 0 or relative_offset > std.math.maxInt(usize)) return invalid(kind);
135 const relative: usize = @intCast(relative_offset);
136 if (base > data.len or relative > data.len - base) return invalid(kind);
137 const child = base + relative;
138 if (!hasBytes(kind, data, child, min_len)) return invalid(kind);
139 return child;
140 }
141
142 pub fn hasBytes(comptime kind: Kind, data: []const u8, offset_value: usize, len: usize) bool {
143 _ = kind;
144 return offset_value <= data.len and len <= data.len - offset_value;
145 }
146
147 pub fn readU16(comptime kind: Kind, data: []const u8, offset_value: usize) FontError!u16 {
148 if (!hasBytes(kind, data, offset_value, 2)) return invalid(kind);
149 return std.mem.readInt(u16, data[offset_value..][0..2], .big);
150 }
151
152 pub fn readI16(comptime kind: Kind, data: []const u8, offset_value: usize) FontError!i16 {
153 return @bitCast(try readU16(kind, data, offset_value));
154 }
155
156 pub fn readU32(comptime kind: Kind, data: []const u8, offset_value: usize) FontError!u32 {
157 if (!hasBytes(kind, data, offset_value, 4)) return invalid(kind);
158 return std.mem.readInt(u32, data[offset_value..][0..4], .big);
159 }
160
161 pub fn invalid(comptime kind: Kind) FontError {
162 return switch (kind) {
163 .gsub => error.InvalidGsub,
164 .gpos => error.InvalidGpos,
165 };
166 }
167
168 pub fn lookupFilterSkipsGlyphs(filter: LookupFilter) bool {
169 return filter.flags != 0 or filter.mark_filtering_set != null;
170 }
171
172 pub fn gsubLookupFilter(data: []const u8, lookup_offset: usize) FontError!LookupFilter {
173 const flags = try gsubReadU16(data, lookup_offset + 2);
174 if ((flags & reserved_flags) != 0) return error.InvalidGsub;
175 const subtable_count = try gsubReadU16(data, lookup_offset + 4);
176 const mark_filtering_set_offset = lookup_offset + 6 + @as(usize, subtable_count) * 2;
177 if ((flags & use_mark_filtering_set) == 0) {
178 return .{ .flags = flags };
179 }
180 if (!gsubHasBytes(data, mark_filtering_set_offset, 2)) return error.InvalidGsub;
181 return .{
182 .flags = flags,
183 .mark_filtering_set = try gsubReadU16(data, mark_filtering_set_offset),
184 };
185 }
186
187 pub fn gposLookupFilter(data: []const u8, lookup_offset: usize) FontError!LookupFilter {
188 const flags = try gposReadU16(data, lookup_offset + 2);
189 if ((flags & reserved_flags) != 0) return error.InvalidGpos;
190 const subtable_count = try gposReadU16(data, lookup_offset + 4);
191 const mark_filtering_set_offset = lookup_offset + 6 + @as(usize, subtable_count) * 2;
192 if ((flags & use_mark_filtering_set) == 0) {
193 return .{ .flags = flags };
194 }
195 if (!gposHasBytes(data, mark_filtering_set_offset, 2)) return error.InvalidGpos;
196 return .{
197 .flags = flags,
198 .mark_filtering_set = try gposReadU16(data, mark_filtering_set_offset),
199 };
200 }
201
202 pub fn lookupGlyphIgnored(data: []const u8, gdef: ?Gdef, filter: LookupFilter, glyph_id: u16) FontError!bool {
203 const needs_gdef =
204 (filter.flags & (ignore_base_glyphs | ignore_ligatures | ignore_marks | mark_attachment_type)) != 0 or
205 filter.mark_filtering_set != null;
206 const definition = gdef orelse return needs_gdef;
207 const class = try definition.glyphClass(data, glyph_id);
208
209 if (class == .base and (filter.flags & ignore_base_glyphs) != 0) return true;
210 if (class == .ligature and (filter.flags & ignore_ligatures) != 0) return true;
211 if (class != .mark) return false;
212 if ((filter.flags & ignore_marks) != 0) return true;
213
214 if (filter.mark_filtering_set) |set_index| {
215 return !try definition.markGlyphSetContains(data, set_index, glyph_id);
216 }
217
218 const attachment_type = (filter.flags & mark_attachment_type) >> 8;
219 if (attachment_type == 0) return false;
220 return try definition.markAttachmentClass(data, glyph_id) != attachment_type;
221 }
222
223 pub fn scriptTagListContains(script_tags: []const Tag, wanted: Tag) bool {
224 for (script_tags) |script_tag| {
225 if (script_tag == wanted) return true;
226 }
227 return false;
228 }
229
230 pub fn gsubOffset(data: []const u8, relative_offset: u16) FontError!usize {
231 return baseOffset(.gsub, data, relative_offset);
232 }
233
234 pub fn gsubChildOffset(data: []const u8, base: usize, relative_offset: u16, min_len: usize) FontError!usize {
235 return childOffset(.gsub, data, base, relative_offset, min_len);
236 }
237
238 pub fn gsubChildOffset32(data: []const u8, base: usize, relative_offset: u32, min_len: usize) FontError!usize {
239 return childOffset32(.gsub, data, base, relative_offset, min_len);
240 }
241
242 pub fn gsubHasBytes(data: []const u8, offset_value: usize, len: usize) bool {
243 return hasBytes(.gsub, data, offset_value, len);
244 }
245
246 pub fn gsubReadU16(data: []const u8, offset_value: usize) FontError!u16 {
247 return readU16(.gsub, data, offset_value);
248 }
249
250 pub fn gsubReadI16(data: []const u8, offset_value: usize) FontError!i16 {
251 return readI16(.gsub, data, offset_value);
252 }
253
254 pub fn gsubReadU32(data: []const u8, offset_value: usize) FontError!u32 {
255 return readU32(.gsub, data, offset_value);
256 }
257
258 pub fn gposOffset(data: []const u8, relative_offset: u16) FontError!usize {
259 return baseOffset(.gpos, data, relative_offset);
260 }
261
262 pub fn gposChildOffset(data: []const u8, base: usize, relative_offset: u16, min_len: usize) FontError!usize {
263 return childOffset(.gpos, data, base, relative_offset, min_len);
264 }
265
266 pub fn gposChildOffset32(data: []const u8, base: usize, relative_offset: u32, min_len: usize) FontError!usize {
267 return childOffset32(.gpos, data, base, relative_offset, min_len);
268 }
269
270 pub fn gposHasBytes(data: []const u8, offset_value: usize, len: usize) bool {
271 return hasBytes(.gpos, data, offset_value, len);
272 }
273
274 pub fn gposReadU16(data: []const u8, offset_value: usize) FontError!u16 {
275 return readU16(.gpos, data, offset_value);
276 }
277
278 pub fn gposReadI16(data: []const u8, offset_value: usize) FontError!i16 {
279 return readI16(.gpos, data, offset_value);
280 }
281
282 pub fn gposReadU32(data: []const u8, offset_value: usize) FontError!u32 {
283 return readU32(.gpos, data, offset_value);
284 }