lib/filigree/src/font/math.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const binary = @import("binary.zig");
  3 const layout = @import("../layout/root.zig");
  4 const model = @import("model.zig");
  5 
  6 const FontError = model.FontError;
  7 const Table = binary.Table;
  8 const readI16 = binary.readI16;
  9 const readU16 = binary.readU16;
 10 
 11 const math_header_size = 10;
 12 const math_constants_size = 214;
 13 const math_glyph_info_header_size = 8;
 14 const math_top_accent_attachment_header_size = 4;
 15 const math_variants_header_size = 10;
 16 const math_glyph_construction_header_size = 4;
 17 const math_glyph_variant_size = 4;
 18 const math_glyph_assembly_header_size = 6;
 19 const math_glyph_part_size = 10;
 20 const math_value_record_size = 4;
 21 const glyph_part_extender_flag = 0x0001;
 22 const max_assembly_parts = 4096;
 23 
 24 pub const ValueRecord = struct {
 25     value: i16,
 26     device_offset: u16,
 27 };
 28 
 29 pub const Constants = struct {
 30     script_percent_scale_down: i16,
 31     script_script_percent_scale_down: i16,
 32     delimited_sub_formula_min_height: u16,
 33     display_operator_min_height: u16,
 34     math_leading: ValueRecord,
 35     axis_height: ValueRecord,
 36     accent_base_height: ValueRecord,
 37     flattened_accent_base_height: ValueRecord,
 38     subscript_shift_down: ValueRecord,
 39     subscript_top_max: ValueRecord,
 40     subscript_baseline_drop_min: ValueRecord,
 41     superscript_shift_up: ValueRecord,
 42     superscript_shift_up_cramped: ValueRecord,
 43     superscript_bottom_min: ValueRecord,
 44     superscript_baseline_drop_max: ValueRecord,
 45     sub_superscript_gap_min: ValueRecord,
 46     superscript_bottom_max_with_subscript: ValueRecord,
 47     space_after_script: ValueRecord,
 48     upper_limit_gap_min: ValueRecord,
 49     upper_limit_baseline_rise_min: ValueRecord,
 50     lower_limit_gap_min: ValueRecord,
 51     lower_limit_baseline_drop_min: ValueRecord,
 52     stack_top_shift_up: ValueRecord,
 53     stack_top_display_style_shift_up: ValueRecord,
 54     stack_bottom_shift_down: ValueRecord,
 55     stack_bottom_display_style_shift_down: ValueRecord,
 56     stack_gap_min: ValueRecord,
 57     stack_display_style_gap_min: ValueRecord,
 58     stretch_stack_top_shift_up: ValueRecord,
 59     stretch_stack_bottom_shift_down: ValueRecord,
 60     stretch_stack_gap_above_min: ValueRecord,
 61     stretch_stack_gap_below_min: ValueRecord,
 62     fraction_numerator_shift_up: ValueRecord,
 63     fraction_numerator_display_style_shift_up: ValueRecord,
 64     fraction_denominator_shift_down: ValueRecord,
 65     fraction_denominator_display_style_shift_down: ValueRecord,
 66     fraction_numerator_gap_min: ValueRecord,
 67     fraction_num_display_style_gap_min: ValueRecord,
 68     fraction_rule_thickness: ValueRecord,
 69     fraction_denominator_gap_min: ValueRecord,
 70     fraction_denom_display_style_gap_min: ValueRecord,
 71     skewed_fraction_horizontal_gap: ValueRecord,
 72     skewed_fraction_vertical_gap: ValueRecord,
 73     overbar_vertical_gap: ValueRecord,
 74     overbar_rule_thickness: ValueRecord,
 75     overbar_extra_ascender: ValueRecord,
 76     underbar_vertical_gap: ValueRecord,
 77     underbar_rule_thickness: ValueRecord,
 78     underbar_extra_descender: ValueRecord,
 79     radical_vertical_gap: ValueRecord,
 80     radical_display_style_vertical_gap: ValueRecord,
 81     radical_rule_thickness: ValueRecord,
 82     radical_extra_ascender: ValueRecord,
 83     radical_kern_before_degree: ValueRecord,
 84     radical_kern_after_degree: ValueRecord,
 85     radical_degree_bottom_raise_percent: i16,
 86 };
 87 
 88 pub const GlyphVariant = struct {
 89     glyph_id: u16,
 90     advance_measurement: u16,
 91 };
 92 
 93 pub const GlyphAssemblyPart = struct {
 94     glyph_id: u16,
 95     advance_offset: u16,
 96     full_advance: u16,
 97 };
 98 
 99 pub const GlyphAssembly = struct {
100     parts: []GlyphAssemblyPart,
101     advance_measurement: u16,
102     italics_correction: ValueRecord,
103 
104     pub fn deinit(self: GlyphAssembly, allocator: std.mem.Allocator) void {
105         allocator.free(self.parts);
106     }
107 };
108 
109 pub const GlyphInfo = struct {
110     top_accent_attachment: ?TopAccentAttachment,
111 
112     pub fn init(data: []const u8, math_table: Table, relative_offset: u16) FontError!GlyphInfo {
113         const glyph_info_offset = try mathChildOffset(math_table, relative_offset, math_glyph_info_header_size);
114         const top_accent_relative = try readU16(data, glyph_info_offset + 2);
115         return .{
116             .top_accent_attachment = if (top_accent_relative == 0) null else try TopAccentAttachment.init(data, math_table, glyph_info_offset, top_accent_relative),
117         };
118     }
119 
120     pub fn topAccentAttachment(self: GlyphInfo, data: []const u8, glyph_id: u32) FontError!?ValueRecord {
121         const attachment = self.top_accent_attachment orelse return null;
122         return try attachment.value(data, glyph_id);
123     }
124 };
125 
126 const TopAccentAttachment = struct {
127     coverage: layout.Coverage,
128     value_offset: usize,
129     count: u16,
130 
131     fn init(data: []const u8, table: Table, glyph_info_offset: usize, relative_offset: u16) FontError!TopAccentAttachment {
132         const offset = try mathChildOffsetFrom(table, glyph_info_offset, relative_offset, math_top_accent_attachment_header_size);
133         const coverage_relative = try readU16(data, offset);
134         const count = try readU16(data, offset + 2);
135         const value_offset = offset + math_top_accent_attachment_header_size;
136         const values_len = @as(usize, count) * math_value_record_size;
137         const offset_relative = offset - table.offset;
138         if (offset_relative > table.len or math_top_accent_attachment_header_size > table.len - offset_relative or values_len > table.len - offset_relative - math_top_accent_attachment_header_size) return error.InvalidFont;
139         const coverage_offset = try mathChildOffsetFrom(table, offset, coverage_relative, 2);
140         const coverage = layout.Coverage.init(data, coverage_offset) catch return error.InvalidFont;
141         return .{
142             .coverage = coverage,
143             .value_offset = value_offset,
144             .count = count,
145         };
146     }
147 
148     fn value(self: TopAccentAttachment, data: []const u8, glyph_id: u32) FontError!?ValueRecord {
149         const index = (self.coverage.index(data, glyph_id) catch return error.InvalidFont) orelse return null;
150         if (index >= self.count) return error.InvalidFont;
151         const offset = self.value_offset + @as(usize, index) * math_value_record_size;
152         return .{
153             .value = try readI16(data, offset),
154             .device_offset = try readU16(data, offset + 2),
155         };
156     }
157 };
158 
159 pub const Variants = struct {
160     table: Table,
161     variants_offset: usize,
162     min_connector_overlap: u16,
163     vertical_coverage: ?layout.Coverage,
164     horizontal_coverage: ?layout.Coverage,
165     vertical_glyph_count: u16,
166     horizontal_glyph_count: u16,
167     vertical_construction_offsets: usize,
168     horizontal_construction_offsets: usize,
169 
170     pub fn init(data: []const u8, math_table: Table, relative_offset: u16) FontError!Variants {
171         const variants_offset = try mathChildOffset(math_table, relative_offset, math_variants_header_size);
172         const min_connector_overlap = try readU16(data, variants_offset);
173         const vertical_coverage_relative = try readU16(data, variants_offset + 2);
174         const horizontal_coverage_relative = try readU16(data, variants_offset + 4);
175         const vertical_glyph_count = try readU16(data, variants_offset + 6);
176         const horizontal_glyph_count = try readU16(data, variants_offset + 8);
177         const vertical_construction_offsets = variants_offset + math_variants_header_size;
178         const horizontal_construction_offsets = vertical_construction_offsets + @as(usize, vertical_glyph_count) * 2;
179         const construction_arrays_len = @as(usize, vertical_glyph_count) * 2 + @as(usize, horizontal_glyph_count) * 2;
180         _ = try mathVariantsChildOffset(math_table, variants_offset, math_variants_header_size, construction_arrays_len);
181         const vertical_coverage = try optionalCoverage(data, math_table, variants_offset, vertical_coverage_relative, vertical_glyph_count);
182         const horizontal_coverage = try optionalCoverage(data, math_table, variants_offset, horizontal_coverage_relative, horizontal_glyph_count);
183         var variants = Variants{
184             .table = math_table,
185             .variants_offset = variants_offset,
186             .min_connector_overlap = min_connector_overlap,
187             .vertical_coverage = vertical_coverage,
188             .horizontal_coverage = horizontal_coverage,
189             .vertical_glyph_count = vertical_glyph_count,
190             .horizontal_glyph_count = horizontal_glyph_count,
191             .vertical_construction_offsets = vertical_construction_offsets,
192             .horizontal_construction_offsets = horizontal_construction_offsets,
193         };
194         try variants.validateConstructions(data, .vertical);
195         try variants.validateConstructions(data, .horizontal);
196         return variants;
197     }
198 
199     pub fn verticalVariant(self: Variants, data: []const u8, glyph_id: u32, target_measurement: u16) FontError!?GlyphVariant {
200         return self.variant(data, glyph_id, target_measurement, .vertical);
201     }
202 
203     pub fn horizontalVariant(self: Variants, data: []const u8, glyph_id: u32, target_measurement: u16) FontError!?GlyphVariant {
204         return self.variant(data, glyph_id, target_measurement, .horizontal);
205     }
206 
207     pub fn verticalAssemblyAlloc(self: Variants, allocator: std.mem.Allocator, data: []const u8, glyph_id: u32, target_measurement: u16) AssemblyError!?GlyphAssembly {
208         return self.assemblyAlloc(allocator, data, glyph_id, target_measurement, .vertical);
209     }
210 
211     pub fn horizontalAssemblyAlloc(self: Variants, allocator: std.mem.Allocator, data: []const u8, glyph_id: u32, target_measurement: u16) AssemblyError!?GlyphAssembly {
212         return self.assemblyAlloc(allocator, data, glyph_id, target_measurement, .horizontal);
213     }
214 
215     fn variant(self: Variants, data: []const u8, glyph_id: u32, target_measurement: u16, direction: VariantDirection) FontError!?GlyphVariant {
216         const construction_offset = (try self.constructionOffset(data, glyph_id, direction)) orelse return null;
217         return try constructionVariant(data, self.table, construction_offset, target_measurement);
218     }
219 
220     fn assemblyAlloc(self: Variants, allocator: std.mem.Allocator, data: []const u8, glyph_id: u32, target_measurement: u16, direction: VariantDirection) AssemblyError!?GlyphAssembly {
221         const construction_offset = (try self.constructionOffset(data, glyph_id, direction)) orelse return null;
222         return try constructionAssemblyAlloc(allocator, data, self.table, construction_offset, self.min_connector_overlap, target_measurement);
223     }
224 
225     fn constructionOffset(self: Variants, data: []const u8, glyph_id: u32, direction: VariantDirection) FontError!?usize {
226         const coverage = switch (direction) {
227             .vertical => self.vertical_coverage orelse return null,
228             .horizontal => self.horizontal_coverage orelse return null,
229         };
230         const index = (coverage.index(data, glyph_id) catch return error.InvalidFont) orelse return null;
231         const count = switch (direction) {
232             .vertical => self.vertical_glyph_count,
233             .horizontal => self.horizontal_glyph_count,
234         };
235         if (index >= count) return error.InvalidFont;
236         const offset_array = switch (direction) {
237             .vertical => self.vertical_construction_offsets,
238             .horizontal => self.horizontal_construction_offsets,
239         };
240         const relative = try readU16(data, offset_array + @as(usize, index) * 2);
241         return try mathVariantsChildOffset(self.table, self.variants_offset, relative, math_glyph_construction_header_size);
242     }
243 
244     fn validateConstructions(self: Variants, data: []const u8, direction: VariantDirection) FontError!void {
245         const count = switch (direction) {
246             .vertical => self.vertical_glyph_count,
247             .horizontal => self.horizontal_glyph_count,
248         };
249         const offset_array = switch (direction) {
250             .vertical => self.vertical_construction_offsets,
251             .horizontal => self.horizontal_construction_offsets,
252         };
253         for (0..count) |index| {
254             const relative = try readU16(data, offset_array + index * 2);
255             const construction_offset = try mathVariantsChildOffset(self.table, self.variants_offset, relative, math_glyph_construction_header_size);
256             try validateConstruction(data, self.table, construction_offset);
257         }
258     }
259 };
260 
261 pub const Math = struct {
262     table: Table,
263     constants_offset: usize,
264     constants: Constants,
265     glyph_info: ?GlyphInfo,
266     variants: ?Variants,
267 
268     pub fn init(data: []const u8, math_table: Table) FontError!Math {
269         if (math_table.len < math_header_size) return error.InvalidFont;
270         const major = try readU16(data, math_table.offset);
271         const minor = try readU16(data, math_table.offset + 2);
272         if (major != 1 or minor != 0) return error.InvalidFont;
273 
274         const constants_relative = try readU16(data, math_table.offset + 4);
275         const glyph_info_relative = try readU16(data, math_table.offset + 6);
276         const variants_relative = try readU16(data, math_table.offset + 8);
277         if (glyph_info_relative > math_table.len or variants_relative > math_table.len) return error.InvalidFont;
278 
279         const constants_offset = try mathChildOffset(math_table, constants_relative, math_constants_size);
280         const glyph_info = if (glyph_info_relative == 0) null else try GlyphInfo.init(data, math_table, glyph_info_relative);
281         const variants = if (variants_relative == 0) null else try Variants.init(data, math_table, variants_relative);
282         return .{
283             .table = math_table,
284             .constants_offset = constants_offset,
285             .constants = try readConstants(data, constants_offset),
286             .glyph_info = glyph_info,
287             .variants = variants,
288         };
289     }
290 
291     pub fn topAccentAttachment(self: Math, data: []const u8, glyph_id: u32) FontError!?ValueRecord {
292         const glyph_info = self.glyph_info orelse return null;
293         return try glyph_info.topAccentAttachment(data, glyph_id);
294     }
295 
296     pub fn verticalVariant(self: Math, data: []const u8, glyph_id: u32, target_measurement: u16) FontError!?GlyphVariant {
297         const variants = self.variants orelse return null;
298         return variants.verticalVariant(data, glyph_id, target_measurement);
299     }
300 
301     pub fn horizontalVariant(self: Math, data: []const u8, glyph_id: u32, target_measurement: u16) FontError!?GlyphVariant {
302         const variants = self.variants orelse return null;
303         return variants.horizontalVariant(data, glyph_id, target_measurement);
304     }
305 
306     pub fn verticalAssemblyAlloc(self: Math, allocator: std.mem.Allocator, data: []const u8, glyph_id: u32, target_measurement: u16) AssemblyError!?GlyphAssembly {
307         const variants = self.variants orelse return null;
308         return variants.verticalAssemblyAlloc(allocator, data, glyph_id, target_measurement);
309     }
310 
311     pub fn horizontalAssemblyAlloc(self: Math, allocator: std.mem.Allocator, data: []const u8, glyph_id: u32, target_measurement: u16) AssemblyError!?GlyphAssembly {
312         const variants = self.variants orelse return null;
313         return variants.horizontalAssemblyAlloc(allocator, data, glyph_id, target_measurement);
314     }
315 };
316 
317 const VariantDirection = enum {
318     vertical,
319     horizontal,
320 };
321 
322 const AssemblyError = FontError || std.mem.Allocator.Error;
323 
324 const GlyphPartRecord = struct {
325     glyph_id: u16,
326     start_connector_length: u16,
327     end_connector_length: u16,
328     full_advance: u16,
329     flags: u16,
330 
331     fn extender(self: GlyphPartRecord) bool {
332         return self.flags & glyph_part_extender_flag != 0;
333     }
334 };
335 
336 const AssemblyRange = struct {
337     min_size: i64,
338     max_size: i64,
339 };
340 
341 const AssemblyConnection = struct {
342     max_overlap: u16,
343     min_overlap: u16,
344     extra: u16 = 0,
345 
346     fn capacity(self: AssemblyConnection) u16 {
347         return self.max_overlap - self.min_overlap;
348     }
349 };
350 
351 fn readConstants(data: []const u8, offset: usize) FontError!Constants {
352     var cursor = offset;
353     const script_percent_scale_down = try readI16(data, cursor);
354     cursor += 2;
355     const script_script_percent_scale_down = try readI16(data, cursor);
356     cursor += 2;
357     const delimited_sub_formula_min_height = try readU16(data, cursor);
358     cursor += 2;
359     const display_operator_min_height = try readU16(data, cursor);
360     cursor += 2;
361     return .{
362         .script_percent_scale_down = script_percent_scale_down,
363         .script_script_percent_scale_down = script_script_percent_scale_down,
364         .delimited_sub_formula_min_height = delimited_sub_formula_min_height,
365         .display_operator_min_height = display_operator_min_height,
366         .math_leading = try readValue(data, &cursor),
367         .axis_height = try readValue(data, &cursor),
368         .accent_base_height = try readValue(data, &cursor),
369         .flattened_accent_base_height = try readValue(data, &cursor),
370         .subscript_shift_down = try readValue(data, &cursor),
371         .subscript_top_max = try readValue(data, &cursor),
372         .subscript_baseline_drop_min = try readValue(data, &cursor),
373         .superscript_shift_up = try readValue(data, &cursor),
374         .superscript_shift_up_cramped = try readValue(data, &cursor),
375         .superscript_bottom_min = try readValue(data, &cursor),
376         .superscript_baseline_drop_max = try readValue(data, &cursor),
377         .sub_superscript_gap_min = try readValue(data, &cursor),
378         .superscript_bottom_max_with_subscript = try readValue(data, &cursor),
379         .space_after_script = try readValue(data, &cursor),
380         .upper_limit_gap_min = try readValue(data, &cursor),
381         .upper_limit_baseline_rise_min = try readValue(data, &cursor),
382         .lower_limit_gap_min = try readValue(data, &cursor),
383         .lower_limit_baseline_drop_min = try readValue(data, &cursor),
384         .stack_top_shift_up = try readValue(data, &cursor),
385         .stack_top_display_style_shift_up = try readValue(data, &cursor),
386         .stack_bottom_shift_down = try readValue(data, &cursor),
387         .stack_bottom_display_style_shift_down = try readValue(data, &cursor),
388         .stack_gap_min = try readValue(data, &cursor),
389         .stack_display_style_gap_min = try readValue(data, &cursor),
390         .stretch_stack_top_shift_up = try readValue(data, &cursor),
391         .stretch_stack_bottom_shift_down = try readValue(data, &cursor),
392         .stretch_stack_gap_above_min = try readValue(data, &cursor),
393         .stretch_stack_gap_below_min = try readValue(data, &cursor),
394         .fraction_numerator_shift_up = try readValue(data, &cursor),
395         .fraction_numerator_display_style_shift_up = try readValue(data, &cursor),
396         .fraction_denominator_shift_down = try readValue(data, &cursor),
397         .fraction_denominator_display_style_shift_down = try readValue(data, &cursor),
398         .fraction_numerator_gap_min = try readValue(data, &cursor),
399         .fraction_num_display_style_gap_min = try readValue(data, &cursor),
400         .fraction_rule_thickness = try readValue(data, &cursor),
401         .fraction_denominator_gap_min = try readValue(data, &cursor),
402         .fraction_denom_display_style_gap_min = try readValue(data, &cursor),
403         .skewed_fraction_horizontal_gap = try readValue(data, &cursor),
404         .skewed_fraction_vertical_gap = try readValue(data, &cursor),
405         .overbar_vertical_gap = try readValue(data, &cursor),
406         .overbar_rule_thickness = try readValue(data, &cursor),
407         .overbar_extra_ascender = try readValue(data, &cursor),
408         .underbar_vertical_gap = try readValue(data, &cursor),
409         .underbar_rule_thickness = try readValue(data, &cursor),
410         .underbar_extra_descender = try readValue(data, &cursor),
411         .radical_vertical_gap = try readValue(data, &cursor),
412         .radical_display_style_vertical_gap = try readValue(data, &cursor),
413         .radical_rule_thickness = try readValue(data, &cursor),
414         .radical_extra_ascender = try readValue(data, &cursor),
415         .radical_kern_before_degree = try readValue(data, &cursor),
416         .radical_kern_after_degree = try readValue(data, &cursor),
417         .radical_degree_bottom_raise_percent = try readI16(data, cursor),
418     };
419 }
420 
421 fn readValue(data: []const u8, cursor: *usize) FontError!ValueRecord {
422     const result = ValueRecord{
423         .value = try readI16(data, cursor.*),
424         .device_offset = try readU16(data, cursor.* + 2),
425     };
426     cursor.* += 4;
427     return result;
428 }
429 
430 fn mathChildOffset(table: Table, relative_offset: u16, min_len: usize) FontError!usize {
431     if (relative_offset == 0) return error.InvalidFont;
432     const offset: usize = relative_offset;
433     if (offset > table.len or min_len > table.len - offset) return error.InvalidFont;
434     return table.offset + offset;
435 }
436 
437 fn mathChildOffsetFrom(table: Table, parent_offset: usize, relative_offset: usize, min_len: usize) FontError!usize {
438     if (relative_offset == 0) return error.InvalidFont;
439     if (parent_offset < table.offset) return error.InvalidFont;
440     const parent_relative = parent_offset - table.offset;
441     if (parent_relative > table.len or relative_offset > table.len - parent_relative) return error.InvalidFont;
442     const child_relative = parent_relative + relative_offset;
443     if (min_len > table.len - child_relative) return error.InvalidFont;
444     return table.offset + child_relative;
445 }
446 
447 fn mathVariantsChildOffset(table: Table, variants_offset: usize, relative_offset: usize, min_len: usize) FontError!usize {
448     return mathChildOffsetFrom(table, variants_offset, relative_offset, min_len);
449 }
450 
451 fn mathConstructionChildOffset(table: Table, construction_offset: usize, relative_offset: usize, min_len: usize) FontError!usize {
452     return mathChildOffsetFrom(table, construction_offset, relative_offset, min_len);
453 }
454 
455 fn optionalCoverage(data: []const u8, table: Table, variants_offset: usize, relative_offset: u16, count: u16) FontError!?layout.Coverage {
456     if (count == 0) return null;
457     const coverage_offset = try mathVariantsChildOffset(table, variants_offset, relative_offset, 2);
458     return layout.Coverage.init(data, coverage_offset) catch error.InvalidFont;
459 }
460 
461 fn validateConstruction(data: []const u8, table: Table, construction_offset: usize) FontError!void {
462     const construction_relative = construction_offset - table.offset;
463     if (construction_relative > table.len or math_glyph_construction_header_size > table.len - construction_relative) return error.InvalidFont;
464     const assembly_relative = try readU16(data, construction_offset);
465     if (assembly_relative != 0) {
466         const assembly_offset = try mathConstructionChildOffset(table, construction_offset, assembly_relative, math_glyph_assembly_header_size);
467         try validateGlyphAssembly(data, table, assembly_offset);
468     }
469     _ = try constructionVariant(data, table, construction_offset, std.math.maxInt(u16));
470 }
471 
472 fn constructionVariant(data: []const u8, table: Table, construction_offset: usize, target_measurement: u16) FontError!?GlyphVariant {
473     const construction_relative = construction_offset - table.offset;
474     if (construction_relative > table.len or math_glyph_construction_header_size > table.len - construction_relative) return error.InvalidFont;
475     const variant_count = try readU16(data, construction_offset + 2);
476     const records_len = @as(usize, variant_count) * math_glyph_variant_size;
477     if (records_len > table.len - construction_relative - math_glyph_construction_header_size) return error.InvalidFont;
478     var fallback: ?GlyphVariant = null;
479     var best: ?GlyphVariant = null;
480     for (0..variant_count) |index| {
481         const record_offset = construction_offset + math_glyph_construction_header_size + index * math_glyph_variant_size;
482         const variant = GlyphVariant{
483             .glyph_id = try readU16(data, record_offset),
484             .advance_measurement = try readU16(data, record_offset + 2),
485         };
486         if (fallback == null or variant.advance_measurement > fallback.?.advance_measurement) fallback = variant;
487         if (variant.advance_measurement >= target_measurement and (best == null or variant.advance_measurement < best.?.advance_measurement)) {
488             best = variant;
489         }
490     }
491     return best orelse fallback;
492 }
493 
494 fn constructionAssemblyAlloc(allocator: std.mem.Allocator, data: []const u8, table: Table, construction_offset: usize, min_connector_overlap: u16, target_measurement: u16) AssemblyError!?GlyphAssembly {
495     const assembly_relative = try readU16(data, construction_offset);
496     if (assembly_relative == 0) return null;
497     const assembly_offset = try mathConstructionChildOffset(table, construction_offset, assembly_relative, math_glyph_assembly_header_size);
498     return try glyphAssemblyAlloc(allocator, data, table, assembly_offset, min_connector_overlap, target_measurement);
499 }
500 
501 fn validateGlyphAssembly(data: []const u8, table: Table, assembly_offset: usize) FontError!void {
502     const assembly_relative = assembly_offset - table.offset;
503     if (assembly_relative > table.len or math_glyph_assembly_header_size > table.len - assembly_relative) return error.InvalidFont;
504     const part_count = try readU16(data, assembly_offset + 4);
505     const records_len = @as(usize, part_count) * math_glyph_part_size;
506     if (records_len > table.len - assembly_relative - math_glyph_assembly_header_size) return error.InvalidFont;
507     for (0..part_count) |index| {
508         _ = try readGlyphPart(data, assembly_offset + math_glyph_assembly_header_size + index * math_glyph_part_size);
509     }
510 }
511 
512 fn glyphAssemblyAlloc(allocator: std.mem.Allocator, data: []const u8, table: Table, assembly_offset: usize, min_connector_overlap: u16, target_measurement: u16) AssemblyError!?GlyphAssembly {
513     try validateGlyphAssembly(data, table, assembly_offset);
514     const italics_correction = ValueRecord{
515         .value = try readI16(data, assembly_offset),
516         .device_offset = try readU16(data, assembly_offset + 2),
517     };
518     const part_count = try readU16(data, assembly_offset + 4);
519     if (part_count == 0) return null;
520     const records = try allocator.alloc(GlyphPartRecord, part_count);
521     defer allocator.free(records);
522     var has_extender = false;
523     for (records, 0..) |*record, index| {
524         record.* = try readGlyphPart(data, assembly_offset + math_glyph_assembly_header_size + index * math_glyph_part_size);
525         has_extender = has_extender or record.extender();
526     }
527 
528     var repeat_count: usize = 0;
529     while (true) : (repeat_count += 1) {
530         const sequence = try assemblySequenceAlloc(allocator, records, repeat_count);
531         defer allocator.free(sequence);
532         if (sequence.len == 0) {
533             if (!has_extender) return null;
534             continue;
535         }
536         const range = try assemblyRange(sequence, min_connector_overlap);
537         if (@as(i64, target_measurement) <= range.max_size) {
538             return try buildAssembly(allocator, sequence, min_connector_overlap, target_measurement, italics_correction);
539         }
540         if (!has_extender) return null;
541         if (assemblySequenceLen(records, repeat_count + 1) > max_assembly_parts) return error.InvalidFont;
542     }
543 }
544 
545 fn readGlyphPart(data: []const u8, offset: usize) FontError!GlyphPartRecord {
546     const part = GlyphPartRecord{
547         .glyph_id = try readU16(data, offset),
548         .start_connector_length = try readU16(data, offset + 2),
549         .end_connector_length = try readU16(data, offset + 4),
550         .full_advance = try readU16(data, offset + 6),
551         .flags = try readU16(data, offset + 8),
552     };
553     if (part.full_advance == 0) return error.InvalidFont;
554     if (part.start_connector_length > part.full_advance or part.end_connector_length > part.full_advance) return error.InvalidFont;
555     if (part.flags & 0xfffe != 0) return error.InvalidFont;
556     return part;
557 }
558 
559 fn assemblySequenceLen(records: []const GlyphPartRecord, repeat_count: usize) usize {
560     var len: usize = 0;
561     for (records) |record| {
562         len += if (record.extender()) repeat_count else 1;
563     }
564     return len;
565 }
566 
567 fn assemblySequenceAlloc(allocator: std.mem.Allocator, records: []const GlyphPartRecord, repeat_count: usize) AssemblyError![]GlyphPartRecord {
568     const len = assemblySequenceLen(records, repeat_count);
569     if (len > max_assembly_parts) return error.InvalidFont;
570     const sequence = try allocator.alloc(GlyphPartRecord, len);
571     var index: usize = 0;
572     for (records) |record| {
573         const count = if (record.extender()) repeat_count else 1;
574         for (0..count) |_| {
575             sequence[index] = record;
576             index += 1;
577         }
578     }
579     return sequence;
580 }
581 
582 fn assemblyRange(parts: []const GlyphPartRecord, min_connector_overlap: u16) FontError!AssemblyRange {
583     if (parts.len == 0) return error.InvalidFont;
584     var min_size: i64 = 0;
585     var max_size: i64 = 0;
586     for (parts) |part| {
587         min_size += part.full_advance;
588         max_size += part.full_advance;
589     }
590     for (0..parts.len - 1) |index| {
591         const connection = assemblyConnection(parts[index], parts[index + 1], min_connector_overlap);
592         min_size -= connection.max_overlap;
593         max_size -= connection.min_overlap;
594     }
595     if (min_size <= 0 or max_size < min_size) return error.InvalidFont;
596     return .{ .min_size = min_size, .max_size = max_size };
597 }
598 
599 fn buildAssembly(allocator: std.mem.Allocator, parts: []const GlyphPartRecord, min_connector_overlap: u16, target_measurement: u16, italics_correction: ValueRecord) AssemblyError!GlyphAssembly {
600     const range = try assemblyRange(parts, min_connector_overlap);
601     const connection_count = parts.len - 1;
602     const connections = try allocator.alloc(AssemblyConnection, connection_count);
603     defer allocator.free(connections);
604     for (connections, 0..) |*connection, index| {
605         connection.* = assemblyConnection(parts[index], parts[index + 1], min_connector_overlap);
606     }
607 
608     var requested_extra: i64 = 0;
609     if (@as(i64, target_measurement) > range.min_size) requested_extra = @as(i64, target_measurement) - range.min_size;
610     requested_extra = @min(requested_extra, range.max_size - range.min_size);
611     distributeConnectionExtra(connections, requested_extra);
612 
613     const out = try allocator.alloc(GlyphAssemblyPart, parts.len);
614     errdefer allocator.free(out);
615     var advance_offset: i64 = 0;
616     for (parts, 0..) |part, index| {
617         if (advance_offset < 0 or advance_offset > std.math.maxInt(u16)) return error.InvalidFont;
618         out[index] = .{
619             .glyph_id = part.glyph_id,
620             .advance_offset = @intCast(advance_offset),
621             .full_advance = part.full_advance,
622         };
623         if (index < connections.len) {
624             const overlap = @as(i64, connections[index].max_overlap) - connections[index].extra;
625             advance_offset += @as(i64, part.full_advance) - overlap;
626         }
627     }
628     const advance_measurement = advance_offset + parts[parts.len - 1].full_advance;
629     if (advance_measurement <= 0 or advance_measurement > std.math.maxInt(u16)) return error.InvalidFont;
630     return .{
631         .parts = out,
632         .advance_measurement = @intCast(advance_measurement),
633         .italics_correction = italics_correction,
634     };
635 }
636 
637 fn assemblyConnection(left: GlyphPartRecord, right: GlyphPartRecord, min_connector_overlap: u16) AssemblyConnection {
638     const max_overlap = @min(left.end_connector_length, right.start_connector_length);
639     return .{
640         .max_overlap = max_overlap,
641         .min_overlap = @min(min_connector_overlap, max_overlap),
642     };
643 }
644 
645 fn distributeConnectionExtra(connections: []AssemblyConnection, requested_extra: i64) void {
646     if (connections.len == 0 or requested_extra <= 0) return;
647     var remaining = requested_extra;
648     while (remaining > 0) {
649         var eligible: usize = 0;
650         for (connections) |connection| {
651             if (connection.extra < connection.capacity()) eligible += 1;
652         }
653         if (eligible == 0) return;
654         var step = @divTrunc(remaining, @as(i64, @intCast(eligible)));
655         if (step <= 0) step = 1;
656         for (connections) |*connection| {
657             if (remaining <= 0) return;
658             const capacity = connection.capacity();
659             if (connection.extra >= capacity) continue;
660             const available = capacity - connection.extra;
661             const add: u16 = @intCast(@min(@as(i64, available), step));
662             connection.extra += add;
663             remaining -= add;
664         }
665     }
666 }
667 
668 fn writeU16(bytes: []u8, offset: usize, value: u16) void {
669     std.mem.writeInt(u16, bytes[offset..][0..2], value, .big);
670 }
671 
672 fn writeI16(bytes: []u8, offset: usize, value: i16) void {
673     std.mem.writeInt(i16, bytes[offset..][0..2], value, .big);
674 }
675 
676 fn writeValue(bytes: []u8, constants_offset: usize, field_index: usize, value: i16, device_offset: u16) void {
677     const offset = constants_offset + 8 + (field_index - 1) * 4;
678     writeI16(bytes, offset, value);
679     writeU16(bytes, offset + 2, device_offset);
680 }
681 
682 fn writeVariant(bytes: []u8, offset: usize, glyph_id: u16, advance_measurement: u16) void {
683     writeU16(bytes, offset, glyph_id);
684     writeU16(bytes, offset + 2, advance_measurement);
685 }
686 
687 fn writePart(bytes: []u8, offset: usize, glyph_id: u16, start_connector_length: u16, end_connector_length: u16, full_advance: u16, flags: u16) void {
688     writeU16(bytes, offset, glyph_id);
689     writeU16(bytes, offset + 2, start_connector_length);
690     writeU16(bytes, offset + 4, end_connector_length);
691     writeU16(bytes, offset + 6, full_advance);
692     writeU16(bytes, offset + 8, flags);
693 }
694 
695 test "MATH constants parse script, fraction, and radical metrics" {
696     var bytes = @as([(math_header_size + math_constants_size)]u8, @splat(0));
697     writeU16(&bytes, 0, 1);
698     writeU16(&bytes, 2, 0);
699     writeU16(&bytes, 4, math_header_size);
700     const constants_offset = math_header_size;
701     writeI16(&bytes, constants_offset, 80);
702     writeI16(&bytes, constants_offset + 2, 60);
703     writeU16(&bytes, constants_offset + 4, 1500);
704     writeU16(&bytes, constants_offset + 6, 2000);
705     writeValue(&bytes, constants_offset, 6, 12, 0);
706     writeValue(&bytes, constants_offset, 7, 34, 0);
707     writeValue(&bytes, constants_offset, 11, 9, 0);
708     writeValue(&bytes, constants_offset, 13, 18, 0);
709     writeValue(&bytes, constants_offset, 20, 61, 0);
710     writeValue(&bytes, constants_offset, 22, 62, 0);
711     writeValue(&bytes, constants_offset, 24, 63, 0);
712     writeValue(&bytes, constants_offset, 35, 55, 7);
713     writeValue(&bytes, constants_offset, 41, 44, 0);
714     writeValue(&bytes, constants_offset, 46, 120, 0);
715     writeValue(&bytes, constants_offset, 48, 50, 0);
716     writeValue(&bytes, constants_offset, 51, -90, 0);
717     writeI16(&bytes, constants_offset + math_constants_size - 2, 65);
718 
719     const parsed = try Math.init(&bytes, .{ .offset = 0, .len = bytes.len });
720 
721     try std.testing.expectEqual(@as(i16, 80), parsed.constants.script_percent_scale_down);
722     try std.testing.expectEqual(@as(i16, 60), parsed.constants.script_script_percent_scale_down);
723     try std.testing.expectEqual(@as(u16, 1500), parsed.constants.delimited_sub_formula_min_height);
724     try std.testing.expectEqual(@as(u16, 2000), parsed.constants.display_operator_min_height);
725     try std.testing.expectEqual(@as(i16, 12), parsed.constants.subscript_top_max.value);
726     try std.testing.expectEqual(@as(i16, 34), parsed.constants.subscript_baseline_drop_min.value);
727     try std.testing.expectEqual(@as(i16, 9), parsed.constants.superscript_baseline_drop_max.value);
728     try std.testing.expectEqual(@as(i16, 18), parsed.constants.superscript_bottom_max_with_subscript.value);
729     try std.testing.expectEqual(@as(i16, 61), parsed.constants.stack_top_display_style_shift_up.value);
730     try std.testing.expectEqual(@as(i16, 62), parsed.constants.stack_bottom_display_style_shift_down.value);
731     try std.testing.expectEqual(@as(i16, 63), parsed.constants.stack_display_style_gap_min.value);
732     try std.testing.expectEqual(@as(i16, 55), parsed.constants.fraction_rule_thickness.value);
733     try std.testing.expectEqual(@as(u16, 7), parsed.constants.fraction_rule_thickness.device_offset);
734     try std.testing.expectEqual(@as(i16, 44), parsed.constants.overbar_rule_thickness.value);
735     try std.testing.expectEqual(@as(i16, 120), parsed.constants.radical_vertical_gap.value);
736     try std.testing.expectEqual(@as(i16, 50), parsed.constants.radical_rule_thickness.value);
737     try std.testing.expectEqual(@as(i16, -90), parsed.constants.radical_kern_after_degree.value);
738     try std.testing.expectEqual(@as(i16, 65), parsed.constants.radical_degree_bottom_raise_percent);
739 }
740 
741 test "MATH constants reject truncated table" {
742     var bytes = @as([math_header_size]u8, @splat(0));
743     writeU16(&bytes, 0, 1);
744     writeU16(&bytes, 2, 0);
745     writeU16(&bytes, 4, math_header_size);
746 
747     try std.testing.expectError(error.InvalidFont, Math.init(&bytes, .{ .offset = 0, .len = bytes.len }));
748 }
749 
750 test "MATH glyph info exposes top accent attachments" {
751     var bytes = @as([(math_header_size + math_constants_size + math_glyph_info_header_size + 32)]u8, @splat(0));
752     writeU16(&bytes, 0, 1);
753     writeU16(&bytes, 2, 0);
754     writeU16(&bytes, 4, math_header_size);
755     const glyph_info_offset = math_header_size + math_constants_size;
756     writeU16(&bytes, 6, glyph_info_offset);
757     writeU16(&bytes, glyph_info_offset + 2, math_glyph_info_header_size);
758     const top_offset = glyph_info_offset + math_glyph_info_header_size;
759     writeU16(&bytes, top_offset, 12);
760     writeU16(&bytes, top_offset + 2, 2);
761     writeI16(&bytes, top_offset + 4, 275);
762     writeU16(&bytes, top_offset + 6, 0);
763     writeI16(&bytes, top_offset + 8, 125);
764     writeU16(&bytes, top_offset + 10, 0);
765     writeU16(&bytes, top_offset + 12, 1);
766     writeU16(&bytes, top_offset + 14, 2);
767     writeU16(&bytes, top_offset + 16, 40);
768     writeU16(&bytes, top_offset + 18, 60);
769 
770     const parsed = try Math.init(&bytes, .{ .offset = 0, .len = bytes.len });
771     const first = (try parsed.topAccentAttachment(&bytes, 40)).?;
772     const second = (try parsed.topAccentAttachment(&bytes, 60)).?;
773 
774     try std.testing.expectEqual(@as(i16, 275), first.value);
775     try std.testing.expectEqual(@as(i16, 125), second.value);
776     try std.testing.expectEqual(@as(?ValueRecord, null), try parsed.topAccentAttachment(&bytes, 41));
777 }
778 
779 test "MATH variants choose ready-made vertical glyph by target measurement" {
780     var bytes = @as([(math_header_size + math_constants_size + 64)]u8, @splat(0));
781     writeU16(&bytes, 0, 1);
782     writeU16(&bytes, 2, 0);
783     writeU16(&bytes, 4, math_header_size);
784     const variants_offset = math_header_size + math_constants_size;
785     writeU16(&bytes, 8, variants_offset);
786     writeU16(&bytes, variants_offset, 5);
787     writeU16(&bytes, variants_offset + 2, 12);
788     writeU16(&bytes, variants_offset + 4, 0);
789     writeU16(&bytes, variants_offset + 6, 1);
790     writeU16(&bytes, variants_offset + 8, 0);
791     writeU16(&bytes, variants_offset + 10, 18);
792     writeU16(&bytes, variants_offset + 12, 1);
793     writeU16(&bytes, variants_offset + 14, 1);
794     writeU16(&bytes, variants_offset + 16, 40);
795     writeU16(&bytes, variants_offset + 18, 0);
796     writeU16(&bytes, variants_offset + 20, 3);
797     writeVariant(&bytes, variants_offset + 22, 41, 700);
798     writeVariant(&bytes, variants_offset + 26, 42, 1000);
799     writeVariant(&bytes, variants_offset + 30, 43, 1300);
800 
801     const parsed = try Math.init(&bytes, .{ .offset = 0, .len = bytes.len });
802     const exact = (try parsed.verticalVariant(&bytes, 40, 1000)).?;
803     const middle = (try parsed.verticalVariant(&bytes, 40, 800)).?;
804     const too_large = (try parsed.verticalVariant(&bytes, 40, 2000)).?;
805 
806     try std.testing.expectEqual(@as(u16, 42), exact.glyph_id);
807     try std.testing.expectEqual(@as(u16, 1000), exact.advance_measurement);
808     try std.testing.expectEqual(@as(u16, 42), middle.glyph_id);
809     try std.testing.expectEqual(@as(u16, 43), too_large.glyph_id);
810     try std.testing.expectEqual(@as(?GlyphVariant, null), try parsed.verticalVariant(&bytes, 39, 800));
811 }
812 
813 test "MATH variants choose ready-made horizontal glyph by target measurement" {
814     var bytes = @as([(math_header_size + math_constants_size + 64)]u8, @splat(0));
815     writeU16(&bytes, 0, 1);
816     writeU16(&bytes, 2, 0);
817     writeU16(&bytes, 4, math_header_size);
818     const variants_offset = math_header_size + math_constants_size;
819     writeU16(&bytes, 8, variants_offset);
820     writeU16(&bytes, variants_offset, 5);
821     writeU16(&bytes, variants_offset + 2, 0);
822     writeU16(&bytes, variants_offset + 4, 12);
823     writeU16(&bytes, variants_offset + 6, 0);
824     writeU16(&bytes, variants_offset + 8, 1);
825     writeU16(&bytes, variants_offset + 10, 18);
826     writeU16(&bytes, variants_offset + 12, 1);
827     writeU16(&bytes, variants_offset + 14, 1);
828     writeU16(&bytes, variants_offset + 16, 40);
829     writeU16(&bytes, variants_offset + 18, 0);
830     writeU16(&bytes, variants_offset + 20, 3);
831     writeVariant(&bytes, variants_offset + 22, 41, 700);
832     writeVariant(&bytes, variants_offset + 26, 42, 1000);
833     writeVariant(&bytes, variants_offset + 30, 43, 1300);
834 
835     const parsed = try Math.init(&bytes, .{ .offset = 0, .len = bytes.len });
836     const exact = (try parsed.horizontalVariant(&bytes, 40, 1000)).?;
837     const middle = (try parsed.horizontalVariant(&bytes, 40, 800)).?;
838     const too_large = (try parsed.horizontalVariant(&bytes, 40, 2000)).?;
839 
840     try std.testing.expectEqual(@as(u16, 42), exact.glyph_id);
841     try std.testing.expectEqual(@as(u16, 1000), exact.advance_measurement);
842     try std.testing.expectEqual(@as(u16, 42), middle.glyph_id);
843     try std.testing.expectEqual(@as(u16, 43), too_large.glyph_id);
844     try std.testing.expectEqual(@as(?GlyphVariant, null), try parsed.horizontalVariant(&bytes, 39, 800));
845 }
846 
847 test "MATH assembly repeats extenders and distributes connector overlap" {
848     var bytes = @as([(math_header_size + math_constants_size + 128)]u8, @splat(0));
849     writeU16(&bytes, 0, 1);
850     writeU16(&bytes, 2, 0);
851     writeU16(&bytes, 4, math_header_size);
852     const variants_offset = math_header_size + math_constants_size;
853     writeU16(&bytes, 8, variants_offset);
854     writeU16(&bytes, variants_offset, 50);
855     writeU16(&bytes, variants_offset + 2, 12);
856     writeU16(&bytes, variants_offset + 4, 0);
857     writeU16(&bytes, variants_offset + 6, 1);
858     writeU16(&bytes, variants_offset + 8, 0);
859     writeU16(&bytes, variants_offset + 10, 18);
860     writeU16(&bytes, variants_offset + 12, 1);
861     writeU16(&bytes, variants_offset + 14, 1);
862     writeU16(&bytes, variants_offset + 16, 40);
863     writeU16(&bytes, variants_offset + 18, 4);
864     writeU16(&bytes, variants_offset + 20, 0);
865     writeI16(&bytes, variants_offset + 22, 0);
866     writeU16(&bytes, variants_offset + 24, 0);
867     writeU16(&bytes, variants_offset + 26, 3);
868     writePart(&bytes, variants_offset + 28, 50, 0, 200, 500, 0);
869     writePart(&bytes, variants_offset + 38, 51, 200, 200, 300, glyph_part_extender_flag);
870     writePart(&bytes, variants_offset + 48, 52, 200, 0, 500, 0);
871 
872     const parsed = try Math.init(&bytes, .{ .offset = 0, .len = bytes.len });
873     var assembly = (try parsed.verticalAssemblyAlloc(std.testing.allocator, &bytes, 40, 1300)).?;
874     defer assembly.deinit(std.testing.allocator);
875 
876     try std.testing.expectEqual(@as(u16, 1300), assembly.advance_measurement);
877     try std.testing.expectEqual(@as(usize, 4), assembly.parts.len);
878     try std.testing.expectEqual(@as(u16, 50), assembly.parts[0].glyph_id);
879     try std.testing.expectEqual(@as(u16, 0), assembly.parts[0].advance_offset);
880     try std.testing.expectEqual(@as(u16, 51), assembly.parts[1].glyph_id);
881     try std.testing.expectEqual(@as(u16, 400), assembly.parts[1].advance_offset);
882     try std.testing.expectEqual(@as(u16, 51), assembly.parts[2].glyph_id);
883     try std.testing.expectEqual(@as(u16, 600), assembly.parts[2].advance_offset);
884     try std.testing.expectEqual(@as(u16, 52), assembly.parts[3].glyph_id);
885     try std.testing.expectEqual(@as(u16, 800), assembly.parts[3].advance_offset);
886 }
887 
888 test "MATH horizontal assembly repeats extenders and distributes connector overlap" {
889     var bytes = @as([(math_header_size + math_constants_size + 128)]u8, @splat(0));
890     writeU16(&bytes, 0, 1);
891     writeU16(&bytes, 2, 0);
892     writeU16(&bytes, 4, math_header_size);
893     const variants_offset = math_header_size + math_constants_size;
894     writeU16(&bytes, 8, variants_offset);
895     writeU16(&bytes, variants_offset, 50);
896     writeU16(&bytes, variants_offset + 2, 0);
897     writeU16(&bytes, variants_offset + 4, 12);
898     writeU16(&bytes, variants_offset + 6, 0);
899     writeU16(&bytes, variants_offset + 8, 1);
900     writeU16(&bytes, variants_offset + 10, 18);
901     writeU16(&bytes, variants_offset + 12, 1);
902     writeU16(&bytes, variants_offset + 14, 1);
903     writeU16(&bytes, variants_offset + 16, 40);
904     writeU16(&bytes, variants_offset + 18, 4);
905     writeU16(&bytes, variants_offset + 20, 0);
906     writeI16(&bytes, variants_offset + 22, 0);
907     writeU16(&bytes, variants_offset + 24, 0);
908     writeU16(&bytes, variants_offset + 26, 3);
909     writePart(&bytes, variants_offset + 28, 50, 0, 200, 500, 0);
910     writePart(&bytes, variants_offset + 38, 51, 200, 200, 300, glyph_part_extender_flag);
911     writePart(&bytes, variants_offset + 48, 52, 200, 0, 500, 0);
912 
913     const parsed = try Math.init(&bytes, .{ .offset = 0, .len = bytes.len });
914     var assembly = (try parsed.horizontalAssemblyAlloc(std.testing.allocator, &bytes, 40, 1300)).?;
915     defer assembly.deinit(std.testing.allocator);
916 
917     try std.testing.expectEqual(@as(u16, 1300), assembly.advance_measurement);
918     try std.testing.expectEqual(@as(usize, 4), assembly.parts.len);
919     try std.testing.expectEqual(@as(u16, 50), assembly.parts[0].glyph_id);
920     try std.testing.expectEqual(@as(u16, 0), assembly.parts[0].advance_offset);
921     try std.testing.expectEqual(@as(u16, 51), assembly.parts[1].glyph_id);
922     try std.testing.expectEqual(@as(u16, 400), assembly.parts[1].advance_offset);
923     try std.testing.expectEqual(@as(u16, 51), assembly.parts[2].glyph_id);
924     try std.testing.expectEqual(@as(u16, 600), assembly.parts[2].advance_offset);
925     try std.testing.expectEqual(@as(u16, 52), assembly.parts[3].glyph_id);
926     try std.testing.expectEqual(@as(u16, 800), assembly.parts[3].advance_offset);
927 }