lib/filigree/src/font/face.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const binary = @import("binary.zig");
3 const cmap_owner = @import("cmap.zig");
4 const directory = @import("directory.zig");
5 const gdef_owner = @import("gdef.zig");
6 const gsub_owner = @import("gsub.zig");
7 const gpos_owner = @import("gpos.zig");
8 const kern_owner = @import("kern.zig");
9 const lookups = @import("lookups.zig");
10 const math_owner = @import("math.zig");
11 const metrics_table = @import("metrics.zig");
12 const model = @import("model.zig");
13 const names = @import("names.zig");
14 const variations = @import("variations.zig");
15 const test_font = @import("../fixture/root.zig");
16 const fixture_binary = test_font.binary;
17
18 const Cmap = cmap_owner.Cmap;
19 const FontError = model.FontError;
20 const Table = binary.Table;
21 const Tag = model.Tag;
22 const readI8 = binary.readI8;
23 const readI16 = binary.readI16;
24 const readI32 = binary.readI32;
25 const readU16 = binary.readU16;
26 const readU24 = binary.readU24;
27 const readU32 = binary.readU32;
28 const table = directory.table;
29 const tableAt = directory.tableAt;
30 const tableAtTag = directory.tableAtTag;
31 const tableOptionalAt = directory.optionalAt;
32 const tableOptionalAtTag = directory.optionalAtTag;
33 const fontDirectoryOffset = directory.fontDirectoryOffset;
34 const tag = model.tag;
35 const tagLiteral = model.tag;
36
37 const GlyphClass = model.GlyphClass;
38 const LigatureCaret = model.LigatureCaret;
39 const FeatureRange = model.FeatureRange;
40 const VariationAxis = model.VariationAxis;
41 const VariationSetting = model.VariationSetting;
42 const NormalizedVariation = model.NormalizedVariation;
43 const LayoutSelection = model.LayoutSelection;
44 const defaultScriptTag = model.defaultScriptTag;
45 const featureEnabled = model.featureEnabled;
46 const fixed16ToFloat = model.fixed16ToFloat;
47
48 const noGposVariationIndex = model.noGposVariationIndex;
49 const GsubSubstitution = model.GsubSubstitution;
50 const GposValueAdjustment = model.GposValueAdjustment;
51 const GposPairAdjustment = model.GposPairAdjustment;
52 const GposPairAdjustmentBefore = model.GposPairAdjustmentBefore;
53 const GposContextualPairAdjustment = model.GposContextualPairAdjustment;
54 const GposSingleAdjustment = model.GposSingleAdjustment;
55 const GposCursiveAttachment = model.GposCursiveAttachment;
56 const Gpos = gpos_owner.Gpos;
57 const Gdef = gdef_owner.Gdef;
58 const Gsub = gsub_owner.Gsub;
59 const Kern = kern_owner.Kern;
60 const Math = math_owner.Math;
61 const MathConstants = math_owner.Constants;
62 const MathGlyphAssembly = math_owner.GlyphAssembly;
63 const MathGlyphVariant = math_owner.GlyphVariant;
64 const MathValueRecord = math_owner.ValueRecord;
65 const VerticalMetrics = metrics_table.VerticalMetrics;
66 const Vorg = metrics_table.Vorg;
67 const HorizontalMetrics = metrics_table.HorizontalMetrics;
68 const verticalMetrics = metrics_table.verticalMetrics;
69 const horizontalTypoMetrics = metrics_table.horizontalTypoMetrics;
70 const bestNameRecord = names.bestNameRecord;
71 const utf16BeToUtf8Alloc = names.utf16BeToUtf8Alloc;
72 const lookup_use_mark_filtering_set = lookups.use_mark_filtering_set;
73 const lookupGlyphIgnored = lookups.lookupGlyphIgnored;
74 const Fvar = variations.Fvar;
75 const Avar = variations.Avar;
76 const Hvar = variations.Hvar;
77 const Vvar = variations.Vvar;
78 const Mvar = variations.Mvar;
79 const DeltaSetIndex = variations.DeltaSetIndex;
80 const ItemVariationStore = variations.ItemVariationStore;
81 const fixed16FromFloat = variations.fixed16FromFloat;
82 const normalizeVariationAxis = variations.normalizeVariationAxis;
83 const fixed16ToF2Dot14 = variations.fixed16ToF2Dot14;
84 const clampDesignMetric = variations.clampDesignMetric;
85 const clampI32 = variations.clampI32;
86
87 pub const Face = struct {
88 data: []const u8,
89 directory_offset: usize,
90 units_per_em: u16,
91 ascender: i16,
92 descender: i16,
93 line_gap: i16,
94 typo_metrics: ?HorizontalMetrics,
95 num_glyphs: u16,
96 num_h_metrics: u16,
97 num_v_metrics: u16,
98 cmap: Cmap,
99 hmtx: Table,
100 vmtx: ?Table,
101 vorg: ?Vorg,
102 kern: ?Kern,
103 gsub: ?Gsub,
104 gpos: ?Gpos,
105 gdef: ?Gdef,
106 fvar: ?Fvar,
107 avar: ?Avar,
108 hvar: ?Hvar,
109 vvar: ?Vvar,
110 mvar: ?Mvar,
111 math: ?Math,
112
113 pub const init = initImpl;
114 pub const initAt = initAtImpl;
115 const initDirectory = initDirectoryImpl;
116 pub const glyphId = glyphIdImpl;
117 pub const tableSlice = tableSliceImpl;
118 pub const tableSliceByTag = tableSliceByTagImpl;
119 pub const nameUtf8Alloc = nameUtf8AllocImpl;
120 pub const glyphIdForVariation = glyphIdForVariationImpl;
121 pub const hasVariationSequences = hasVariationSequencesImpl;
122 pub const variationAxisCount = variationAxisCountImpl;
123 pub const variationAxis = variationAxisImpl;
124 pub const variationAxisFor = variationAxisForImpl;
125 pub const validateVariations = validateVariationsImpl;
126 pub const normalizedVariation = normalizedVariationImpl;
127 pub const normalizedCoordinateAt = normalizedCoordinateAtImpl;
128 pub const advanceWidth = advanceWidthImpl;
129 pub const advanceWidthForVariations = advanceWidthForVariationsImpl;
130 pub const advanceHeight = advanceHeightImpl;
131 pub const advanceHeightForVariations = advanceHeightForVariationsImpl;
132 pub const verticalOriginY = verticalOriginYImpl;
133 pub const metricVariationDelta = metricVariationDeltaImpl;
134 pub const ascenderForVariations = ascenderForVariationsImpl;
135 pub const descenderForVariations = descenderForVariationsImpl;
136 pub const lineGapForVariations = lineGapForVariationsImpl;
137 pub const heightUnitsForVariations = heightUnitsForVariationsImpl;
138 pub const kerning = kerningImpl;
139 pub const hasGpos = hasGposImpl;
140 pub const gposLookupCount = gposLookupCountImpl;
141 pub const gposLookupType = gposLookupTypeImpl;
142 pub const gposVariationValue = gposVariationValueImpl;
143 const variationContext = variationContextImpl;
144 pub const hasGsub = hasGsubImpl;
145 pub const usesAutomaticFractions = usesAutomaticFractionsImpl;
146 pub const usesArabicJoiningForms = usesArabicJoiningFormsImpl;
147 pub const gposPairAdjustment = gposPairAdjustmentImpl;
148 pub const gposPairAdjustmentFor = gposPairAdjustmentForImpl;
149 pub const gposPairAdjustmentBeforeFor = gposPairAdjustmentBeforeForImpl;
150 pub const gposPairAdjustmentBeforeForRange = gposPairAdjustmentBeforeForRangeImpl;
151 pub const gposPairAdjustmentBeforeAtLookupForRange = gposPairAdjustmentBeforeAtLookupForRangeImpl;
152 pub const gposContextualPairAdjustmentAtFor = gposContextualPairAdjustmentAtForImpl;
153 pub const gposContextualPairAdjustmentAtForRange = gposContextualPairAdjustmentAtForRangeImpl;
154 pub const gposContextualPairAdjustmentAtLookupForRange = gposContextualPairAdjustmentAtLookupForRangeImpl;
155 pub const hasGposContextualPairAdjustmentFor = hasGposContextualPairAdjustmentForImpl;
156 pub const gposSingleAdjustment = gposSingleAdjustmentImpl;
157 pub const gposSingleAdjustmentFor = gposSingleAdjustmentForImpl;
158 pub const gposSingleAdjustmentAtFor = gposSingleAdjustmentAtForImpl;
159 pub const gposSingleAdjustmentAtForRange = gposSingleAdjustmentAtForRangeImpl;
160 pub const gposSingleAdjustmentAtLookupForRange = gposSingleAdjustmentAtLookupForRangeImpl;
161 pub const gposCursiveAttachment = gposCursiveAttachmentImpl;
162 pub const gposCursiveAttachmentFor = gposCursiveAttachmentForImpl;
163 pub const gposCursiveAttachmentForRange = gposCursiveAttachmentForRangeImpl;
164 pub const gposCursiveAttachmentAtLookupForRange = gposCursiveAttachmentAtLookupForRangeImpl;
165 pub const gposMarkToBaseAdjustment = gposMarkToBaseAdjustmentImpl;
166 pub const gposMarkToBaseAdjustmentFor = gposMarkToBaseAdjustmentForImpl;
167 pub const gposMarkToBaseAdjustmentForRange = gposMarkToBaseAdjustmentForRangeImpl;
168 pub const gposMarkToBaseAdjustmentAtLookupForRange = gposMarkToBaseAdjustmentAtLookupForRangeImpl;
169 pub const gposMarkToLigatureAdjustment = gposMarkToLigatureAdjustmentImpl;
170 pub const gposMarkToLigatureAdjustmentFor = gposMarkToLigatureAdjustmentForImpl;
171 pub const gposMarkToLigatureAdjustmentForRange = gposMarkToLigatureAdjustmentForRangeImpl;
172 pub const gposMarkToLigatureAdjustmentAtLookupForRange = gposMarkToLigatureAdjustmentAtLookupForRangeImpl;
173 pub const gposMarkToMarkAdjustment = gposMarkToMarkAdjustmentImpl;
174 pub const gposMarkToMarkAdjustmentFor = gposMarkToMarkAdjustmentForImpl;
175 pub const gposMarkToMarkAdjustmentForRange = gposMarkToMarkAdjustmentForRangeImpl;
176 pub const gposMarkToMarkAdjustmentAtLookupForRange = gposMarkToMarkAdjustmentAtLookupForRangeImpl;
177 pub const hasGposMarkToBase = hasGposMarkToBaseImpl;
178 pub const hasGposMarkToBaseFor = hasGposMarkToBaseForImpl;
179 pub const hasGposMarkToLigature = hasGposMarkToLigatureImpl;
180 pub const hasGposMarkToLigatureFor = hasGposMarkToLigatureForImpl;
181 pub const hasGposSingleAdjustment = hasGposSingleAdjustmentImpl;
182 pub const hasGposSingleAdjustmentFor = hasGposSingleAdjustmentForImpl;
183 pub const hasGposCursiveAttachment = hasGposCursiveAttachmentImpl;
184 pub const hasGposCursiveAttachmentFor = hasGposCursiveAttachmentForImpl;
185 pub const hasGposMarkToMark = hasGposMarkToMarkImpl;
186 pub const hasGposMarkToMarkFor = hasGposMarkToMarkForImpl;
187 pub const glyphClass = glyphClassImpl;
188 pub const ligatureCaretCount = ligatureCaretCountImpl;
189 pub const ligatureCaret = ligatureCaretImpl;
190 pub const substituteGlyph = substituteGlyphImpl;
191 pub const substituteGlyphFor = substituteGlyphForImpl;
192 pub const gsubLookupCount = gsubLookupCountImpl;
193 pub const gsubLookupIsReverseFor = gsubLookupIsReverseForImpl;
194 pub const substitutionAt = substitutionAtImpl;
195 pub const substitutionAtFor = substitutionAtForImpl;
196 pub const substitutionAtForRange = substitutionAtForRangeImpl;
197 pub const substitutionGlyph = substitutionGlyphImpl;
198 pub const mathConstants = mathConstantsImpl;
199 pub const mathHorizontalAssemblyAlloc = mathHorizontalAssemblyAllocImpl;
200 pub const mathHorizontalVariant = mathHorizontalVariantImpl;
201 pub const mathTopAccentAttachment = mathTopAccentAttachmentImpl;
202 pub const mathVerticalAssemblyAlloc = mathVerticalAssemblyAllocImpl;
203 pub const mathVerticalVariant = mathVerticalVariantImpl;
204 pub const heightUnits = heightUnitsImpl;
205 const horizontalMetrics = horizontalMetricsImpl;
206 };
207
208 fn initImpl(data: []const u8) FontError!Face {
209 return initAtImpl(data, 0);
210 }
211
212 fn initAtImpl(data: []const u8, face_index: u32) FontError!Face {
213 const directory_offset = try fontDirectoryOffset(data, face_index);
214 return initDirectoryImpl(data, directory_offset);
215 }
216
217 fn initDirectoryImpl(data: []const u8, directory_offset: usize) FontError!Face {
218 if (data.len < 12) return error.InvalidFont;
219
220 const version = try readU32(data, directory_offset);
221 if (version != 0x00010000 and version != tagLiteral("true") and version != tagLiteral("OTTO")) {
222 return error.InvalidFont;
223 }
224 const has_cff_outlines = version == tagLiteral("OTTO");
225
226 const num_tables = try readU16(data, directory_offset + 4);
227 if (directory_offset > data.len or data.len - directory_offset < 12 or @as(usize, num_tables) > (data.len - directory_offset - 12) / 16) return error.InvalidFont;
228
229 const head = try tableAt(data, directory_offset, "head");
230 const hhea = try tableAt(data, directory_offset, "hhea");
231 const hmtx = try tableAt(data, directory_offset, "hmtx");
232 const maxp = try tableAt(data, directory_offset, "maxp");
233 const cmap_table = try tableAt(data, directory_offset, "cmap");
234
235 if (head.len < 54 or hhea.len < 36 or maxp.len < 6) return error.InvalidFont;
236
237 const units_per_em = try readU16(data, head.offset + 18);
238 const ascender = try readI16(data, hhea.offset + 4);
239 const descender = try readI16(data, hhea.offset + 6);
240 const line_gap = try readI16(data, hhea.offset + 8);
241 const num_h_metrics = try readU16(data, hhea.offset + 34);
242 const num_glyphs = try readU16(data, maxp.offset + 4);
243
244 if (num_h_metrics == 0) return error.InvalidFont;
245
246 const vertical_metrics = verticalMetrics(data, directory_offset, num_glyphs);
247 const vorg = if (tableOptionalAt(data, directory_offset, "VORG")) |vorg_table|
248 Vorg.init(data, vorg_table) catch null
249 else
250 null;
251 const typo_metrics = horizontalTypoMetrics(data, directory_offset);
252 const kern = if (has_cff_outlines)
253 null
254 else if (tableOptionalAt(data, directory_offset, "kern")) |kern_table|
255 Kern.init(data, kern_table) catch null
256 else
257 null;
258 const gsub = if (tableOptionalAt(data, directory_offset, "GSUB")) |gsub_table|
259 Gsub.init(data, gsub_table) catch null
260 else
261 null;
262 const gpos = if (tableOptionalAt(data, directory_offset, "GPOS")) |gpos_table|
263 Gpos.init(data, gpos_table) catch null
264 else
265 null;
266 const fvar = if (tableOptionalAt(data, directory_offset, "fvar")) |fvar_table|
267 Fvar.init(data, fvar_table) catch null
268 else
269 null;
270 const gdef = if (tableOptionalAt(data, directory_offset, "GDEF")) |gdef_table|
271 Gdef.init(data, gdef_table, if (fvar) |variation_table| variation_table.axis_count else null) catch null
272 else
273 null;
274 const avar = if (fvar) |variation_table|
275 if (tableOptionalAt(data, directory_offset, "avar")) |avar_table|
276 Avar.init(data, avar_table, variation_table.axis_count) catch null
277 else
278 null
279 else
280 null;
281 const hvar = if (fvar) |variation_table|
282 if (tableOptionalAt(data, directory_offset, "HVAR")) |hvar_table|
283 Hvar.init(data, hvar_table, variation_table.axis_count) catch null
284 else
285 null
286 else
287 null;
288 const vvar = if (fvar) |variation_table|
289 if (tableOptionalAt(data, directory_offset, "VVAR")) |vvar_table|
290 Vvar.init(data, vvar_table, variation_table.axis_count) catch null
291 else
292 null
293 else
294 null;
295 const mvar = if (fvar) |variation_table|
296 if (tableOptionalAt(data, directory_offset, "MVAR")) |mvar_table|
297 Mvar.init(data, mvar_table, variation_table.axis_count) catch null
298 else
299 null
300 else
301 null;
302 const math = if (tableOptionalAt(data, directory_offset, "MATH")) |math_table|
303 Math.init(data, math_table) catch null
304 else
305 null;
306
307 return .{
308 .data = data,
309 .directory_offset = directory_offset,
310 .units_per_em = if (units_per_em == 0) 1000 else units_per_em,
311 .ascender = ascender,
312 .descender = descender,
313 .line_gap = line_gap,
314 .typo_metrics = typo_metrics,
315 .num_glyphs = num_glyphs,
316 .num_h_metrics = num_h_metrics,
317 .num_v_metrics = vertical_metrics.count,
318 .cmap = try Cmap.init(data, cmap_table),
319 .hmtx = hmtx,
320 .vmtx = vertical_metrics.table,
321 .vorg = vorg,
322 .kern = kern,
323 .gsub = gsub,
324 .gpos = gpos,
325 .gdef = gdef,
326 .fvar = fvar,
327 .avar = avar,
328 .hvar = hvar,
329 .vvar = vvar,
330 .mvar = mvar,
331 .math = math,
332 };
333 }
334
335 fn glyphIdImpl(self: Face, codepoint: u21) u32 {
336 const glyph = self.cmap.map(self.data, @intCast(codepoint)) catch return 0;
337 if (glyph >= self.num_glyphs) return 0;
338 return glyph;
339 }
340
341 fn tableSliceImpl(self: Face, comptime name: []const u8) ?[]const u8 {
342 const found = tableOptionalAt(self.data, self.directory_offset, name) orelse return null;
343 return self.data[found.offset..][0..found.len];
344 }
345
346 fn tableSliceByTagImpl(self: Face, table_tag: Tag) ?[]const u8 {
347 const found = tableOptionalAtTag(self.data, self.directory_offset, table_tag) orelse return null;
348 return self.data[found.offset..][0..found.len];
349 }
350
351 fn mathConstantsImpl(self: Face) ?MathConstants {
352 if (self.math) |math| return math.constants;
353 return null;
354 }
355
356 fn mathTopAccentAttachmentImpl(self: Face, glyph_id: u32) ?MathValueRecord {
357 const math = self.math orelse return null;
358 return math.topAccentAttachment(self.data, glyph_id) catch null;
359 }
360
361 fn mathVerticalVariantImpl(self: Face, glyph_id: u32, target_measurement: u16) ?MathGlyphVariant {
362 const math = self.math orelse return null;
363 return math.verticalVariant(self.data, glyph_id, target_measurement) catch null;
364 }
365
366 fn mathHorizontalVariantImpl(self: Face, glyph_id: u32, target_measurement: u16) ?MathGlyphVariant {
367 const math = self.math orelse return null;
368 return math.horizontalVariant(self.data, glyph_id, target_measurement) catch null;
369 }
370
371 fn mathVerticalAssemblyAllocImpl(self: Face, allocator: std.mem.Allocator, glyph_id: u32, target_measurement: u16) std.mem.Allocator.Error!?MathGlyphAssembly {
372 const math = self.math orelse return null;
373 return math.verticalAssemblyAlloc(allocator, self.data, glyph_id, target_measurement) catch |err| switch (err) {
374 error.OutOfMemory => return error.OutOfMemory,
375 else => return null,
376 };
377 }
378
379 fn mathHorizontalAssemblyAllocImpl(self: Face, allocator: std.mem.Allocator, glyph_id: u32, target_measurement: u16) std.mem.Allocator.Error!?MathGlyphAssembly {
380 const math = self.math orelse return null;
381 return math.horizontalAssemblyAlloc(allocator, self.data, glyph_id, target_measurement) catch |err| switch (err) {
382 error.OutOfMemory => return error.OutOfMemory,
383 else => return null,
384 };
385 }
386
387 fn nameUtf8AllocImpl(self: Face, allocator: std.mem.Allocator, name_id: u16) !?[]u8 {
388 const name_table = tableOptionalAt(self.data, self.directory_offset, "name") orelse return null;
389 const record = (try bestNameRecord(self.data, name_table, name_id)) orelse return null;
390 return try utf16BeToUtf8Alloc(allocator, record.bytes);
391 }
392
393 fn glyphIdForVariationImpl(self: Face, codepoint: u21, variation_selector: u21) ?u32 {
394 const glyph = self.cmap.mapVariation(self.data, @intCast(codepoint), @intCast(variation_selector)) catch return null;
395 const mapped = glyph orelse return null;
396 if (mapped >= self.num_glyphs) return 0;
397 return mapped;
398 }
399
400 fn hasVariationSequencesImpl(self: Face) bool {
401 return self.cmap.variation != null;
402 }
403
404 fn variationAxisCountImpl(self: Face) u16 {
405 if (self.fvar) |fvar| return fvar.axis_count;
406 return 0;
407 }
408
409 fn variationAxisImpl(self: Face, axis_index: usize) ?VariationAxis {
410 if (self.fvar) |fvar| return fvar.axis(self.data, axis_index) catch null;
411 return null;
412 }
413
414 fn variationAxisForImpl(self: Face, axis_tag: Tag) ?VariationAxis {
415 const fvar = self.fvar orelse return null;
416 const axis_index = (fvar.axisIndex(self.data, axis_tag) catch return null) orelse return null;
417 return fvar.axis(self.data, axis_index) catch null;
418 }
419
420 fn validateVariationsImpl(self: Face, settings: []const VariationSetting) FontError!void {
421 if (settings.len == 0) return;
422 const fvar = self.fvar orelse return error.InvalidVariations;
423 for (settings) |setting| {
424 const axis_index = (try fvar.axisIndex(self.data, setting.tag)) orelse return error.InvalidVariations;
425 _ = try self.normalizedCoordinateAt(settings, axis_index);
426 }
427 }
428
429 fn normalizedVariationImpl(self: Face, setting: VariationSetting) FontError!?NormalizedVariation {
430 const fvar = self.fvar orelse return null;
431 const axis_index = (try fvar.axisIndex(self.data, setting.tag)) orelse return null;
432 return .{
433 .tag = setting.tag,
434 .value = try self.normalizedCoordinateAt(&.{setting}, axis_index),
435 };
436 }
437
438 fn normalizedCoordinateAtImpl(self: Face, settings: []const VariationSetting, axis_index: usize) FontError!i16 {
439 const fvar = self.fvar orelse return error.InvalidVariations;
440 const axis = try fvar.axis(self.data, axis_index);
441 var user_value = axis.default_value;
442 for (settings) |setting| {
443 if (setting.tag != axis.tag) continue;
444 user_value = try fixed16FromFloat(setting.value);
445 }
446 var normalized = normalizeVariationAxis(axis, user_value);
447 if (self.avar) |avar| {
448 normalized = try avar.map(self.data, axis_index, normalized);
449 }
450 return fixed16ToF2Dot14(normalized);
451 }
452
453 fn advanceWidthImpl(self: Face, glyph_id: u32) u16 {
454 const metric_count: usize = self.num_h_metrics;
455 const glyph_index = @as(usize, @intCast(glyph_id));
456 const metric_index = if (glyph_index < metric_count) glyph_index else metric_count - 1;
457 const offset = self.hmtx.offset + metric_index * 4;
458 if (offset + 2 > self.data.len) return self.units_per_em;
459 return readU16(self.data, offset) catch self.units_per_em;
460 }
461
462 fn advanceWidthForVariationsImpl(self: Face, glyph_id: u32, settings: []const VariationSetting) i32 {
463 const base = @as(i32, self.advanceWidth(glyph_id));
464 if (settings.len == 0) return base;
465 const delta = if (self.hvar) |hvar|
466 hvar.advanceWidthDelta(self.data, self, settings, glyph_id) catch 0
467 else
468 0;
469 return clampDesignMetric(base, delta);
470 }
471
472 fn advanceHeightImpl(self: Face, glyph_id: u32) u16 {
473 const metrics = self.vmtx orelse return self.heightUnits();
474 const metric_count: usize = self.num_v_metrics;
475 if (metric_count == 0) return self.heightUnits();
476 const glyph_index = @as(usize, @intCast(glyph_id));
477 const metric_index = if (glyph_index < metric_count) glyph_index else metric_count - 1;
478 if (metric_index > (std.math.maxInt(usize) - metrics.offset) / 4) return self.heightUnits();
479 const offset = metrics.offset + metric_index * 4;
480 if (offset + 2 > self.data.len) return self.heightUnits();
481 return readU16(self.data, offset) catch self.heightUnits();
482 }
483
484 fn advanceHeightForVariationsImpl(self: Face, glyph_id: u32, settings: []const VariationSetting) i32 {
485 const base = @as(i32, self.advanceHeight(glyph_id));
486 if (settings.len == 0) return base;
487 const delta = if (self.vvar) |vvar|
488 vvar.advanceHeightDelta(self.data, self, settings, glyph_id) catch 0
489 else
490 0;
491 return clampDesignMetric(base, delta);
492 }
493
494 fn verticalOriginYImpl(self: Face, glyph_id: u32) ?i16 {
495 const vorg = self.vorg orelse return null;
496 return vorg.originY(self.data, glyph_id) catch null;
497 }
498
499 fn metricVariationDeltaImpl(self: Face, metric_tag: Tag, settings: []const VariationSetting) i32 {
500 if (settings.len == 0) return 0;
501 const mvar = self.mvar orelse return 0;
502 return mvar.valueDelta(self.data, self, settings, metric_tag) catch 0;
503 }
504
505 fn ascenderForVariationsImpl(self: Face, settings: []const VariationSetting) i32 {
506 const metrics = self.horizontalMetrics();
507 return clampI32(@as(i64, metrics.ascender) + @as(i64, self.metricVariationDelta(tagLiteral("hasc"), settings)));
508 }
509
510 fn descenderForVariationsImpl(self: Face, settings: []const VariationSetting) i32 {
511 const metrics = self.horizontalMetrics();
512 return clampI32(@as(i64, metrics.descender) + @as(i64, self.metricVariationDelta(tagLiteral("hdsc"), settings)));
513 }
514
515 fn lineGapForVariationsImpl(self: Face, settings: []const VariationSetting) i32 {
516 const metrics = self.horizontalMetrics();
517 return clampI32(@as(i64, metrics.line_gap) + @as(i64, self.metricVariationDelta(tagLiteral("hlgp"), settings)));
518 }
519
520 fn heightUnitsForVariationsImpl(self: Face, settings: []const VariationSetting) i32 {
521 const height = @as(i64, self.ascenderForVariations(settings)) - @as(i64, self.descenderForVariations(settings));
522 if (height <= 0) return self.units_per_em;
523 return clampI32(height);
524 }
525
526 fn kerningImpl(self: Face, left_glyph: u32, right_glyph: u32) i16 {
527 if (self.kern) |kern| {
528 return kern.value(self.data, left_glyph, right_glyph) catch 0;
529 }
530 return 0;
531 }
532
533 fn hasGposImpl(self: Face) bool {
534 return self.gpos != null;
535 }
536
537 fn gposLookupCountImpl(self: Face) u16 {
538 if (self.gpos) |gpos| {
539 return gpos.lookupCount(self.data) catch 0;
540 }
541 return 0;
542 }
543
544 fn gposLookupTypeImpl(self: Face, lookup_index: u16) u16 {
545 if (self.gpos) |gpos| {
546 return gpos.lookupResolvedType(self.data, lookup_index) catch 0;
547 }
548 return 0;
549 }
550
551 fn gposVariationValueImpl(self: Face, base: i32, settings: []const VariationSetting, packed_index: u32) i32 {
552 return gposVariationValueForFace(base, self, settings, packed_index);
553 }
554
555 fn variationContextImpl(self: *const Face) lookups.VariationContext {
556 return .{
557 .ptr = self,
558 .variation_axis_count = faceVariationAxisCount,
559 .normalized_coordinate_at = faceNormalizedCoordinateAt,
560 };
561 }
562
563 fn hasGsubImpl(self: Face) bool {
564 return self.gsub != null;
565 }
566
567 fn usesAutomaticFractionsImpl(self: Face, selection: LayoutSelection) bool {
568 if (self.gsub) |gsub| {
569 return gsub.usesAutomaticFractions(self.data, selection) catch false;
570 }
571 return false;
572 }
573
574 fn usesArabicJoiningFormsImpl(self: Face, selection: LayoutSelection) bool {
575 if (self.gsub) |gsub| {
576 return gsub.usesArabicJoiningForms(self.data, selection) catch false;
577 }
578 return false;
579 }
580
581 fn gposPairAdjustmentImpl(self: Face, left_glyph: u32, right_glyph: u32) GposPairAdjustment {
582 return self.gposPairAdjustmentFor(.{}, left_glyph, right_glyph);
583 }
584
585 fn gposPairAdjustmentForImpl(self: Face, selection: LayoutSelection, left_glyph: u32, right_glyph: u32) GposPairAdjustment {
586 if (self.gpos) |gpos| {
587 return gpos.pairAdjustment(self.data, self.variationContext(), selection, self.gdef, left_glyph, right_glyph) catch .{};
588 }
589 return .{};
590 }
591
592 fn gposPairAdjustmentBeforeForImpl(self: Face, selection: LayoutSelection, glyph_ids: []const u32, glyph_index: usize) ?GposPairAdjustmentBefore {
593 return self.gposPairAdjustmentBeforeForRange(selection, glyph_ids, glyph_index, null);
594 }
595
596 fn gposPairAdjustmentBeforeForRangeImpl(self: Face, selection: LayoutSelection, glyph_ids: []const u32, glyph_index: usize, source: ?FeatureRange) ?GposPairAdjustmentBefore {
597 if (self.gpos) |gpos| {
598 return gpos.pairAdjustmentBefore(self.data, self.variationContext(), selection, self.gdef, glyph_ids, glyph_index, source) catch null;
599 }
600 return null;
601 }
602
603 fn gposPairAdjustmentBeforeAtLookupForRangeImpl(self: Face, selection: LayoutSelection, lookup_index: u16, glyph_ids: []const u32, glyph_index: usize, source: ?FeatureRange) ?GposPairAdjustmentBefore {
604 if (self.gpos) |gpos| {
605 return gpos.pairAdjustmentBeforeAtLookup(self.data, self.variationContext(), selection, self.gdef, lookup_index, glyph_ids, glyph_index, source) catch null;
606 }
607 return null;
608 }
609
610 fn gposContextualPairAdjustmentAtForImpl(self: Face, selection: LayoutSelection, glyph_ids: []const u32, glyph_index: usize) ?GposContextualPairAdjustment {
611 return self.gposContextualPairAdjustmentAtForRange(selection, glyph_ids, glyph_index, null);
612 }
613
614 fn gposContextualPairAdjustmentAtForRangeImpl(self: Face, selection: LayoutSelection, glyph_ids: []const u32, glyph_index: usize, source: ?FeatureRange) ?GposContextualPairAdjustment {
615 if (self.gpos) |gpos| {
616 return gpos.contextualPairAdjustmentAt(self.data, self.variationContext(), selection, self.gdef, glyph_ids, glyph_index, source) catch null;
617 }
618 return null;
619 }
620
621 fn gposContextualPairAdjustmentAtLookupForRangeImpl(self: Face, selection: LayoutSelection, lookup_index: u16, glyph_ids: []const u32, glyph_index: usize, source: ?FeatureRange) ?GposContextualPairAdjustment {
622 if (self.gpos) |gpos| {
623 return gpos.contextualPairAdjustmentAtLookup(self.data, self.variationContext(), selection, self.gdef, lookup_index, glyph_ids, glyph_index, source) catch null;
624 }
625 return null;
626 }
627
628 fn hasGposContextualPairAdjustmentForImpl(self: Face, selection: LayoutSelection) bool {
629 if (self.gpos) |gpos| {
630 return gpos.hasActiveContextualPairAdjustment(self.data, self.variationContext(), selection) catch false;
631 }
632 return false;
633 }
634
635 fn gposSingleAdjustmentImpl(self: Face, glyph_id: u32) GposValueAdjustment {
636 return self.gposSingleAdjustmentFor(.{}, glyph_id);
637 }
638
639 fn gposSingleAdjustmentForImpl(self: Face, selection: LayoutSelection, glyph_id: u32) GposValueAdjustment {
640 var glyphs = [_]u32{glyph_id};
641 const result = self.gposSingleAdjustmentAtFor(selection, &glyphs, 0) orelse return .{};
642 if (result.target_offset != 0) return .{};
643 return result.adjustment;
644 }
645
646 fn gposSingleAdjustmentAtForImpl(self: Face, selection: LayoutSelection, glyph_ids: []const u32, glyph_index: usize) ?GposSingleAdjustment {
647 return self.gposSingleAdjustmentAtForRange(selection, glyph_ids, glyph_index, null);
648 }
649
650 fn gposSingleAdjustmentAtForRangeImpl(self: Face, selection: LayoutSelection, glyph_ids: []const u32, glyph_index: usize, source: ?FeatureRange) ?GposSingleAdjustment {
651 if (self.gpos) |gpos| {
652 return gpos.singleAdjustmentAt(self.data, self.variationContext(), selection, self.gdef, glyph_ids, glyph_index, source) catch null;
653 }
654 return null;
655 }
656
657 fn gposSingleAdjustmentAtLookupForRangeImpl(self: Face, selection: LayoutSelection, lookup_index: u16, glyph_ids: []const u32, glyph_index: usize, source: ?FeatureRange) ?GposSingleAdjustment {
658 if (self.gpos) |gpos| {
659 return gpos.singleAdjustmentAtLookup(self.data, self.variationContext(), selection, self.gdef, lookup_index, glyph_ids, glyph_index, source) catch null;
660 }
661 return null;
662 }
663
664 fn gposCursiveAttachmentImpl(self: Face, exit_glyph: u32, entry_glyph: u32) ?GposCursiveAttachment {
665 return self.gposCursiveAttachmentFor(.{}, exit_glyph, entry_glyph);
666 }
667
668 fn gposCursiveAttachmentForImpl(self: Face, selection: LayoutSelection, exit_glyph: u32, entry_glyph: u32) ?GposCursiveAttachment {
669 return self.gposCursiveAttachmentForRange(selection, exit_glyph, entry_glyph, null);
670 }
671
672 fn gposCursiveAttachmentForRangeImpl(self: Face, selection: LayoutSelection, exit_glyph: u32, entry_glyph: u32, source: ?FeatureRange) ?GposCursiveAttachment {
673 if (self.gpos) |gpos| {
674 return gpos.cursiveAttachment(self.data, self.variationContext(), selection, self.gdef, exit_glyph, entry_glyph, source) catch null;
675 }
676 return null;
677 }
678
679 fn gposCursiveAttachmentAtLookupForRangeImpl(self: Face, selection: LayoutSelection, lookup_index: u16, exit_glyph: u32, entry_glyph: u32, source: ?FeatureRange) ?GposCursiveAttachment {
680 if (self.gpos) |gpos| {
681 return gpos.cursiveAttachmentAtLookup(self.data, self.variationContext(), selection, self.gdef, lookup_index, exit_glyph, entry_glyph, source) catch null;
682 }
683 return null;
684 }
685
686 fn gposMarkToBaseAdjustmentImpl(self: Face, base_glyph: u32, mark_glyph: u32) ?GposValueAdjustment {
687 return self.gposMarkToBaseAdjustmentFor(.{}, base_glyph, mark_glyph);
688 }
689
690 fn gposMarkToBaseAdjustmentForImpl(self: Face, selection: LayoutSelection, base_glyph: u32, mark_glyph: u32) ?GposValueAdjustment {
691 return self.gposMarkToBaseAdjustmentForRange(selection, base_glyph, mark_glyph, null);
692 }
693
694 fn gposMarkToBaseAdjustmentForRangeImpl(self: Face, selection: LayoutSelection, base_glyph: u32, mark_glyph: u32, source: ?FeatureRange) ?GposValueAdjustment {
695 if (self.gpos) |gpos| {
696 return gpos.markToBaseAdjustment(self.data, self.variationContext(), selection, self.gdef, base_glyph, mark_glyph, source) catch null;
697 }
698 return null;
699 }
700
701 fn gposMarkToBaseAdjustmentAtLookupForRangeImpl(self: Face, selection: LayoutSelection, lookup_index: u16, base_glyph: u32, mark_glyph: u32, source: ?FeatureRange) ?GposValueAdjustment {
702 if (self.gpos) |gpos| {
703 return gpos.markToBaseAdjustmentAtLookup(self.data, self.variationContext(), selection, self.gdef, lookup_index, base_glyph, mark_glyph, source) catch null;
704 }
705 return null;
706 }
707
708 fn gposMarkToLigatureAdjustmentImpl(self: Face, ligature_glyph: u32, mark_glyph: u32, component_index: u16) ?GposValueAdjustment {
709 return self.gposMarkToLigatureAdjustmentFor(.{}, ligature_glyph, mark_glyph, component_index);
710 }
711
712 fn gposMarkToLigatureAdjustmentForImpl(self: Face, selection: LayoutSelection, ligature_glyph: u32, mark_glyph: u32, component_index: u16) ?GposValueAdjustment {
713 return self.gposMarkToLigatureAdjustmentForRange(selection, ligature_glyph, mark_glyph, component_index, null);
714 }
715
716 fn gposMarkToLigatureAdjustmentForRangeImpl(self: Face, selection: LayoutSelection, ligature_glyph: u32, mark_glyph: u32, component_index: u16, source: ?FeatureRange) ?GposValueAdjustment {
717 if (self.gpos) |gpos| {
718 return gpos.markToLigatureAdjustment(self.data, self.variationContext(), selection, self.gdef, ligature_glyph, mark_glyph, component_index, source) catch null;
719 }
720 return null;
721 }
722
723 fn gposMarkToLigatureAdjustmentAtLookupForRangeImpl(self: Face, selection: LayoutSelection, lookup_index: u16, ligature_glyph: u32, mark_glyph: u32, component_index: u16, source: ?FeatureRange) ?GposValueAdjustment {
724 if (self.gpos) |gpos| {
725 return gpos.markToLigatureAdjustmentAtLookup(self.data, self.variationContext(), selection, self.gdef, lookup_index, ligature_glyph, mark_glyph, component_index, source) catch null;
726 }
727 return null;
728 }
729
730 fn gposMarkToMarkAdjustmentImpl(self: Face, base_mark_glyph: u32, mark_glyph: u32) ?GposValueAdjustment {
731 return self.gposMarkToMarkAdjustmentFor(.{}, base_mark_glyph, mark_glyph);
732 }
733
734 fn gposMarkToMarkAdjustmentForImpl(self: Face, selection: LayoutSelection, base_mark_glyph: u32, mark_glyph: u32) ?GposValueAdjustment {
735 return self.gposMarkToMarkAdjustmentForRange(selection, base_mark_glyph, mark_glyph, null);
736 }
737
738 fn gposMarkToMarkAdjustmentForRangeImpl(self: Face, selection: LayoutSelection, base_mark_glyph: u32, mark_glyph: u32, source: ?FeatureRange) ?GposValueAdjustment {
739 if (self.gpos) |gpos| {
740 return gpos.markToMarkAdjustment(self.data, self.variationContext(), selection, self.gdef, base_mark_glyph, mark_glyph, source) catch null;
741 }
742 return null;
743 }
744
745 fn gposMarkToMarkAdjustmentAtLookupForRangeImpl(self: Face, selection: LayoutSelection, lookup_index: u16, base_mark_glyph: u32, mark_glyph: u32, source: ?FeatureRange) ?GposValueAdjustment {
746 if (self.gpos) |gpos| {
747 return gpos.markToMarkAdjustmentAtLookup(self.data, self.variationContext(), selection, self.gdef, lookup_index, base_mark_glyph, mark_glyph, source) catch null;
748 }
749 return null;
750 }
751
752 fn hasGposMarkToBaseImpl(self: Face) bool {
753 return self.hasGposMarkToBaseFor(.{});
754 }
755
756 fn hasGposMarkToBaseForImpl(self: Face, selection: LayoutSelection) bool {
757 if (self.gpos) |gpos| {
758 return gpos.hasActiveLookupType(self.data, self.variationContext(), selection, 4) catch false;
759 }
760 return false;
761 }
762
763 fn hasGposMarkToLigatureImpl(self: Face) bool {
764 return self.hasGposMarkToLigatureFor(.{});
765 }
766
767 fn hasGposMarkToLigatureForImpl(self: Face, selection: LayoutSelection) bool {
768 if (self.gpos) |gpos| {
769 return gpos.hasActiveLookupType(self.data, self.variationContext(), selection, 5) catch false;
770 }
771 return false;
772 }
773
774 fn hasGposSingleAdjustmentImpl(self: Face) bool {
775 return self.hasGposSingleAdjustmentFor(.{});
776 }
777
778 fn hasGposSingleAdjustmentForImpl(self: Face, selection: LayoutSelection) bool {
779 if (self.gpos) |gpos| {
780 return gpos.hasActiveSingleAdjustment(self.data, self.variationContext(), selection) catch false;
781 }
782 return false;
783 }
784
785 fn hasGposCursiveAttachmentImpl(self: Face) bool {
786 return self.hasGposCursiveAttachmentFor(.{});
787 }
788
789 fn hasGposCursiveAttachmentForImpl(self: Face, selection: LayoutSelection) bool {
790 if (self.gpos) |gpos| {
791 return gpos.hasActiveLookupType(self.data, self.variationContext(), selection, 3) catch false;
792 }
793 return false;
794 }
795
796 fn hasGposMarkToMarkImpl(self: Face) bool {
797 return self.hasGposMarkToMarkFor(.{});
798 }
799
800 fn hasGposMarkToMarkForImpl(self: Face, selection: LayoutSelection) bool {
801 if (self.gpos) |gpos| {
802 return gpos.hasActiveLookupType(self.data, self.variationContext(), selection, 6) catch false;
803 }
804 return false;
805 }
806
807 fn glyphClassImpl(self: Face, glyph_id: u32) GlyphClass {
808 if (self.gdef) |gdef| {
809 return gdef.glyphClass(self.data, glyph_id) catch .unknown;
810 }
811 return .unknown;
812 }
813
814 fn ligatureCaretCountImpl(self: Face, glyph_id: u32) u16 {
815 if (self.gdef) |gdef| {
816 return gdef.ligatureCaretCount(self.data, glyph_id) catch 0;
817 }
818 return 0;
819 }
820
821 fn ligatureCaretImpl(self: Face, glyph_id: u32, caret_index: usize) ?LigatureCaret {
822 if (self.gdef) |gdef| {
823 return gdef.ligatureCaret(self.data, glyph_id, caret_index) catch null;
824 }
825 return null;
826 }
827
828 fn substituteGlyphImpl(self: Face, glyph_id: u32) u32 {
829 return self.substituteGlyphFor(.{}, glyph_id);
830 }
831
832 fn substituteGlyphForImpl(self: Face, selection: LayoutSelection, glyph_id: u32) u32 {
833 var glyphs = [_]u32{glyph_id};
834 const lookup_count = self.gsubLookupCount() catch return glyph_id;
835 for (0..lookup_count) |lookup_index| {
836 const substitute = self.substitutionAtFor(selection, @intCast(lookup_index), &glyphs, 0) catch return glyph_id;
837 if (substitute) |result| {
838 if (result.component_count == 1 and result.replacement_count == 1 and result.glyph_id < self.num_glyphs) return result.glyph_id;
839 }
840 }
841 return glyph_id;
842 }
843
844 fn gsubLookupCountImpl(self: Face) FontError!u16 {
845 if (self.gsub) |gsub| return gsub.lookupCount(self.data);
846 return 0;
847 }
848
849 fn gsubLookupIsReverseForImpl(self: Face, selection: LayoutSelection, lookup_index: u16) FontError!bool {
850 if (self.gsub) |gsub| return gsub.lookupIsReverse(self.data, selection, lookup_index);
851 return false;
852 }
853
854 fn substitutionAtImpl(self: Face, lookup_index: u16, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {
855 return self.substitutionAtFor(.{}, lookup_index, glyph_ids, glyph_index);
856 }
857
858 fn substitutionAtForImpl(self: Face, selection: LayoutSelection, lookup_index: u16, glyph_ids: []const u32, glyph_index: usize) FontError!?GsubSubstitution {
859 return self.substitutionAtForRange(selection, lookup_index, glyph_ids, glyph_index, null);
860 }
861
862 fn substitutionAtForRangeImpl(self: Face, selection: LayoutSelection, lookup_index: u16, glyph_ids: []const u32, glyph_index: usize, source: ?FeatureRange) FontError!?GsubSubstitution {
863 if (self.gsub) |gsub| {
864 const result = try gsub.substitutionAtWithGdef(self.data, self.variationContext(), selection, self.gdef, lookup_index, glyph_ids, glyph_index, source);
865 if (result) |substitution| {
866 if (substitution.component_count == 0 or substitution.replacement_count == 0) return null;
867 for (0..substitution.replacement_count) |replacement_index| {
868 _ = self.substitutionGlyph(substitution, replacement_index) orelse return null;
869 }
870 return substitution;
871 }
872 }
873 return null;
874 }
875
876 fn substitutionGlyphImpl(self: Face, substitution: GsubSubstitution, replacement_index: usize) ?u32 {
877 if (replacement_index >= substitution.replacement_count) return null;
878 if (substitution.replacement_glyphs_offset == 0) {
879 if (replacement_index != 0) return null;
880 return if (substitution.glyph_id < self.num_glyphs) substitution.glyph_id else null;
881 }
882 const byte_delta = replacement_index * 2;
883 if (substitution.replacement_glyphs_offset > std.math.maxInt(usize) - byte_delta) return null;
884 const offset = substitution.replacement_glyphs_offset + byte_delta;
885 const glyph = readU16(self.data, offset) catch return null;
886 return if (glyph < self.num_glyphs) glyph else null;
887 }
888
889 fn heightUnitsImpl(self: Face) u16 {
890 const height = @as(i32, self.ascender) - @as(i32, self.descender);
891 if (height <= 0 or height > std.math.maxInt(u16)) return self.units_per_em;
892 return @intCast(height);
893 }
894
895 fn horizontalMetricsImpl(self: Face) HorizontalMetrics {
896 return self.typo_metrics orelse .{
897 .ascender = self.ascender,
898 .descender = self.descender,
899 .line_gap = self.line_gap,
900 };
901 }
902
903 fn faceVariationAxisCount(ptr: *const anyopaque) u16 {
904 const face: *const Face = @ptrCast(@alignCast(ptr));
905 return face.variationAxisCount();
906 }
907
908 fn faceNormalizedCoordinateAt(ptr: *const anyopaque, settings: []const VariationSetting, axis_index: usize) FontError!i16 {
909 const face: *const Face = @ptrCast(@alignCast(ptr));
910 return try face.normalizedCoordinateAt(settings, axis_index);
911 }
912
913 fn gposVariationValueForFace(base: i32, face: Face, settings: []const VariationSetting, packed_index: u32) i32 {
914 if (packed_index == noGposVariationIndex or settings.len == 0) return base;
915 const gdef = face.gdef orelse return base;
916 const index = DeltaSetIndex{
917 .outer = packed_index >> 16,
918 .inner = packed_index & 0xffff,
919 };
920 const delta = gdef.variationDelta(face.data, face, settings, index) catch return base;
921 return clampI32(@as(i64, base) + @as(i64, delta));
922 }
923
924 test "Face rejects empty data" {
925 try std.testing.expectError(error.InvalidFont, Face.init(&.{}));
926 }
927
928 test "Face maps BMP codepoints through cmap format 4" {
929 const bytes = try test_font.create(std.testing.allocator);
930 defer std.testing.allocator.free(bytes);
931
932 const face = try Face.init(bytes);
933 try std.testing.expectEqual(@as(u16, 1000), face.units_per_em);
934 try std.testing.expectEqual(@as(u16, 500), face.advanceWidth(face.glyphId('A')));
935 try std.testing.expectEqual(@as(u32, 'A' - 31), face.glyphId('A'));
936 try std.testing.expectEqual(@as(u32, 96), face.glyphId(0x25cc));
937 try std.testing.expectEqual(@as(u32, 0), face.glyphId(0x20ac));
938 }
939
940 test "Face opens indexed OpenType collection faces" {
941 const bytes = try test_font.createCollection(std.testing.allocator);
942 defer std.testing.allocator.free(bytes);
943
944 const first = try Face.init(bytes);
945 const second = try Face.initAt(bytes, 1);
946 const a = first.glyphId('A');
947
948 try std.testing.expectEqual(a, first.substituteGlyph(a));
949 try std.testing.expectEqual(second.glyphId('Z'), second.substituteGlyph(second.glyphId('A')));
950 try std.testing.expectError(error.InvalidFont, Face.initAt(bytes, 2));
951
952 const plain = try test_font.create(std.testing.allocator);
953 defer std.testing.allocator.free(plain);
954 try std.testing.expectError(error.InvalidFont, Face.initAt(plain, 1));
955 }
956
957 test "Face exposes table slices for indexed faces" {
958 const bytes = try test_font.createCollection(std.testing.allocator);
959 defer std.testing.allocator.free(bytes);
960
961 const second = try Face.initAt(bytes, 1);
962 const head = second.tableSlice("head").?;
963 const by_tag = second.tableSliceByTag(tag("head")).?;
964
965 try std.testing.expectEqual(@as(usize, 54), head.len);
966 try std.testing.expectEqualSlices(u8, head, by_tag);
967 try std.testing.expectEqual(@as(?[]const u8, null), second.tableSlice("COLR"));
968 }
969
970 test "Face decodes UTF-16BE name records" {
971 const bytes = try test_font.create(std.testing.allocator);
972 defer std.testing.allocator.free(bytes);
973
974 const face = try Face.init(bytes);
975 const family = (try face.nameUtf8Alloc(std.testing.allocator, 1)).?;
976 defer std.testing.allocator.free(family);
977 const full = (try face.nameUtf8Alloc(std.testing.allocator, 4)).?;
978 defer std.testing.allocator.free(full);
979
980 try std.testing.expectEqualStrings("Tiny Fixture", family);
981 try std.testing.expectEqualStrings("Tiny Fixture Regular", full);
982 try std.testing.expectEqual(@as(?[]u8, null), try face.nameUtf8Alloc(std.testing.allocator, 6));
983 }
984
985 test "Face reads vertical advance metrics from vmtx" {
986 const bytes = try test_font.createWithVerticalMetrics(std.testing.allocator);
987 defer std.testing.allocator.free(bytes);
988
989 const face = try Face.init(bytes);
990
991 try std.testing.expectEqual(@as(u16, 700), face.advanceHeight(face.glyphId('A')));
992 try std.testing.expectEqual(@as(u16, 900), face.advanceHeight(face.glyphId('B')));
993 try std.testing.expectEqual(@as(u16, 700), face.advanceHeight(face.glyphId('!')));
994 }
995
996 test "Face reads vertical origin metrics from VORG" {
997 const bytes = try test_font.createWithVerticalOrigins(std.testing.allocator);
998 defer std.testing.allocator.free(bytes);
999
1000 const face = try Face.init(bytes);
1001
1002 try std.testing.expectEqual(@as(?i16, 760), face.verticalOriginY(face.glyphId('A')));
1003 try std.testing.expectEqual(@as(?i16, 880), face.verticalOriginY(face.glyphId('B')));
1004 }
1005
1006 test "Face falls back to line height without vertical metrics" {
1007 const bytes = try test_font.create(std.testing.allocator);
1008 defer std.testing.allocator.free(bytes);
1009
1010 const face = try Face.init(bytes);
1011
1012 try std.testing.expectEqual(@as(u16, 1000), face.advanceHeight(face.glyphId('A')));
1013 }
1014
1015 test "Face maps non-default Unicode variation sequences through cmap format 14" {
1016 const bytes = try test_font.createWithVariationSequences(std.testing.allocator);
1017 defer std.testing.allocator.free(bytes);
1018
1019 const face = try Face.init(bytes);
1020
1021 try std.testing.expectEqual(@as(u32, 'A' - 31), face.glyphId('A'));
1022 try std.testing.expectEqual(@as(?u32, 96), face.glyphIdForVariation('A', 0xfe0f));
1023 try std.testing.expectEqual(@as(?u32, null), face.glyphIdForVariation('B', 0xfe0f));
1024 try std.testing.expectEqual(@as(?u32, null), face.glyphIdForVariation('A', 0xfe0e));
1025 }
1026
1027 test "Face reads fvar variation axes" {
1028 const bytes = try test_font.createWithVariations(std.testing.allocator);
1029 defer std.testing.allocator.free(bytes);
1030
1031 const face = try Face.init(bytes);
1032 try std.testing.expectEqual(@as(u16, 2), face.variationAxisCount());
1033
1034 const weight = face.variationAxis(0).?;
1035 try std.testing.expectEqual(tag("wght"), weight.tag);
1036 try std.testing.expectEqual(@as(i32, 100 << 16), weight.min_value);
1037 try std.testing.expectEqual(@as(i32, 400 << 16), weight.default_value);
1038 try std.testing.expectEqual(@as(i32, 900 << 16), weight.max_value);
1039 try std.testing.expectEqual(@as(u16, 0), weight.flags);
1040 try std.testing.expectEqual(@as(u16, 256), weight.name_id);
1041 try std.testing.expectEqual(@as(f32, 400), fixed16ToFloat(weight.default_value));
1042
1043 const width = face.variationAxisFor(tag("wdth")).?;
1044 try std.testing.expectEqual(@as(i32, 50 << 16), width.min_value);
1045 try std.testing.expectEqual(@as(i32, 100 << 16), width.default_value);
1046 try std.testing.expectEqual(@as(i32, 200 << 16), width.max_value);
1047 try std.testing.expectEqual(@as(u16, 1), width.flags);
1048 try std.testing.expectEqual(@as(?VariationAxis, null), face.variationAxis(2));
1049 try std.testing.expectEqual(@as(?VariationAxis, null), face.variationAxisFor(tag("opsz")));
1050 }
1051
1052 test "Face normalizes fvar variation coordinates" {
1053 const bytes = try test_font.createWithVariations(std.testing.allocator);
1054 defer std.testing.allocator.free(bytes);
1055
1056 const face = try Face.init(bytes);
1057 try std.testing.expectEqual(@as(i16, 0), try face.normalizedCoordinateAt(&.{}, 0));
1058 try std.testing.expectEqual(@as(i16, -8192), try face.normalizedCoordinateAt(&.{.{ .tag = tag("wght"), .value = 250.0 }}, 0));
1059 try std.testing.expectEqual(@as(i16, 8192), try face.normalizedCoordinateAt(&.{.{ .tag = tag("wght"), .value = 650.0 }}, 0));
1060 try std.testing.expectEqual(@as(i16, 16384), try face.normalizedCoordinateAt(&.{.{ .tag = tag("wght"), .value = 1200.0 }}, 0));
1061 try std.testing.expectEqual(@as(i16, -16384), try face.normalizedCoordinateAt(&.{.{ .tag = tag("wdth"), .value = 25.0 }}, 1));
1062
1063 const normalized = (try face.normalizedVariation(.{ .tag = tag("wght"), .value = 650.0 })).?;
1064 try std.testing.expectEqual(tag("wght"), normalized.tag);
1065 try std.testing.expectEqual(@as(i16, 8192), normalized.value);
1066 try std.testing.expectEqual(@as(?NormalizedVariation, null), try face.normalizedVariation(.{ .tag = tag("opsz"), .value = 12.0 }));
1067 try std.testing.expectError(error.InvalidVariations, face.validateVariations(&.{.{ .tag = tag("opsz"), .value = 12.0 }}));
1068 try std.testing.expectError(error.InvalidVariations, face.validateVariations(&.{.{ .tag = tag("wght"), .value = std.math.nan(f32) }}));
1069 }
1070
1071 test "Face applies avar variation coordinate remapping" {
1072 const bytes = try test_font.createWithAxisVariations(std.testing.allocator);
1073 defer std.testing.allocator.free(bytes);
1074
1075 const face = try Face.init(bytes);
1076 try std.testing.expectEqual(@as(i16, -4096), try face.normalizedCoordinateAt(&.{.{ .tag = tag("wght"), .value = 250.0 }}, 0));
1077 try std.testing.expectEqual(@as(i16, 12288), try face.normalizedCoordinateAt(&.{.{ .tag = tag("wght"), .value = 650.0 }}, 0));
1078 try std.testing.expectEqual(@as(i16, 8192), try face.normalizedCoordinateAt(&.{.{ .tag = tag("wdth"), .value = 150.0 }}, 1));
1079 }
1080
1081 test "Face applies HVAR advance width deltas" {
1082 const bytes = try test_font.createWithHorizontalMetricVariations(std.testing.allocator);
1083 defer std.testing.allocator.free(bytes);
1084
1085 const face = try Face.init(bytes);
1086 const a = face.glyphId('A');
1087 const v = face.glyphId('V');
1088
1089 try std.testing.expectEqual(@as(u16, 500), face.advanceWidth(a));
1090 try std.testing.expectEqual(@as(i32, 500), face.advanceWidthForVariations(a, &.{}));
1091 try std.testing.expectEqual(@as(i32, 500), face.advanceWidthForVariations(a, &.{.{ .tag = tag("wght"), .value = 400.0 }}));
1092 try std.testing.expectEqual(@as(i32, 550), face.advanceWidthForVariations(a, &.{.{ .tag = tag("wght"), .value = 650.0 }}));
1093 try std.testing.expectEqual(@as(i32, 600), face.advanceWidthForVariations(a, &.{.{ .tag = tag("wght"), .value = 900.0 }}));
1094 try std.testing.expectEqual(@as(i32, 500), face.advanceWidthForVariations(v, &.{.{ .tag = tag("wght"), .value = 900.0 }}));
1095 }
1096
1097 test "Face applies avar-normalized coordinates to HVAR" {
1098 const bytes = try test_font.createWithHorizontalMetricAxisVariations(std.testing.allocator);
1099 defer std.testing.allocator.free(bytes);
1100
1101 const face = try Face.init(bytes);
1102 const a = face.glyphId('A');
1103
1104 try std.testing.expectEqual(@as(i32, 575), face.advanceWidthForVariations(a, &.{.{ .tag = tag("wght"), .value = 650.0 }}));
1105 }
1106
1107 test "Face applies VVAR advance height deltas" {
1108 const bytes = try test_font.createWithVerticalMetricVariations(std.testing.allocator);
1109 defer std.testing.allocator.free(bytes);
1110
1111 const face = try Face.init(bytes);
1112 const a = face.glyphId('A');
1113 const b = face.glyphId('B');
1114
1115 try std.testing.expectEqual(@as(u16, 700), face.advanceHeight(a));
1116 try std.testing.expectEqual(@as(i32, 700), face.advanceHeightForVariations(a, &.{}));
1117 try std.testing.expectEqual(@as(i32, 700), face.advanceHeightForVariations(a, &.{.{ .tag = tag("wght"), .value = 400.0 }}));
1118 try std.testing.expectEqual(@as(i32, 750), face.advanceHeightForVariations(a, &.{.{ .tag = tag("wght"), .value = 650.0 }}));
1119 try std.testing.expectEqual(@as(i32, 800), face.advanceHeightForVariations(a, &.{.{ .tag = tag("wght"), .value = 900.0 }}));
1120 try std.testing.expectEqual(@as(i32, 900), face.advanceHeightForVariations(b, &.{.{ .tag = tag("wght"), .value = 900.0 }}));
1121 }
1122
1123 test "Face applies avar-normalized coordinates to VVAR" {
1124 const bytes = try test_font.createWithVerticalMetricAxisVariations(std.testing.allocator);
1125 defer std.testing.allocator.free(bytes);
1126
1127 const face = try Face.init(bytes);
1128 const a = face.glyphId('A');
1129
1130 try std.testing.expectEqual(@as(i32, 775), face.advanceHeightForVariations(a, &.{.{ .tag = tag("wght"), .value = 650.0 }}));
1131 }
1132
1133 test "Face applies MVAR horizontal metric deltas" {
1134 const bytes = try test_font.createWithMetricVariations(std.testing.allocator);
1135 defer std.testing.allocator.free(bytes);
1136
1137 const face = try Face.init(bytes);
1138
1139 try std.testing.expectEqual(@as(i32, 800), face.ascenderForVariations(&.{}));
1140 try std.testing.expectEqual(@as(i32, 800), face.ascenderForVariations(&.{.{ .tag = tag("wght"), .value = 400.0 }}));
1141 try std.testing.expectEqual(@as(i32, 860), face.ascenderForVariations(&.{.{ .tag = tag("wght"), .value = 650.0 }}));
1142 try std.testing.expectEqual(@as(i32, 920), face.ascenderForVariations(&.{.{ .tag = tag("wght"), .value = 900.0 }}));
1143
1144 try std.testing.expectEqual(@as(i32, -200), face.descenderForVariations(&.{}));
1145 try std.testing.expectEqual(@as(i32, -240), face.descenderForVariations(&.{.{ .tag = tag("wght"), .value = 650.0 }}));
1146 try std.testing.expectEqual(@as(i32, -280), face.descenderForVariations(&.{.{ .tag = tag("wght"), .value = 900.0 }}));
1147
1148 try std.testing.expectEqual(@as(i32, 40), face.lineGapForVariations(&.{}));
1149 try std.testing.expectEqual(@as(i32, 50), face.lineGapForVariations(&.{.{ .tag = tag("wght"), .value = 650.0 }}));
1150 try std.testing.expectEqual(@as(i32, 60), face.lineGapForVariations(&.{.{ .tag = tag("wght"), .value = 900.0 }}));
1151
1152 try std.testing.expectEqual(@as(i32, 0), face.metricVariationDelta(tag("vasc"), &.{.{ .tag = tag("wght"), .value = 900.0 }}));
1153 try std.testing.expectEqual(@as(i32, 1000), face.heightUnitsForVariations(&.{}));
1154 try std.testing.expectEqual(@as(i32, 1100), face.heightUnitsForVariations(&.{.{ .tag = tag("wght"), .value = 650.0 }}));
1155 try std.testing.expectEqual(@as(i32, 1200), face.heightUnitsForVariations(&.{.{ .tag = tag("wght"), .value = 900.0 }}));
1156 }
1157
1158 test "Face applies avar-normalized coordinates to MVAR" {
1159 const bytes = try test_font.createWithMetricAxisVariations(std.testing.allocator);
1160 defer std.testing.allocator.free(bytes);
1161
1162 const face = try Face.init(bytes);
1163
1164 try std.testing.expectEqual(@as(i32, 890), face.ascenderForVariations(&.{.{ .tag = tag("wght"), .value = 650.0 }}));
1165 try std.testing.expectEqual(@as(i32, -260), face.descenderForVariations(&.{.{ .tag = tag("wght"), .value = 650.0 }}));
1166 try std.testing.expectEqual(@as(i32, 55), face.lineGapForVariations(&.{.{ .tag = tag("wght"), .value = 650.0 }}));
1167 try std.testing.expectEqual(@as(i32, 1150), face.heightUnitsForVariations(&.{.{ .tag = tag("wght"), .value = 650.0 }}));
1168 }
1169
1170 test "Face reads classic kern format 0 pairs" {
1171 const bytes = try test_font.createWithKern(std.testing.allocator);
1172 defer std.testing.allocator.free(bytes);
1173
1174 const face = try Face.init(bytes);
1175 const a = face.glyphId('A');
1176 const v = face.glyphId('V');
1177
1178 try std.testing.expectEqual(@as(i16, -80), face.kerning(a, v));
1179 try std.testing.expectEqual(@as(i16, 0), face.kerning(v, a));
1180 }
1181
1182 test "Face applies GSUB required single substitution" {
1183 const bytes = try test_font.createWithGsubSingleSubstitution(std.testing.allocator);
1184 defer std.testing.allocator.free(bytes);
1185
1186 const face = try Face.init(bytes);
1187 const a = face.glyphId('A');
1188 const z = face.glyphId('Z');
1189
1190 try std.testing.expectEqual(z, face.substituteGlyph(a));
1191 try std.testing.expectEqual(face.glyphId('B'), face.substituteGlyph(face.glyphId('B')));
1192 }
1193
1194 test "Face applies GSUB single substitutions with mark filters" {
1195 const bytes = try test_font.createWithGsubSingleIgnoreMarks(std.testing.allocator);
1196 defer std.testing.allocator.free(bytes);
1197
1198 const face = try Face.init(bytes);
1199 const a = face.glyphId('A');
1200 const mark = face.glyphId('^');
1201 const glyphs = [_]u32{ a, mark };
1202
1203 const substitution = (try face.substitutionAt(0, &glyphs, 0)).?;
1204 try std.testing.expectEqual(face.glyphId('Z'), substitution.glyph_id);
1205 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(0, &glyphs, 1));
1206 }
1207
1208 test "Face applies GSUB alternate substitutions" {
1209 const bytes = try test_font.createWithGsubAlternateSubstitution(std.testing.allocator);
1210 defer std.testing.allocator.free(bytes);
1211
1212 const face = try Face.init(bytes);
1213 const a = face.glyphId('A');
1214 const glyphs = [_]u32{a};
1215 const substitution = (try face.substitutionAt(0, &glyphs, 0)).?;
1216
1217 try std.testing.expectEqual(@as(u32, 96), face.substituteGlyph(a));
1218 try std.testing.expectEqual(@as(u32, 96), substitution.glyph_id);
1219 try std.testing.expectEqual(@as(u16, 1), substitution.component_count);
1220 try std.testing.expectEqual(face.glyphId('B'), face.substituteGlyph(face.glyphId('B')));
1221 }
1222
1223 test "Face applies GSUB contextual substitutions" {
1224 const bytes = try test_font.createWithGsubContextualSubstitution(std.testing.allocator);
1225 defer std.testing.allocator.free(bytes);
1226
1227 const face = try Face.init(bytes);
1228 const glyphs = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('C') };
1229 const mismatch = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('!') };
1230 const substitution = (try face.substitutionAt(0, &glyphs, 0)).?;
1231
1232 try std.testing.expectEqual(@as(u16, 2), try face.gsubLookupCount());
1233 try std.testing.expectEqual(@as(u32, face.glyphId('Z')), substitution.glyph_id);
1234 try std.testing.expectEqual(@as(u16, 1), substitution.component_count);
1235 try std.testing.expectEqual(@as(usize, 1), substitution.target_offset);
1236 try std.testing.expectEqual(@as(usize, 3), substitution.skip_count);
1237 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(0, &mismatch, 0));
1238 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(1, &glyphs, 1));
1239 }
1240
1241 test "Face applies GSUB class contextual substitutions" {
1242 const bytes = try test_font.createWithGsubClassContextualSubstitution(std.testing.allocator);
1243 defer std.testing.allocator.free(bytes);
1244
1245 const face = try Face.init(bytes);
1246 const glyphs = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('C') };
1247 const mismatch = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('!') };
1248 const substitution = (try face.substitutionAt(0, &glyphs, 0)).?;
1249
1250 try std.testing.expectEqual(@as(u16, 2), try face.gsubLookupCount());
1251 try std.testing.expectEqual(@as(u32, face.glyphId('Z')), substitution.glyph_id);
1252 try std.testing.expectEqual(@as(u16, 1), substitution.component_count);
1253 try std.testing.expectEqual(@as(usize, 1), substitution.target_offset);
1254 try std.testing.expectEqual(@as(usize, 3), substitution.skip_count);
1255 try std.testing.expectEqual(face.glyphId('B'), face.substituteGlyph(face.glyphId('B')));
1256 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(0, &mismatch, 0));
1257 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(1, &glyphs, 1));
1258 }
1259
1260 test "Face applies GSUB coverage contextual substitutions" {
1261 const bytes = try test_font.createWithGsubCoverageContextualSubstitution(std.testing.allocator);
1262 defer std.testing.allocator.free(bytes);
1263
1264 const face = try Face.init(bytes);
1265 const glyphs = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('C') };
1266 const mismatch = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('!') };
1267 const substitution = (try face.substitutionAt(0, &glyphs, 0)).?;
1268
1269 try std.testing.expectEqual(@as(u16, 2), try face.gsubLookupCount());
1270 try std.testing.expectEqual(@as(u32, face.glyphId('Z')), substitution.glyph_id);
1271 try std.testing.expectEqual(@as(u16, 1), substitution.component_count);
1272 try std.testing.expectEqual(@as(usize, 1), substitution.target_offset);
1273 try std.testing.expectEqual(@as(usize, 3), substitution.skip_count);
1274 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(0, &mismatch, 0));
1275 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(1, &glyphs, 1));
1276 }
1277
1278 test "Face applies GSUB chained contextual substitutions" {
1279 const bytes = try test_font.createWithGsubChainedContextualSubstitution(std.testing.allocator);
1280 defer std.testing.allocator.free(bytes);
1281
1282 const face = try Face.init(bytes);
1283 const glyphs = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('C') };
1284 const missing_backtrack = [_]u32{ face.glyphId('!'), face.glyphId('B'), face.glyphId('C') };
1285 const missing_lookahead = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('!') };
1286 const substitution = (try face.substitutionAt(0, &glyphs, 1)).?;
1287
1288 try std.testing.expectEqual(@as(u16, 2), try face.gsubLookupCount());
1289 try std.testing.expectEqual(@as(u32, face.glyphId('Z')), substitution.glyph_id);
1290 try std.testing.expectEqual(@as(u16, 1), substitution.component_count);
1291 try std.testing.expectEqual(@as(usize, 0), substitution.target_offset);
1292 try std.testing.expectEqual(@as(usize, 1), substitution.skip_count);
1293 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(0, &glyphs, 0));
1294 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(0, &missing_backtrack, 1));
1295 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(0, &missing_lookahead, 1));
1296 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(1, &glyphs, 1));
1297 }
1298
1299 test "Face applies GSUB chained simple contextual substitutions" {
1300 const bytes = try test_font.createWithGsubChainedSimpleContextualSubstitution(std.testing.allocator);
1301 defer std.testing.allocator.free(bytes);
1302
1303 const face = try Face.init(bytes);
1304 const glyphs = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('C') };
1305 const missing_backtrack = [_]u32{ face.glyphId('!'), face.glyphId('B'), face.glyphId('C') };
1306 const missing_lookahead = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('!') };
1307 const substitution = (try face.substitutionAt(0, &glyphs, 1)).?;
1308
1309 try std.testing.expectEqual(@as(u16, 2), try face.gsubLookupCount());
1310 try std.testing.expectEqual(@as(u32, face.glyphId('Z')), substitution.glyph_id);
1311 try std.testing.expectEqual(@as(u16, 1), substitution.component_count);
1312 try std.testing.expectEqual(@as(usize, 0), substitution.target_offset);
1313 try std.testing.expectEqual(@as(usize, 1), substitution.skip_count);
1314 try std.testing.expectEqual(face.glyphId('B'), face.substituteGlyph(face.glyphId('B')));
1315 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(0, &glyphs, 0));
1316 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(0, &missing_backtrack, 1));
1317 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(0, &missing_lookahead, 1));
1318 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(1, &glyphs, 1));
1319 }
1320
1321 test "Face applies GSUB chained class contextual substitutions" {
1322 const bytes = try test_font.createWithGsubChainedClassContextualSubstitution(std.testing.allocator);
1323 defer std.testing.allocator.free(bytes);
1324
1325 const face = try Face.init(bytes);
1326 const glyphs = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('C') };
1327 const missing_backtrack = [_]u32{ face.glyphId('!'), face.glyphId('B'), face.glyphId('C') };
1328 const missing_lookahead = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('!') };
1329 const substitution = (try face.substitutionAt(0, &glyphs, 1)).?;
1330
1331 try std.testing.expectEqual(@as(u16, 2), try face.gsubLookupCount());
1332 try std.testing.expectEqual(@as(u32, face.glyphId('Z')), substitution.glyph_id);
1333 try std.testing.expectEqual(@as(u16, 1), substitution.component_count);
1334 try std.testing.expectEqual(@as(usize, 0), substitution.target_offset);
1335 try std.testing.expectEqual(@as(usize, 1), substitution.skip_count);
1336 try std.testing.expectEqual(face.glyphId('B'), face.substituteGlyph(face.glyphId('B')));
1337 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(0, &glyphs, 0));
1338 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(0, &missing_backtrack, 1));
1339 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(0, &missing_lookahead, 1));
1340 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(1, &glyphs, 1));
1341 }
1342
1343 test "Face exposes GSUB default ligature substitutions" {
1344 const bytes = try test_font.createWithGsubLigature(std.testing.allocator);
1345 defer std.testing.allocator.free(bytes);
1346
1347 const face = try Face.init(bytes);
1348 const glyphs = [_]u32{ face.glyphId('f'), face.glyphId('i') };
1349 const substitution = (try face.substitutionAt(0, &glyphs, 0)).?;
1350
1351 try std.testing.expectEqual(@as(u16, 1), try face.gsubLookupCount());
1352 try std.testing.expectEqual(@as(u32, 96), substitution.glyph_id);
1353 try std.testing.expectEqual(@as(u16, 2), substitution.component_count);
1354 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(0, &glyphs, 1));
1355 }
1356
1357 test "Face applies GSUB localized forms by default" {
1358 const bytes = try test_font.createWithGsubLocalizedForms(std.testing.allocator);
1359 defer std.testing.allocator.free(bytes);
1360
1361 const face = try Face.init(bytes);
1362 const glyphs = [_]u32{face.glyphId('A')};
1363 const disabled = LayoutSelection{ .features = &.{.{ .tag = tag("locl"), .value = 0 }} };
1364
1365 try std.testing.expectEqual(face.glyphId('Z'), (try face.substitutionAtFor(.{}, 0, &glyphs, 0)).?.glyph_id);
1366 try std.testing.expectEqual(face.glyphId('Z'), (try face.substitutionAtFor(.{ .vertical = true }, 0, &glyphs, 0)).?.glyph_id);
1367 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAtFor(disabled, 0, &glyphs, 0));
1368 }
1369
1370 test "Face applies GSUB contextual ligatures as a horizontal default" {
1371 const bytes = try test_font.createWithGsubContextualLigature(std.testing.allocator);
1372 defer std.testing.allocator.free(bytes);
1373
1374 const face = try Face.init(bytes);
1375 const glyphs = [_]u32{ face.glyphId('f'), face.glyphId('i') };
1376 const disabled = LayoutSelection{ .features = &.{.{ .tag = tag("clig"), .value = 0 }} };
1377 const vertical_enabled = LayoutSelection{ .vertical = true, .features = &.{.{ .tag = tag("clig"), .value = 1 }} };
1378
1379 const substitution = (try face.substitutionAtFor(.{}, 0, &glyphs, 0)).?;
1380 try std.testing.expectEqual(@as(u32, 96), substitution.glyph_id);
1381 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAtFor(.{ .vertical = true }, 0, &glyphs, 0));
1382 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAtFor(disabled, 0, &glyphs, 0));
1383 try std.testing.expectEqual(@as(u32, 96), (try face.substitutionAtFor(vertical_enabled, 0, &glyphs, 0)).?.glyph_id);
1384 }
1385
1386 test "Face applies GSUB contextual alternates as horizontal defaults" {
1387 const calt_bytes = try test_font.createWithGsubContextualAlternates(std.testing.allocator);
1388 defer std.testing.allocator.free(calt_bytes);
1389 const rclt_bytes = try test_font.createWithGsubRequiredContextualAlternates(std.testing.allocator);
1390 defer std.testing.allocator.free(rclt_bytes);
1391
1392 const calt_face = try Face.init(calt_bytes);
1393 const rclt_face = try Face.init(rclt_bytes);
1394 const calt_glyphs = [_]u32{ calt_face.glyphId('A'), calt_face.glyphId('B'), calt_face.glyphId('C') };
1395 const rclt_glyphs = [_]u32{ rclt_face.glyphId('A'), rclt_face.glyphId('B'), rclt_face.glyphId('C') };
1396 const disabled_calt = LayoutSelection{ .features = &.{.{ .tag = tag("calt"), .value = 0 }} };
1397 const disabled_rclt = LayoutSelection{ .features = &.{.{ .tag = tag("rclt"), .value = 0 }} };
1398
1399 try std.testing.expectEqual(calt_face.glyphId('Z'), (try calt_face.substitutionAtFor(.{}, 0, &calt_glyphs, 0)).?.glyph_id);
1400 try std.testing.expectEqual(rclt_face.glyphId('Z'), (try rclt_face.substitutionAtFor(.{}, 0, &rclt_glyphs, 0)).?.glyph_id);
1401 try std.testing.expectEqual(@as(?GsubSubstitution, null), try calt_face.substitutionAtFor(.{ .vertical = true }, 0, &calt_glyphs, 0));
1402 try std.testing.expectEqual(@as(?GsubSubstitution, null), try rclt_face.substitutionAtFor(.{ .vertical = true }, 0, &rclt_glyphs, 0));
1403 try std.testing.expectEqual(@as(?GsubSubstitution, null), try calt_face.substitutionAtFor(disabled_calt, 0, &calt_glyphs, 0));
1404 try std.testing.expectEqual(rclt_face.glyphId('Z'), (try rclt_face.substitutionAtFor(disabled_rclt, 0, &rclt_glyphs, 0)).?.glyph_id);
1405 }
1406
1407 test "Face applies GSUB direction alternates by resolved direction" {
1408 const ltra_bytes = try test_font.createWithGsubLeftToRightAlternates(std.testing.allocator);
1409 defer std.testing.allocator.free(ltra_bytes);
1410 const ltrm_bytes = try test_font.createWithGsubLeftToRightMirroredForms(std.testing.allocator);
1411 defer std.testing.allocator.free(ltrm_bytes);
1412 const rtla_bytes = try test_font.createWithGsubRightToLeftAlternates(std.testing.allocator);
1413 defer std.testing.allocator.free(rtla_bytes);
1414 const rtlm_bytes = try test_font.createWithGsubRightToLeftMirroredForms(std.testing.allocator);
1415 defer std.testing.allocator.free(rtlm_bytes);
1416
1417 const ltra_face = try Face.init(ltra_bytes);
1418 const ltrm_face = try Face.init(ltrm_bytes);
1419 const rtla_face = try Face.init(rtla_bytes);
1420 const rtlm_face = try Face.init(rtlm_bytes);
1421 const ltra_glyphs = [_]u32{ltra_face.glyphId('A')};
1422 const ltrm_glyphs = [_]u32{ltrm_face.glyphId('A')};
1423 const rtla_glyphs = [_]u32{rtla_face.glyphId('A')};
1424 const rtlm_glyphs = [_]u32{rtlm_face.glyphId('A')};
1425 const rtl_selection = LayoutSelection{ .right_to_left = true };
1426 const disabled_ltra = LayoutSelection{ .features = &.{.{ .tag = tag("ltra"), .value = 0 }} };
1427 const enabled_ltra_rtl = LayoutSelection{ .right_to_left = true, .features = &.{.{ .tag = tag("ltra"), .value = 1 }} };
1428 const disabled_rtla_rtl = LayoutSelection{ .right_to_left = true, .features = &.{.{ .tag = tag("rtla"), .value = 0 }} };
1429
1430 try std.testing.expectEqual(ltra_face.glyphId('Z'), (try ltra_face.substitutionAtFor(.{}, 0, <ra_glyphs, 0)).?.glyph_id);
1431 try std.testing.expectEqual(ltrm_face.glyphId('Z'), (try ltrm_face.substitutionAtFor(.{}, 0, <rm_glyphs, 0)).?.glyph_id);
1432 try std.testing.expectEqual(@as(?GsubSubstitution, null), try ltra_face.substitutionAtFor(rtl_selection, 0, <ra_glyphs, 0));
1433 try std.testing.expectEqual(@as(?GsubSubstitution, null), try ltrm_face.substitutionAtFor(rtl_selection, 0, <rm_glyphs, 0));
1434 try std.testing.expectEqual(@as(?GsubSubstitution, null), try ltra_face.substitutionAtFor(disabled_ltra, 0, <ra_glyphs, 0));
1435 try std.testing.expectEqual(ltra_face.glyphId('Z'), (try ltra_face.substitutionAtFor(enabled_ltra_rtl, 0, <ra_glyphs, 0)).?.glyph_id);
1436
1437 try std.testing.expectEqual(@as(?GsubSubstitution, null), try rtla_face.substitutionAtFor(.{}, 0, &rtla_glyphs, 0));
1438 try std.testing.expectEqual(@as(?GsubSubstitution, null), try rtlm_face.substitutionAtFor(.{}, 0, &rtlm_glyphs, 0));
1439 try std.testing.expectEqual(rtla_face.glyphId('Z'), (try rtla_face.substitutionAtFor(rtl_selection, 0, &rtla_glyphs, 0)).?.glyph_id);
1440 try std.testing.expectEqual(rtlm_face.glyphId('Z'), (try rtlm_face.substitutionAtFor(rtl_selection, 0, &rtlm_glyphs, 0)).?.glyph_id);
1441 try std.testing.expectEqual(@as(?GsubSubstitution, null), try rtla_face.substitutionAtFor(disabled_rtla_rtl, 0, &rtla_glyphs, 0));
1442 }
1443
1444 test "Face detects automatic fraction feature availability" {
1445 const frac_bytes = try test_font.createWithGsubFractions(std.testing.allocator);
1446 defer std.testing.allocator.free(frac_bytes);
1447 const component_bytes = try test_font.createWithGsubFractionComponents(std.testing.allocator);
1448 defer std.testing.allocator.free(component_bytes);
1449 const ordinary_bytes = try test_font.create(std.testing.allocator);
1450 defer std.testing.allocator.free(ordinary_bytes);
1451
1452 const frac_face = try Face.init(frac_bytes);
1453 const component_face = try Face.init(component_bytes);
1454 const ordinary_face = try Face.init(ordinary_bytes);
1455
1456 try std.testing.expect(frac_face.usesAutomaticFractions(.{}));
1457 try std.testing.expect(component_face.usesAutomaticFractions(.{}));
1458 try std.testing.expect(!ordinary_face.usesAutomaticFractions(.{}));
1459 }
1460
1461 test "Face detects Arabic joining form feature availability" {
1462 const joining_bytes = try test_font.createWithGsubArabicJoiningForms(std.testing.allocator);
1463 defer std.testing.allocator.free(joining_bytes);
1464 const ordinary_bytes = try test_font.create(std.testing.allocator);
1465 defer std.testing.allocator.free(ordinary_bytes);
1466
1467 const joining_face = try Face.init(joining_bytes);
1468 const ordinary_face = try Face.init(ordinary_bytes);
1469
1470 try std.testing.expect(joining_face.usesArabicJoiningForms(.{}));
1471 try std.testing.expect(!ordinary_face.usesArabicJoiningForms(.{}));
1472 try std.testing.expectEqual(@as(u32, 'A' - 31), joining_face.glyphId(0x0628));
1473 try std.testing.expectEqual(@as(u32, 'B' - 31), joining_face.glyphId(0x0627));
1474 }
1475
1476 test "Face applies GSUB feature variations" {
1477 const bytes = try test_font.createWithGsubFeatureVariations(std.testing.allocator);
1478 defer std.testing.allocator.free(bytes);
1479
1480 const face = try Face.init(bytes);
1481 const glyphs = [_]u32{face.glyphId('A')};
1482 const heavy = [_]VariationSetting{.{ .tag = tag("wght"), .value = 900.0 }};
1483
1484 const default_substitution = (try face.substitutionAtFor(.{}, 0, &glyphs, 0)).?;
1485 try std.testing.expectEqual(face.glyphId('Z'), default_substitution.glyph_id);
1486 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAtFor(.{}, 1, &glyphs, 0));
1487
1488 const heavy_selection = LayoutSelection{ .variations = &heavy };
1489 const heavy_substitution = (try face.substitutionAtFor(heavy_selection, 1, &glyphs, 0)).?;
1490 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAtFor(heavy_selection, 0, &glyphs, 0));
1491 try std.testing.expectEqual(face.glyphId('B'), heavy_substitution.glyph_id);
1492 try std.testing.expectEqual(face.glyphId('B'), face.substituteGlyphFor(heavy_selection, face.glyphId('A')));
1493 }
1494
1495 test "Face applies GSUB required variation alternates" {
1496 const bytes = try test_font.createWithGsubRequiredVariationAlternates(std.testing.allocator);
1497 defer std.testing.allocator.free(bytes);
1498
1499 const face = try Face.init(bytes);
1500 const glyphs = [_]u32{face.glyphId('A')};
1501 const ligature_glyphs = [_]u32{ face.glyphId('f'), face.glyphId('i') };
1502 const heavy = [_]VariationSetting{.{ .tag = tag("wght"), .value = 900.0 }};
1503 const heavy_selection = LayoutSelection{ .variations = &heavy };
1504 const disabled_selection = LayoutSelection{ .features = &.{.{ .tag = tag("rvrn"), .value = 0 }} };
1505 const disabled_heavy_selection = LayoutSelection{ .features = disabled_selection.features, .variations = &heavy };
1506
1507 const default_substitution = (try face.substitutionAtFor(.{}, 0, &glyphs, 0)).?;
1508 try std.testing.expectEqual(@as(u16, 3), try face.gsubLookupCount());
1509 try std.testing.expectEqual(face.glyphId('Z'), default_substitution.glyph_id);
1510 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAtFor(.{}, 1, &glyphs, 0));
1511 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAtFor(.{}, 2, &ligature_glyphs, 0));
1512 try std.testing.expectEqual(face.glyphId('Z'), face.substituteGlyphFor(.{}, face.glyphId('A')));
1513
1514 const heavy_substitution = (try face.substitutionAtFor(heavy_selection, 1, &glyphs, 0)).?;
1515 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAtFor(heavy_selection, 0, &glyphs, 0));
1516 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAtFor(heavy_selection, 2, &ligature_glyphs, 0));
1517 try std.testing.expectEqual(face.glyphId('B'), heavy_substitution.glyph_id);
1518 try std.testing.expectEqual(face.glyphId('B'), face.substituteGlyphFor(heavy_selection, face.glyphId('A')));
1519
1520 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAtFor(disabled_selection, 0, &glyphs, 0));
1521 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAtFor(disabled_heavy_selection, 1, &glyphs, 0));
1522 try std.testing.expectEqual(face.glyphId('A'), face.substituteGlyphFor(disabled_selection, face.glyphId('A')));
1523 }
1524
1525 test "Face applies GSUB vertical alternates only for vertical selection" {
1526 const bytes = try test_font.createWithGsubVerticalAlternates(std.testing.allocator);
1527 defer std.testing.allocator.free(bytes);
1528
1529 const face = try Face.init(bytes);
1530 const glyphs = [_]u32{face.glyphId('A')};
1531
1532 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAtFor(.{}, 0, &glyphs, 0));
1533
1534 const vertical_substitution = (try face.substitutionAtFor(.{ .vertical = true }, 0, &glyphs, 0)).?;
1535 try std.testing.expectEqual(face.glyphId('B'), vertical_substitution.glyph_id);
1536
1537 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAtFor(.{
1538 .vertical = true,
1539 .features = &.{.{ .tag = tag("vert"), .value = 0 }},
1540 }, 0, &glyphs, 0));
1541 }
1542
1543 test "Face applies GSUB vertical rotation alternates only for vertical selection" {
1544 const bytes = try test_font.createWithGsubVerticalRotationAlternates(std.testing.allocator);
1545 defer std.testing.allocator.free(bytes);
1546
1547 const face = try Face.init(bytes);
1548 const glyphs = [_]u32{face.glyphId('A')};
1549
1550 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAtFor(.{}, 0, &glyphs, 0));
1551
1552 const vertical_substitution = (try face.substitutionAtFor(.{ .vertical = true }, 0, &glyphs, 0)).?;
1553 try std.testing.expectEqual(face.glyphId('C'), vertical_substitution.glyph_id);
1554
1555 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAtFor(.{
1556 .vertical = true,
1557 .features = &.{.{ .tag = tag("vrt2"), .value = 0 }},
1558 }, 0, &glyphs, 0));
1559 }
1560
1561 test "Face activates GSUB substitutions for selected script tags" {
1562 const bytes = try test_font.createWithGsubLigature(std.testing.allocator);
1563 defer std.testing.allocator.free(bytes);
1564 try fixture_binary.replaceLayoutScript(bytes, "GSUB", "latn");
1565
1566 const face = try Face.init(bytes);
1567 const glyphs = [_]u32{ face.glyphId('f'), face.glyphId('i') };
1568
1569 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(0, &glyphs, 0));
1570
1571 const substitution = (try face.substitutionAtFor(.{ .script = tag("latn") }, 0, &glyphs, 0)).?;
1572 try std.testing.expectEqual(@as(u32, 96), substitution.glyph_id);
1573 try std.testing.expectEqual(@as(u16, 2), substitution.component_count);
1574
1575 const fallback = try face.substitutionAtFor(.{ .script = tag("cyrl") }, 0, &glyphs, 0);
1576 try std.testing.expectEqual(@as(?GsubSubstitution, null), fallback);
1577 }
1578
1579 test "Face resolves GSUB substitutions from ordered script tags" {
1580 const bytes = try test_font.createWithGsubLigature(std.testing.allocator);
1581 defer std.testing.allocator.free(bytes);
1582 try fixture_binary.replaceLayoutScript(bytes, "GSUB", "dev2");
1583
1584 const face = try Face.init(bytes);
1585 const glyphs = [_]u32{ face.glyphId('f'), face.glyphId('i') };
1586 const script_tags = [_]Tag{ tag("dev3"), tag("dev2"), tag("deva") };
1587
1588 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAtFor(.{ .script = tag("deva") }, 0, &glyphs, 0));
1589
1590 const substitution = (try face.substitutionAtFor(.{ .script = tag("deva"), .script_tags = &script_tags }, 0, &glyphs, 0)).?;
1591 try std.testing.expectEqual(@as(u32, 96), substitution.glyph_id);
1592 try std.testing.expectEqual(@as(u16, 2), substitution.component_count);
1593 }
1594
1595 test "Face exposes GSUB default multiple substitutions" {
1596 const bytes = try test_font.createWithGsubMultipleSubstitution(std.testing.allocator);
1597 defer std.testing.allocator.free(bytes);
1598
1599 const face = try Face.init(bytes);
1600 const a = face.glyphId('A');
1601 const glyphs = [_]u32{ a, face.glyphId('!') };
1602 const substitution = (try face.substitutionAt(0, &glyphs, 0)).?;
1603
1604 try std.testing.expectEqual(@as(u16, 1), try face.gsubLookupCount());
1605 try std.testing.expectEqual(face.glyphId('X'), substitution.glyph_id);
1606 try std.testing.expectEqual(@as(u16, 1), substitution.component_count);
1607 try std.testing.expectEqual(@as(u16, 2), substitution.replacement_count);
1608 try std.testing.expectEqual(@as(?u32, face.glyphId('X')), face.substitutionGlyph(substitution, 0));
1609 try std.testing.expectEqual(@as(?u32, face.glyphId('Y')), face.substitutionGlyph(substitution, 1));
1610 try std.testing.expectEqual(@as(?u32, null), face.substitutionGlyph(substitution, 2));
1611 try std.testing.expectEqual(a, face.substituteGlyph(a));
1612 try std.testing.expectEqual(@as(?GsubSubstitution, null), try face.substitutionAt(0, &glyphs, 1));
1613 }
1614
1615 test "Face applies GPOS pair adjustments" {
1616 const bytes = try test_font.createWithGposPairAdjustment(std.testing.allocator);
1617 defer std.testing.allocator.free(bytes);
1618
1619 const face = try Face.init(bytes);
1620 const a = face.glyphId('A');
1621 const v = face.glyphId('V');
1622
1623 const adjustment = face.gposPairAdjustment(a, v);
1624 try std.testing.expectEqual(@as(i32, -80), adjustment.left.x_advance);
1625 try std.testing.expectEqual(@as(i32, 0), adjustment.right.x_advance);
1626 try std.testing.expect(face.gposPairAdjustment(v, a).isZero());
1627 try std.testing.expect(!face.hasGposSingleAdjustment());
1628 try std.testing.expect(!face.hasGposCursiveAttachment());
1629 try std.testing.expect(!face.hasGposMarkToBase());
1630 try std.testing.expect(!face.hasGposMarkToLigature());
1631 try std.testing.expect(!face.hasGposMarkToMark());
1632 }
1633
1634 test "Face applies GPOS vertical pair adjustments only for vertical selection" {
1635 const bytes = try test_font.createWithGposVerticalPairAdjustment(std.testing.allocator);
1636 defer std.testing.allocator.free(bytes);
1637
1638 const face = try Face.init(bytes);
1639 const a = face.glyphId('A');
1640 const v = face.glyphId('V');
1641
1642 try std.testing.expect(face.gposPairAdjustmentFor(.{}, a, v).isZero());
1643
1644 const vertical = face.gposPairAdjustmentFor(.{ .vertical = true }, a, v);
1645 try std.testing.expectEqual(@as(i32, 0), vertical.left.x_advance);
1646 try std.testing.expectEqual(@as(i32, -80), vertical.left.y_advance);
1647 try std.testing.expectEqual(@as(i32, 0), vertical.right.y_advance);
1648
1649 const disabled = face.gposPairAdjustmentFor(.{
1650 .vertical = true,
1651 .features = &.{.{ .tag = tag("vkrn"), .value = 0 }},
1652 }, a, v);
1653 try std.testing.expect(disabled.isZero());
1654 }
1655
1656 test "Face applies GPOS pair adjustments across ignored marks" {
1657 const bytes = try test_font.createWithGposPairIgnoreMarks(std.testing.allocator);
1658 defer std.testing.allocator.free(bytes);
1659
1660 const face = try Face.init(bytes);
1661 const glyphs = [_]u32{
1662 face.glyphId('A'),
1663 face.glyphId('^'),
1664 face.glyphId('V'),
1665 };
1666
1667 const adjustment = face.gposPairAdjustmentBeforeFor(.{}, &glyphs, 2).?;
1668 try std.testing.expectEqual(@as(usize, 2), adjustment.left_offset);
1669 try std.testing.expectEqual(@as(i32, -80), adjustment.adjustment.left.x_advance);
1670 try std.testing.expectEqual(@as(i32, 0), adjustment.adjustment.right.x_advance);
1671 try std.testing.expectEqual(@as(?GposPairAdjustmentBefore, null), face.gposPairAdjustmentBeforeFor(.{}, &glyphs, 1));
1672 }
1673
1674 test "Face keeps unfiltered GPOS pair adjustments adjacent" {
1675 const bytes = try test_font.createWithGposPairAdjustment(std.testing.allocator);
1676 defer std.testing.allocator.free(bytes);
1677
1678 const face = try Face.init(bytes);
1679 const glyphs = [_]u32{
1680 face.glyphId('A'),
1681 face.glyphId('^'),
1682 face.glyphId('V'),
1683 };
1684
1685 try std.testing.expectEqual(@as(?GposPairAdjustmentBefore, null), face.gposPairAdjustmentBeforeFor(.{}, &glyphs, 2));
1686 }
1687
1688 test "Face applies GPOS contextual pair adjustments" {
1689 const bytes = try test_font.createWithGposContextualPairAdjustment(std.testing.allocator);
1690 defer std.testing.allocator.free(bytes);
1691
1692 const face = try Face.init(bytes);
1693 const glyphs = [_]u32{ face.glyphId('A'), face.glyphId('V'), face.glyphId('C') };
1694 const mismatch = [_]u32{ face.glyphId('A'), face.glyphId('V'), face.glyphId('!') };
1695 const adjustment = face.gposContextualPairAdjustmentAtFor(.{}, &glyphs, 0).?;
1696
1697 try std.testing.expect(face.gposPairAdjustment(face.glyphId('A'), face.glyphId('V')).isZero());
1698 try std.testing.expectEqual(@as(usize, 0), adjustment.target_offset);
1699 try std.testing.expectEqual(@as(usize, 1), adjustment.skip_count);
1700 try std.testing.expectEqual(@as(i32, -100), adjustment.adjustment.left.x_advance);
1701 try std.testing.expectEqual(@as(i32, 0), adjustment.adjustment.right.x_advance);
1702 try std.testing.expectEqual(@as(?GposContextualPairAdjustment, null), face.gposContextualPairAdjustmentAtFor(.{}, &glyphs, 1));
1703 try std.testing.expectEqual(@as(?GposContextualPairAdjustment, null), face.gposContextualPairAdjustmentAtFor(.{}, &mismatch, 0));
1704 }
1705
1706 test "Face applies GPOS simple contextual pair adjustments" {
1707 const bytes = try test_font.createWithGposSimpleContextualPairAdjustment(std.testing.allocator);
1708 defer std.testing.allocator.free(bytes);
1709
1710 const face = try Face.init(bytes);
1711 const glyphs = [_]u32{ face.glyphId('A'), face.glyphId('V'), face.glyphId('C') };
1712 const mismatch = [_]u32{ face.glyphId('A'), face.glyphId('V'), face.glyphId('!') };
1713 const adjustment = face.gposContextualPairAdjustmentAtFor(.{}, &glyphs, 0).?;
1714
1715 try std.testing.expect(face.gposPairAdjustment(face.glyphId('A'), face.glyphId('V')).isZero());
1716 try std.testing.expectEqual(@as(usize, 0), adjustment.target_offset);
1717 try std.testing.expectEqual(@as(usize, 1), adjustment.skip_count);
1718 try std.testing.expectEqual(@as(i32, -100), adjustment.adjustment.left.x_advance);
1719 try std.testing.expectEqual(@as(i32, 0), adjustment.adjustment.right.x_advance);
1720 try std.testing.expectEqual(@as(?GposContextualPairAdjustment, null), face.gposContextualPairAdjustmentAtFor(.{}, &glyphs, 1));
1721 try std.testing.expectEqual(@as(?GposContextualPairAdjustment, null), face.gposContextualPairAdjustmentAtFor(.{}, &mismatch, 0));
1722 }
1723
1724 test "Face applies GPOS class contextual pair adjustments" {
1725 const bytes = try test_font.createWithGposClassContextualPairAdjustment(std.testing.allocator);
1726 defer std.testing.allocator.free(bytes);
1727
1728 const face = try Face.init(bytes);
1729 const glyphs = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('C') };
1730 const mismatch = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('!') };
1731 const adjustment = face.gposContextualPairAdjustmentAtFor(.{}, &glyphs, 0).?;
1732
1733 try std.testing.expect(face.gposPairAdjustment(face.glyphId('A'), face.glyphId('B')).isZero());
1734 try std.testing.expectEqual(@as(usize, 0), adjustment.target_offset);
1735 try std.testing.expectEqual(@as(usize, 1), adjustment.skip_count);
1736 try std.testing.expectEqual(@as(i32, -100), adjustment.adjustment.left.x_advance);
1737 try std.testing.expectEqual(@as(i32, 0), adjustment.adjustment.right.x_advance);
1738 try std.testing.expectEqual(@as(?GposContextualPairAdjustment, null), face.gposContextualPairAdjustmentAtFor(.{}, &glyphs, 1));
1739 try std.testing.expectEqual(@as(?GposContextualPairAdjustment, null), face.gposContextualPairAdjustmentAtFor(.{}, &mismatch, 0));
1740 }
1741
1742 test "Face applies GPOS chained contextual pair adjustments" {
1743 const bytes = try test_font.createWithGposChainedContextualPairAdjustment(std.testing.allocator);
1744 defer std.testing.allocator.free(bytes);
1745
1746 const face = try Face.init(bytes);
1747 const glyphs = [_]u32{ face.glyphId('A'), face.glyphId('V'), face.glyphId('C'), face.glyphId('!') };
1748 const lookahead_mismatch = [_]u32{ face.glyphId('A'), face.glyphId('V'), face.glyphId('C'), face.glyphId('A') };
1749 const adjustment = face.gposContextualPairAdjustmentAtFor(.{}, &glyphs, 1).?;
1750
1751 try std.testing.expect(face.gposPairAdjustment(face.glyphId('V'), face.glyphId('C')).isZero());
1752 try std.testing.expectEqual(@as(usize, 0), adjustment.target_offset);
1753 try std.testing.expectEqual(@as(usize, 1), adjustment.skip_count);
1754 try std.testing.expectEqual(@as(i32, -100), adjustment.adjustment.left.x_advance);
1755 try std.testing.expectEqual(@as(i32, 0), adjustment.adjustment.right.x_advance);
1756 try std.testing.expectEqual(@as(?GposContextualPairAdjustment, null), face.gposContextualPairAdjustmentAtFor(.{}, &glyphs, 0));
1757 try std.testing.expectEqual(@as(?GposContextualPairAdjustment, null), face.gposContextualPairAdjustmentAtFor(.{}, &lookahead_mismatch, 1));
1758 }
1759
1760 test "Face applies GPOS chained simple contextual pair adjustments" {
1761 const bytes = try test_font.createWithGposChainedSimpleContextualPairAdjustment(std.testing.allocator);
1762 defer std.testing.allocator.free(bytes);
1763
1764 const face = try Face.init(bytes);
1765 const glyphs = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('C') };
1766 const missing_backtrack = [_]u32{ face.glyphId('B'), face.glyphId('C'), face.glyphId('!') };
1767 const missing_input = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('!') };
1768 const adjustment = face.gposContextualPairAdjustmentAtFor(.{}, &glyphs, 1).?;
1769
1770 try std.testing.expect(face.gposPairAdjustment(face.glyphId('B'), face.glyphId('C')).isZero());
1771 try std.testing.expectEqual(@as(usize, 0), adjustment.target_offset);
1772 try std.testing.expectEqual(@as(usize, 1), adjustment.skip_count);
1773 try std.testing.expectEqual(@as(i32, -100), adjustment.adjustment.left.x_advance);
1774 try std.testing.expectEqual(@as(i32, 0), adjustment.adjustment.right.x_advance);
1775 try std.testing.expectEqual(@as(?GposContextualPairAdjustment, null), face.gposContextualPairAdjustmentAtFor(.{}, &glyphs, 0));
1776 try std.testing.expectEqual(@as(?GposContextualPairAdjustment, null), face.gposContextualPairAdjustmentAtFor(.{}, &missing_backtrack, 0));
1777 try std.testing.expectEqual(@as(?GposContextualPairAdjustment, null), face.gposContextualPairAdjustmentAtFor(.{}, &missing_input, 1));
1778 }
1779
1780 test "Face applies GPOS chained class contextual pair adjustments" {
1781 const bytes = try test_font.createWithGposChainedClassContextualPairAdjustment(std.testing.allocator);
1782 defer std.testing.allocator.free(bytes);
1783
1784 const face = try Face.init(bytes);
1785 const glyphs = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('C') };
1786 const missing_backtrack = [_]u32{ face.glyphId('B'), face.glyphId('C'), face.glyphId('!') };
1787 const missing_input = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('!') };
1788 const adjustment = face.gposContextualPairAdjustmentAtFor(.{}, &glyphs, 1).?;
1789
1790 try std.testing.expect(face.gposPairAdjustment(face.glyphId('B'), face.glyphId('C')).isZero());
1791 try std.testing.expectEqual(@as(usize, 0), adjustment.target_offset);
1792 try std.testing.expectEqual(@as(usize, 1), adjustment.skip_count);
1793 try std.testing.expectEqual(@as(i32, -100), adjustment.adjustment.left.x_advance);
1794 try std.testing.expectEqual(@as(i32, 0), adjustment.adjustment.right.x_advance);
1795 try std.testing.expectEqual(@as(?GposContextualPairAdjustment, null), face.gposContextualPairAdjustmentAtFor(.{}, &glyphs, 0));
1796 try std.testing.expectEqual(@as(?GposContextualPairAdjustment, null), face.gposContextualPairAdjustmentAtFor(.{}, &missing_backtrack, 0));
1797 try std.testing.expectEqual(@as(?GposContextualPairAdjustment, null), face.gposContextualPairAdjustmentAtFor(.{}, &missing_input, 1));
1798 }
1799
1800 test "Face activates GPOS adjustments for selected script tags" {
1801 const bytes = try test_font.createWithGposPairAdjustment(std.testing.allocator);
1802 defer std.testing.allocator.free(bytes);
1803 try fixture_binary.replaceLayoutScript(bytes, "GPOS", "latn");
1804
1805 const face = try Face.init(bytes);
1806 const a = face.glyphId('A');
1807 const v = face.glyphId('V');
1808
1809 try std.testing.expect(face.gposPairAdjustment(a, v).isZero());
1810
1811 const adjustment = face.gposPairAdjustmentFor(.{ .script = tag("latn") }, a, v);
1812 try std.testing.expectEqual(@as(i32, -80), adjustment.left.x_advance);
1813 try std.testing.expectEqual(@as(i32, 0), adjustment.right.x_advance);
1814 try std.testing.expect(face.gposPairAdjustmentFor(.{ .script = tag("cyrl") }, a, v).isZero());
1815 }
1816
1817 test "Face resolves GPOS adjustments from ordered script tags" {
1818 const bytes = try test_font.createWithGposPairAdjustment(std.testing.allocator);
1819 defer std.testing.allocator.free(bytes);
1820 try fixture_binary.replaceLayoutScript(bytes, "GPOS", "bng2");
1821
1822 const face = try Face.init(bytes);
1823 const a = face.glyphId('A');
1824 const v = face.glyphId('V');
1825 const script_tags = [_]Tag{ tag("bng3"), tag("bng2"), tag("beng") };
1826
1827 try std.testing.expect(face.gposPairAdjustmentFor(.{ .script = tag("beng") }, a, v).isZero());
1828
1829 const adjustment = face.gposPairAdjustmentFor(.{ .script = tag("beng"), .script_tags = &script_tags }, a, v);
1830 try std.testing.expectEqual(@as(i32, -80), adjustment.left.x_advance);
1831 try std.testing.expectEqual(@as(i32, 0), adjustment.right.x_advance);
1832 }
1833
1834 test "Face applies GPOS single adjustments" {
1835 const bytes = try test_font.createWithGposSingleAdjustment(std.testing.allocator);
1836 defer std.testing.allocator.free(bytes);
1837
1838 const face = try Face.init(bytes);
1839 const a = face.glyphId('A');
1840 const b = face.glyphId('B');
1841 const bang = face.glyphId('!');
1842
1843 try std.testing.expect(face.hasGposSingleAdjustment());
1844 const a_adjustment = face.gposSingleAdjustment(a);
1845 try std.testing.expectEqual(@as(i32, 20), a_adjustment.x_placement);
1846 try std.testing.expectEqual(@as(i32, 30), a_adjustment.y_placement);
1847 try std.testing.expectEqual(@as(i32, -40), a_adjustment.x_advance);
1848 const b_adjustment = face.gposSingleAdjustment(b);
1849 try std.testing.expectEqual(@as(i32, -10), b_adjustment.x_placement);
1850 try std.testing.expectEqual(@as(i32, 0), b_adjustment.y_placement);
1851 try std.testing.expectEqual(@as(i32, 0), b_adjustment.x_advance);
1852 try std.testing.expect(face.gposSingleAdjustment(bang).isZero());
1853
1854 const vertical = LayoutSelection{ .vertical = true };
1855 const vertical_enabled = LayoutSelection{ .vertical = true, .features = &.{.{ .tag = tag("dist"), .value = 1 }} };
1856 const glyphs = [_]u32{a};
1857
1858 try std.testing.expect(!face.hasGposSingleAdjustmentFor(vertical));
1859 try std.testing.expect(face.hasGposSingleAdjustmentFor(vertical_enabled));
1860 try std.testing.expectEqual(@as(?GposSingleAdjustment, null), face.gposSingleAdjustmentAtFor(vertical, &glyphs, 0));
1861 try std.testing.expectEqual(@as(i32, 20), face.gposSingleAdjustmentAtFor(vertical_enabled, &glyphs, 0).?.adjustment.x_placement);
1862 }
1863
1864 test "Face applies GPOS feature variations" {
1865 const bytes = try test_font.createWithGposFeatureVariations(std.testing.allocator);
1866 defer std.testing.allocator.free(bytes);
1867
1868 const face = try Face.init(bytes);
1869 const glyph = face.glyphId('A');
1870 const heavy = [_]VariationSetting{.{ .tag = tag("wght"), .value = 900.0 }};
1871 const heavy_selection = LayoutSelection{ .variations = &heavy };
1872
1873 const default_adjustment = face.gposSingleAdjustmentFor(.{}, glyph);
1874 try std.testing.expectEqual(@as(i32, -40), default_adjustment.x_advance);
1875 try std.testing.expectEqual(@as(i32, 0), default_adjustment.x_placement);
1876
1877 const heavy_adjustment = face.gposSingleAdjustmentFor(heavy_selection, glyph);
1878 try std.testing.expect(face.hasGposSingleAdjustmentFor(heavy_selection));
1879 try std.testing.expectEqual(@as(i32, -120), heavy_adjustment.x_advance);
1880 try std.testing.expectEqual(@as(i32, 0), heavy_adjustment.x_placement);
1881 }
1882
1883 test "Face applies GPOS contextual single adjustments" {
1884 const bytes = try test_font.createWithGposContextualSingleAdjustment(std.testing.allocator);
1885 defer std.testing.allocator.free(bytes);
1886
1887 const face = try Face.init(bytes);
1888 const glyphs = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('C') };
1889 const mismatch = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('!') };
1890 const adjustment = face.gposSingleAdjustmentAtFor(.{}, &glyphs, 0).?;
1891
1892 try std.testing.expect(face.hasGposSingleAdjustment());
1893 try std.testing.expect(face.gposSingleAdjustment(face.glyphId('B')).isZero());
1894 try std.testing.expectEqual(@as(usize, 1), adjustment.target_offset);
1895 try std.testing.expectEqual(@as(usize, 3), adjustment.skip_count);
1896 try std.testing.expectEqual(@as(i32, 50), adjustment.adjustment.x_placement);
1897 try std.testing.expectEqual(@as(i32, -100), adjustment.adjustment.x_advance);
1898 try std.testing.expectEqual(@as(?GposSingleAdjustment, null), face.gposSingleAdjustmentAtFor(.{}, &mismatch, 0));
1899 try std.testing.expectEqual(@as(?GposSingleAdjustment, null), face.gposSingleAdjustmentAtFor(.{}, &glyphs, 1));
1900 }
1901
1902 test "Face applies GPOS simple contextual single adjustments" {
1903 const bytes = try test_font.createWithGposSimpleContextualSingleAdjustment(std.testing.allocator);
1904 defer std.testing.allocator.free(bytes);
1905
1906 const face = try Face.init(bytes);
1907 const glyphs = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('C') };
1908 const mismatch = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('!') };
1909 const adjustment = face.gposSingleAdjustmentAtFor(.{}, &glyphs, 0).?;
1910
1911 try std.testing.expect(face.hasGposSingleAdjustment());
1912 try std.testing.expect(face.gposSingleAdjustment(face.glyphId('B')).isZero());
1913 try std.testing.expectEqual(@as(usize, 1), adjustment.target_offset);
1914 try std.testing.expectEqual(@as(usize, 3), adjustment.skip_count);
1915 try std.testing.expectEqual(@as(i32, 50), adjustment.adjustment.x_placement);
1916 try std.testing.expectEqual(@as(i32, -100), adjustment.adjustment.x_advance);
1917 try std.testing.expectEqual(@as(?GposSingleAdjustment, null), face.gposSingleAdjustmentAtFor(.{}, &mismatch, 0));
1918 try std.testing.expectEqual(@as(?GposSingleAdjustment, null), face.gposSingleAdjustmentAtFor(.{}, &glyphs, 1));
1919 }
1920
1921 test "Face applies GPOS class contextual single adjustments" {
1922 const bytes = try test_font.createWithGposClassContextualSingleAdjustment(std.testing.allocator);
1923 defer std.testing.allocator.free(bytes);
1924
1925 const face = try Face.init(bytes);
1926 const glyphs = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('C') };
1927 const mismatch = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('!') };
1928 const adjustment = face.gposSingleAdjustmentAtFor(.{}, &glyphs, 0).?;
1929
1930 try std.testing.expect(face.hasGposSingleAdjustment());
1931 try std.testing.expect(face.gposSingleAdjustment(face.glyphId('B')).isZero());
1932 try std.testing.expectEqual(@as(usize, 1), adjustment.target_offset);
1933 try std.testing.expectEqual(@as(usize, 3), adjustment.skip_count);
1934 try std.testing.expectEqual(@as(i32, 50), adjustment.adjustment.x_placement);
1935 try std.testing.expectEqual(@as(i32, -100), adjustment.adjustment.x_advance);
1936 try std.testing.expectEqual(@as(?GposSingleAdjustment, null), face.gposSingleAdjustmentAtFor(.{}, &mismatch, 0));
1937 try std.testing.expectEqual(@as(?GposSingleAdjustment, null), face.gposSingleAdjustmentAtFor(.{}, &glyphs, 1));
1938 }
1939
1940 test "Face applies GPOS chained contextual single adjustments" {
1941 const bytes = try test_font.createWithGposChainedContextualSingleAdjustment(std.testing.allocator);
1942 defer std.testing.allocator.free(bytes);
1943
1944 const face = try Face.init(bytes);
1945 const glyphs = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('C'), face.glyphId('!') };
1946 const lookahead_mismatch = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('C'), face.glyphId('A') };
1947 const adjustment = face.gposSingleAdjustmentAtFor(.{}, &glyphs, 1).?;
1948
1949 try std.testing.expect(face.hasGposSingleAdjustment());
1950 try std.testing.expect(face.gposSingleAdjustment(face.glyphId('B')).isZero());
1951 try std.testing.expectEqual(@as(usize, 0), adjustment.target_offset);
1952 try std.testing.expectEqual(@as(usize, 2), adjustment.skip_count);
1953 try std.testing.expectEqual(@as(i32, 75), adjustment.adjustment.x_placement);
1954 try std.testing.expectEqual(@as(i32, -125), adjustment.adjustment.x_advance);
1955 try std.testing.expectEqual(@as(?GposSingleAdjustment, null), face.gposSingleAdjustmentAtFor(.{}, &glyphs, 0));
1956 try std.testing.expectEqual(@as(?GposSingleAdjustment, null), face.gposSingleAdjustmentAtFor(.{}, &lookahead_mismatch, 1));
1957 }
1958
1959 test "Face applies GPOS chained simple contextual single adjustments" {
1960 const bytes = try test_font.createWithGposChainedSimpleContextualSingleAdjustment(std.testing.allocator);
1961 defer std.testing.allocator.free(bytes);
1962
1963 const face = try Face.init(bytes);
1964 const glyphs = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('C') };
1965 const missing_backtrack = [_]u32{ face.glyphId('B'), face.glyphId('C'), face.glyphId('!') };
1966 const missing_lookahead = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('!') };
1967 const adjustment = face.gposSingleAdjustmentAtFor(.{}, &glyphs, 1).?;
1968
1969 try std.testing.expect(face.hasGposSingleAdjustment());
1970 try std.testing.expect(face.gposSingleAdjustment(face.glyphId('B')).isZero());
1971 try std.testing.expectEqual(@as(usize, 0), adjustment.target_offset);
1972 try std.testing.expectEqual(@as(usize, 1), adjustment.skip_count);
1973 try std.testing.expectEqual(@as(i32, 75), adjustment.adjustment.x_placement);
1974 try std.testing.expectEqual(@as(i32, -125), adjustment.adjustment.x_advance);
1975 try std.testing.expectEqual(@as(?GposSingleAdjustment, null), face.gposSingleAdjustmentAtFor(.{}, &glyphs, 0));
1976 try std.testing.expectEqual(@as(?GposSingleAdjustment, null), face.gposSingleAdjustmentAtFor(.{}, &missing_backtrack, 0));
1977 try std.testing.expectEqual(@as(?GposSingleAdjustment, null), face.gposSingleAdjustmentAtFor(.{}, &missing_lookahead, 1));
1978 }
1979
1980 test "Face applies GPOS chained class contextual single adjustments" {
1981 const bytes = try test_font.createWithGposChainedClassContextualSingleAdjustment(std.testing.allocator);
1982 defer std.testing.allocator.free(bytes);
1983
1984 const face = try Face.init(bytes);
1985 const glyphs = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('C') };
1986 const missing_backtrack = [_]u32{ face.glyphId('B'), face.glyphId('C'), face.glyphId('!') };
1987 const missing_lookahead = [_]u32{ face.glyphId('A'), face.glyphId('B'), face.glyphId('!') };
1988 const adjustment = face.gposSingleAdjustmentAtFor(.{}, &glyphs, 1).?;
1989
1990 try std.testing.expect(face.hasGposSingleAdjustment());
1991 try std.testing.expect(face.gposSingleAdjustment(face.glyphId('B')).isZero());
1992 try std.testing.expectEqual(@as(usize, 0), adjustment.target_offset);
1993 try std.testing.expectEqual(@as(usize, 1), adjustment.skip_count);
1994 try std.testing.expectEqual(@as(i32, 75), adjustment.adjustment.x_placement);
1995 try std.testing.expectEqual(@as(i32, -125), adjustment.adjustment.x_advance);
1996 try std.testing.expectEqual(@as(?GposSingleAdjustment, null), face.gposSingleAdjustmentAtFor(.{}, &glyphs, 0));
1997 try std.testing.expectEqual(@as(?GposSingleAdjustment, null), face.gposSingleAdjustmentAtFor(.{}, &missing_backtrack, 0));
1998 try std.testing.expectEqual(@as(?GposSingleAdjustment, null), face.gposSingleAdjustmentAtFor(.{}, &missing_lookahead, 1));
1999 }
2000
2001 test "Face applies GPOS cursive attachments" {
2002 const bytes = try test_font.createWithGposCursiveAttachment(std.testing.allocator);
2003 defer std.testing.allocator.free(bytes);
2004
2005 const face = try Face.init(bytes);
2006 const a = face.glyphId('A');
2007 const b = face.glyphId('B');
2008
2009 try std.testing.expect(face.hasGposCursiveAttachment());
2010 const attachment = face.gposCursiveAttachment(a, b).?;
2011 try std.testing.expectEqual(@as(i32, 460), attachment.x_anchor_delta);
2012 try std.testing.expectEqual(@as(i32, 100), attachment.y_anchor_delta);
2013 try std.testing.expect(face.gposCursiveAttachment(b, a) == null);
2014 try std.testing.expect(face.gposCursiveAttachment(a, face.glyphId('!')) == null);
2015
2016 const vertical = LayoutSelection{ .vertical = true };
2017 const vertical_enabled = LayoutSelection{ .vertical = true, .features = &.{.{ .tag = tag("curs"), .value = 1 }} };
2018
2019 try std.testing.expect(!face.hasGposCursiveAttachmentFor(vertical));
2020 try std.testing.expect(face.hasGposCursiveAttachmentFor(vertical_enabled));
2021 try std.testing.expect(face.gposCursiveAttachmentFor(vertical, a, b) == null);
2022 try std.testing.expectEqual(@as(i32, 460), face.gposCursiveAttachmentFor(vertical_enabled, a, b).?.x_anchor_delta);
2023 }
2024
2025 test "Face applies GPOS mark-to-base attachment" {
2026 const bytes = try test_font.createWithGposMarkToBase(std.testing.allocator);
2027 defer std.testing.allocator.free(bytes);
2028
2029 const face = try Face.init(bytes);
2030 const a = face.glyphId('A');
2031 const mark = face.glyphId('^');
2032
2033 try std.testing.expect(face.hasGposMarkToBase());
2034 const adjustment = face.gposMarkToBaseAdjustment(a, mark).?;
2035 try std.testing.expectEqual(@as(i32, 150), adjustment.x_placement);
2036 try std.testing.expectEqual(@as(i32, 700), adjustment.y_placement);
2037 try std.testing.expect(face.gposMarkToBaseAdjustment(mark, a) == null);
2038 try std.testing.expect(face.gposMarkToBaseAdjustment(a, face.glyphId('!')) == null);
2039 }
2040
2041 test "Face applies GPOS above and below-base mark positioning by default" {
2042 const abvm_bytes = try test_font.createWithGposAboveBaseMarkPositioning(std.testing.allocator);
2043 defer std.testing.allocator.free(abvm_bytes);
2044 const blwm_bytes = try test_font.createWithGposBelowBaseMarkPositioning(std.testing.allocator);
2045 defer std.testing.allocator.free(blwm_bytes);
2046
2047 const abvm_face = try Face.init(abvm_bytes);
2048 const blwm_face = try Face.init(blwm_bytes);
2049 const abvm_base = abvm_face.glyphId('A');
2050 const abvm_mark = abvm_face.glyphId('^');
2051 const blwm_base = blwm_face.glyphId('A');
2052 const blwm_mark = blwm_face.glyphId('^');
2053 const disabled_abvm = LayoutSelection{ .features = &.{.{ .tag = tag("abvm"), .value = 0 }} };
2054 const disabled_blwm = LayoutSelection{ .features = &.{.{ .tag = tag("blwm"), .value = 0 }} };
2055
2056 try std.testing.expectEqual(@as(i32, 150), abvm_face.gposMarkToBaseAdjustment(abvm_base, abvm_mark).?.x_placement);
2057 try std.testing.expectEqual(@as(i32, 700), abvm_face.gposMarkToBaseAdjustmentFor(.{ .vertical = true }, abvm_base, abvm_mark).?.y_placement);
2058 try std.testing.expectEqual(@as(i32, 150), blwm_face.gposMarkToBaseAdjustment(blwm_base, blwm_mark).?.x_placement);
2059 try std.testing.expectEqual(@as(i32, 700), blwm_face.gposMarkToBaseAdjustmentFor(.{ .vertical = true }, blwm_base, blwm_mark).?.y_placement);
2060 try std.testing.expect(abvm_face.gposMarkToBaseAdjustmentFor(disabled_abvm, abvm_base, abvm_mark) == null);
2061 try std.testing.expect(blwm_face.gposMarkToBaseAdjustmentFor(disabled_blwm, blwm_base, blwm_mark) == null);
2062 }
2063
2064 test "Face applies GPOS mark-to-ligature attachment" {
2065 const bytes = try test_font.createWithGposMarkToLigature(std.testing.allocator);
2066 defer std.testing.allocator.free(bytes);
2067
2068 const face = try Face.init(bytes);
2069 const ligature = @as(u32, 96);
2070 const mark = face.glyphId('^');
2071
2072 try std.testing.expect(face.hasGposMarkToLigature());
2073 const first_component = face.gposMarkToLigatureAdjustment(ligature, mark, 0).?;
2074 try std.testing.expectEqual(@as(i32, 80), first_component.x_placement);
2075 try std.testing.expectEqual(@as(i32, 700), first_component.y_placement);
2076 const second_component = face.gposMarkToLigatureAdjustment(ligature, mark, 1).?;
2077 try std.testing.expectEqual(@as(i32, 230), second_component.x_placement);
2078 try std.testing.expectEqual(@as(i32, 650), second_component.y_placement);
2079 try std.testing.expect(face.gposMarkToLigatureAdjustment(ligature, mark, 2) == null);
2080 try std.testing.expect(face.gposMarkToLigatureAdjustment(mark, ligature, 0) == null);
2081 try std.testing.expect(face.gposMarkToLigatureAdjustment(ligature, face.glyphId('!'), 0) == null);
2082 }
2083
2084 test "Face applies GPOS mark-to-mark attachment" {
2085 const bytes = try test_font.createWithGposMarkToMark(std.testing.allocator);
2086 defer std.testing.allocator.free(bytes);
2087
2088 const face = try Face.init(bytes);
2089 const base_mark = face.glyphId('^');
2090 const mark = face.glyphId('_');
2091
2092 try std.testing.expect(face.hasGposMarkToMark());
2093 const adjustment = face.gposMarkToMarkAdjustment(base_mark, mark).?;
2094 try std.testing.expectEqual(@as(i32, 20), adjustment.x_placement);
2095 try std.testing.expectEqual(@as(i32, 300), adjustment.y_placement);
2096 try std.testing.expect(face.gposMarkToMarkAdjustment(mark, base_mark) == null);
2097 try std.testing.expect(face.gposMarkToMarkAdjustment(base_mark, face.glyphId('!')) == null);
2098 }
2099
2100 test "Face reads GDEF glyph classes" {
2101 const bytes = try test_font.createWithGdefGlyphClasses(std.testing.allocator);
2102 defer std.testing.allocator.free(bytes);
2103
2104 const face = try Face.init(bytes);
2105
2106 try std.testing.expectEqual(GlyphClass.base, face.glyphClass(face.glyphId('A')));
2107 try std.testing.expectEqual(GlyphClass.mark, face.glyphClass(face.glyphId('^')));
2108 try std.testing.expectEqual(GlyphClass.component, face.glyphClass(face.glyphId('_')));
2109 try std.testing.expectEqual(GlyphClass.ligature, face.glyphClass(96));
2110 try std.testing.expectEqual(GlyphClass.unknown, face.glyphClass(face.glyphId('!')));
2111 }
2112
2113 test "Face applies GPOS mark-to-base with matching mark attachment type" {
2114 const bytes = try test_font.createWithGposMarkToBase(std.testing.allocator);
2115 defer std.testing.allocator.free(bytes);
2116 try fixture_binary.setLayoutLookupFlag(bytes, "GPOS", 0, 0x0300);
2117 const gdef_offset = try fixture_binary.tableOffset(bytes, "GDEF");
2118 fixture_binary.writeU16(bytes, gdef_offset + 10, 12);
2119
2120 const face = try Face.init(bytes);
2121 const adjustment = face.gposMarkToBaseAdjustment(face.glyphId('A'), face.glyphId('^')).?;
2122
2123 try std.testing.expectEqual(@as(i32, 150), adjustment.x_placement);
2124 try std.testing.expectEqual(@as(i32, 700), adjustment.y_placement);
2125 }
2126
2127 test "Face filters GPOS mark-to-base with mismatched mark attachment type" {
2128 const bytes = try test_font.createWithGposMarkToBase(std.testing.allocator);
2129 defer std.testing.allocator.free(bytes);
2130 try fixture_binary.setLayoutLookupFlag(bytes, "GPOS", 0, 0x0200);
2131 const gdef_offset = try fixture_binary.tableOffset(bytes, "GDEF");
2132 fixture_binary.writeU16(bytes, gdef_offset + 10, 12);
2133
2134 const face = try Face.init(bytes);
2135
2136 try std.testing.expect(face.gposMarkToBaseAdjustment(face.glyphId('A'), face.glyphId('^')) == null);
2137 }
2138
2139 test "Face reads GDEF ligature caret coordinates" {
2140 const bytes = try test_font.createWithGsubLigatureAndGdefCarets(std.testing.allocator);
2141 defer std.testing.allocator.free(bytes);
2142
2143 const face = try Face.init(bytes);
2144 const caret = face.ligatureCaret(96, 0).?;
2145
2146 try std.testing.expectEqual(@as(u16, 1), face.ligatureCaretCount(96));
2147 try std.testing.expectEqual(@as(i16, 250), caret.coordinate);
2148 try std.testing.expectEqual(false, caret.synthesized);
2149 try std.testing.expect(face.ligatureCaret(96, 1) == null);
2150 try std.testing.expectEqual(@as(u16, 0), face.ligatureCaretCount(face.glyphId('A')));
2151 try std.testing.expect(face.ligatureCaret(face.glyphId('A'), 0) == null);
2152 }
2153
2154 test "Face exposes horizontal MATH assemblies" {
2155 const bytes = try test_font.createWithMathAssembly(std.testing.allocator);
2156 defer std.testing.allocator.free(bytes);
2157
2158 const face = try Face.init(bytes);
2159 const arrow = face.glyphId(0x2192);
2160 var assembly = (try face.mathHorizontalAssemblyAlloc(std.testing.allocator, arrow, 1300)).?;
2161 defer assembly.deinit(std.testing.allocator);
2162
2163 try std.testing.expect(arrow != 0);
2164 try std.testing.expect(face.mathHorizontalVariant(arrow, 1300) == null);
2165 try std.testing.expectEqual(@as(u16, 1300), assembly.advance_measurement);
2166 try std.testing.expectEqual(@as(usize, 4), assembly.parts.len);
2167 }
2168
2169 test "Face exposes MATH top accent attachments" {
2170 const bytes = try test_font.createWithMathAssembly(std.testing.allocator);
2171 defer std.testing.allocator.free(bytes);
2172
2173 const face = try Face.init(bytes);
2174 const base = face.mathTopAccentAttachment(face.glyphId('A')).?;
2175 const accent = face.mathTopAccentAttachment(face.glyphId('`')).?;
2176
2177 try std.testing.expectEqual(@as(i16, 420), base.value);
2178 try std.testing.expectEqual(@as(i16, 80), accent.value);
2179 try std.testing.expect(face.mathTopAccentAttachment(face.glyphId('B')) == null);
2180 }
2181
2182 test "Face ignores malformed optional GSUB table" {
2183 const bytes = try test_font.createWithGsubSingleSubstitution(std.testing.allocator);
2184 defer std.testing.allocator.free(bytes);
2185
2186 const gsub_table = try table(bytes, "GSUB");
2187 std.mem.writeInt(u16, bytes[gsub_table.offset..][0..2], 2, .big);
2188
2189 const face = try Face.init(bytes);
2190 const a = face.glyphId('A');
2191 try std.testing.expectEqual(a, face.substituteGlyph(a));
2192 }
2193
2194 test "Face ignores malformed optional GPOS table" {
2195 const bytes = try test_font.createWithGposPairAdjustment(std.testing.allocator);
2196 defer std.testing.allocator.free(bytes);
2197
2198 const gpos_table = try table(bytes, "GPOS");
2199 std.mem.writeInt(u16, bytes[gpos_table.offset..][0..2], 2, .big);
2200
2201 const face = try Face.init(bytes);
2202 try std.testing.expect(face.gposPairAdjustment(face.glyphId('A'), face.glyphId('V')).isZero());
2203 }
2204
2205 test "Face ignores malformed optional GDEF table" {
2206 const bytes = try test_font.createWithGdefGlyphClasses(std.testing.allocator);
2207 defer std.testing.allocator.free(bytes);
2208
2209 const gdef_table = try table(bytes, "GDEF");
2210 std.mem.writeInt(u16, bytes[gdef_table.offset..][0..2], 2, .big);
2211
2212 const face = try Face.init(bytes);
2213 try std.testing.expectEqual(GlyphClass.unknown, face.glyphClass(face.glyphId('A')));
2214 }
2215
2216 test "Face ignores malformed optional kern table" {
2217 const bytes = try test_font.createWithKern(std.testing.allocator);
2218 defer std.testing.allocator.free(bytes);
2219
2220 const kern_table = try table(bytes, "kern");
2221 std.mem.writeInt(u16, bytes[kern_table.offset + 6 ..][0..2], 2, .big);
2222
2223 const face = try Face.init(bytes);
2224 try std.testing.expectEqual(@as(i16, 0), face.kerning(face.glyphId('A'), face.glyphId('V')));
2225 }
2226
2227 test "GPOS value records apply GDEF variation deltas" {
2228 const bytes = try test_font.createWithGposSingleVariationAdjustment(std.testing.allocator);
2229 defer std.testing.allocator.free(bytes);
2230 const face = try Face.init(bytes);
2231
2232 const adjustment = face.gposSingleAdjustment(face.glyphId('A'));
2233 try std.testing.expectEqual(@as(i32, 20), adjustment.x_placement);
2234 try std.testing.expectEqual(@as(i32, 20), adjustment.withVariations(face, &.{}).x_placement);
2235 try std.testing.expectEqual(@as(i32, 60), adjustment.withVariations(face, &.{.{ .tag = tag("wght"), .value = 650.0 }}).x_placement);
2236 try std.testing.expectEqual(@as(i32, 100), adjustment.withVariations(face, &.{.{ .tag = tag("wght"), .value = 900.0 }}).x_placement);
2237 }
2238
2239 test "GDEF reads mark attachment classes and mark glyph sets" {
2240 const bytes = try createGdefMarkFilterFixture(std.testing.allocator);
2241 defer std.testing.allocator.free(bytes);
2242
2243 const gdef = try Gdef.init(bytes, .{ .offset = 0, .len = bytes.len }, null);
2244
2245 try std.testing.expectEqual(GlyphClass.mark, try gdef.glyphClass(bytes, '^' - 31));
2246 try std.testing.expectEqual(@as(u16, 1), try gdef.markAttachmentClass(bytes, '^' - 31));
2247 try std.testing.expectEqual(@as(u16, 2), try gdef.markAttachmentClass(bytes, '_' - 31));
2248 try std.testing.expect(try gdef.markGlyphSetContains(bytes, 0, '^' - 31));
2249 try std.testing.expect(!try gdef.markGlyphSetContains(bytes, 0, '_' - 31));
2250 }
2251
2252 test "lookup filters use GDEF mark classes and mark glyph sets" {
2253 const bytes = try createGdefMarkFilterFixture(std.testing.allocator);
2254 defer std.testing.allocator.free(bytes);
2255
2256 const gdef = try Gdef.init(bytes, .{ .offset = 0, .len = bytes.len }, null);
2257
2258 try std.testing.expect(!try lookupGlyphIgnored(bytes, gdef, .{ .flags = 0x0100 }, '^' - 31));
2259 try std.testing.expect(try lookupGlyphIgnored(bytes, gdef, .{ .flags = 0x0100 }, '_' - 31));
2260 try std.testing.expect(!try lookupGlyphIgnored(bytes, gdef, .{ .flags = lookup_use_mark_filtering_set, .mark_filtering_set = 0 }, '^' - 31));
2261 try std.testing.expect(try lookupGlyphIgnored(bytes, gdef, .{ .flags = lookup_use_mark_filtering_set, .mark_filtering_set = 0 }, '_' - 31));
2262 }
2263
2264 fn createGdefMarkFilterFixture(allocator: std.mem.Allocator) ![]u8 {
2265 var out = std.ArrayListUnmanaged(u8).empty;
2266 errdefer out.deinit(allocator);
2267
2268 try fixture_binary.appendU16(allocator, &out, 1);
2269 try fixture_binary.appendU16(allocator, &out, 2);
2270 try fixture_binary.appendU16(allocator, &out, 14);
2271 try fixture_binary.appendU16(allocator, &out, 0);
2272 try fixture_binary.appendU16(allocator, &out, 0);
2273 try fixture_binary.appendU16(allocator, &out, 30);
2274 try fixture_binary.appendU16(allocator, &out, 46);
2275
2276 try fixture_binary.appendU16(allocator, &out, 2);
2277 try fixture_binary.appendU16(allocator, &out, 2);
2278 try fixture_binary.appendU16(allocator, &out, '^' - 31);
2279 try fixture_binary.appendU16(allocator, &out, '^' - 31);
2280 try fixture_binary.appendU16(allocator, &out, 3);
2281 try fixture_binary.appendU16(allocator, &out, '_' - 31);
2282 try fixture_binary.appendU16(allocator, &out, '_' - 31);
2283 try fixture_binary.appendU16(allocator, &out, 3);
2284
2285 try fixture_binary.appendU16(allocator, &out, 2);
2286 try fixture_binary.appendU16(allocator, &out, 2);
2287 try fixture_binary.appendU16(allocator, &out, '^' - 31);
2288 try fixture_binary.appendU16(allocator, &out, '^' - 31);
2289 try fixture_binary.appendU16(allocator, &out, 1);
2290 try fixture_binary.appendU16(allocator, &out, '_' - 31);
2291 try fixture_binary.appendU16(allocator, &out, '_' - 31);
2292 try fixture_binary.appendU16(allocator, &out, 2);
2293
2294 try fixture_binary.appendU16(allocator, &out, 1);
2295 try fixture_binary.appendU16(allocator, &out, 1);
2296 try fixture_binary.appendU32(allocator, &out, 8);
2297
2298 try fixture_binary.appendU16(allocator, &out, 1);
2299 try fixture_binary.appendU16(allocator, &out, 1);
2300 try fixture_binary.appendU16(allocator, &out, '^' - 31);
2301
2302 return out.toOwnedSlice(allocator);
2303 }