lib/filigree/src/font/variations.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const binary = @import("binary.zig");
  3 const model = @import("model.zig");
  4 
  5 const FontError = model.FontError;
  6 const Table = binary.Table;
  7 const Tag = model.Tag;
  8 const VariationAxis = model.VariationAxis;
  9 const VariationSetting = model.VariationSetting;
 10 const readI8 = binary.readI8;
 11 const readI16 = binary.readI16;
 12 const readI32 = binary.readI32;
 13 const readU16 = binary.readU16;
 14 const readU32 = binary.readU32;
 15 
 16 pub const Fvar = struct {
 17     table: Table,
 18     axes_offset: usize,
 19     axis_count: u16,
 20     axis_size: u16,
 21 
 22     pub fn init(data: []const u8, fvar_table: Table) FontError!Fvar {
 23         if (fvar_table.len < 16) return error.InvalidVariations;
 24         const major = try readU16(data, fvar_table.offset);
 25         const minor = try readU16(data, fvar_table.offset + 2);
 26         if (major != 1 or minor != 0) return error.InvalidVariations;
 27         const axes_array_offset = try readU16(data, fvar_table.offset + 4);
 28         const axis_count = try readU16(data, fvar_table.offset + 8);
 29         const axis_size = try readU16(data, fvar_table.offset + 10);
 30         const instance_count = try readU16(data, fvar_table.offset + 12);
 31         const instance_size = try readU16(data, fvar_table.offset + 14);
 32         if (axis_count == 0 or axis_size < 20) return error.InvalidVariations;
 33         if (axes_array_offset > fvar_table.len) return error.InvalidVariations;
 34         const axes_offset = fvar_table.offset + axes_array_offset;
 35         const axes_len = @as(usize, axis_count) * @as(usize, axis_size);
 36         if (axes_len > fvar_table.len - axes_array_offset) return error.InvalidVariations;
 37         const instances_offset = axes_array_offset + axes_len;
 38         const minimum_instance_size = @as(usize, axis_count) * 4 + 4;
 39         if (instance_count != 0 and instance_size < minimum_instance_size) return error.InvalidVariations;
 40         const instances_len = @as(usize, instance_count) * @as(usize, instance_size);
 41         if (instances_len > fvar_table.len - instances_offset) return error.InvalidVariations;
 42 
 43         var result = Fvar{
 44             .table = fvar_table,
 45             .axes_offset = axes_offset,
 46             .axis_count = axis_count,
 47             .axis_size = axis_size,
 48         };
 49         for (0..axis_count) |axis_index| {
 50             _ = try result.axis(data, axis_index);
 51         }
 52         return result;
 53     }
 54 
 55     pub fn axis(self: Fvar, data: []const u8, axis_index: usize) FontError!VariationAxis {
 56         if (axis_index >= self.axis_count) return error.InvalidVariations;
 57         const offset = self.axes_offset + axis_index * @as(usize, self.axis_size);
 58         const variation_axis = VariationAxis{
 59             .tag = try readU32(data, offset),
 60             .min_value = try readI32(data, offset + 4),
 61             .default_value = try readI32(data, offset + 8),
 62             .max_value = try readI32(data, offset + 12),
 63             .flags = try readU16(data, offset + 16),
 64             .name_id = try readU16(data, offset + 18),
 65         };
 66         if (variation_axis.min_value > variation_axis.default_value or variation_axis.default_value > variation_axis.max_value) return error.InvalidVariations;
 67         return variation_axis;
 68     }
 69 
 70     pub fn axisIndex(self: Fvar, data: []const u8, axis_tag: Tag) FontError!?usize {
 71         for (0..self.axis_count) |axis_index| {
 72             const variation_axis = try self.axis(data, axis_index);
 73             if (variation_axis.tag == axis_tag) return axis_index;
 74         }
 75         return null;
 76     }
 77 };
 78 
 79 pub const Avar = struct {
 80     table: Table,
 81     axis_count: u16,
 82 
 83     pub fn init(data: []const u8, avar_table: Table, expected_axis_count: u16) FontError!Avar {
 84         if (avar_table.len < 8) return error.InvalidVariations;
 85         const major = try readU16(data, avar_table.offset);
 86         const minor = try readU16(data, avar_table.offset + 2);
 87         if (major != 1 or minor != 0) return error.InvalidVariations;
 88         const axis_count = try readU16(data, avar_table.offset + 6);
 89         if (axis_count != expected_axis_count) return error.InvalidVariations;
 90         var cursor = avar_table.offset + 8;
 91         const end = avar_table.offset + avar_table.len;
 92         for (0..axis_count) |_| {
 93             if (cursor > end or end - cursor < 2) return error.InvalidVariations;
 94             const position_map_count = try readU16(data, cursor);
 95             cursor += 2;
 96             const records_len = @as(usize, position_map_count) * 4;
 97             if (records_len > end - cursor) return error.InvalidVariations;
 98             cursor += records_len;
 99         }
100         return .{ .table = avar_table, .axis_count = axis_count };
101     }
102 
103     pub fn map(self: Avar, data: []const u8, axis_index: usize, value: i32) FontError!i32 {
104         const map_offset = try self.segmentMapOffset(data, axis_index);
105         const count = try readU16(data, map_offset);
106         if (count < 3) return value;
107         const records = map_offset + 2;
108         if (!try avarHasRequiredMaps(data, records, count)) return value;
109 
110         var previous_valid: ?AvarMap = null;
111         var start: ?AvarMap = null;
112         var end: ?AvarMap = null;
113         for (0..count) |map_index| {
114             const map_record = try avarMapAt(data, records, map_index);
115             if (previous_valid) |previous| {
116                 if (map_record.from <= previous.from or map_record.to < previous.to) continue;
117             }
118             previous_valid = map_record;
119             if (map_record.from <= value) start = map_record;
120             if (map_record.from >= value) {
121                 end = map_record;
122                 break;
123             }
124         }
125 
126         const start_map = start orelse return value;
127         const end_map = end orelse return value;
128         if (start_map.from == value) return start_map.to;
129         if (end_map.from == value) return end_map.to;
130         if (start_map.from == end_map.from) return value;
131         const numerator = @as(i64, value - start_map.from) * @as(i64, end_map.to - start_map.to);
132         const denominator = @as(i64, end_map.from - start_map.from);
133         const mapped = @as(i64, start_map.to) + @divTrunc(numerator, denominator);
134         return clampFixed16(mapped);
135     }
136 
137     fn segmentMapOffset(self: Avar, data: []const u8, axis_index: usize) FontError!usize {
138         if (axis_index >= self.axis_count) return error.InvalidVariations;
139         var cursor = self.table.offset + 8;
140         for (0..axis_index) |_| {
141             const count = try readU16(data, cursor);
142             cursor += 2 + @as(usize, count) * 4;
143         }
144         return cursor;
145     }
146 };
147 
148 const AvarMap = struct {
149     from: i32,
150     to: i32,
151 };
152 
153 pub const DeltaSetIndex = struct {
154     outer: u32,
155     inner: u32,
156 };
157 
158 pub const Hvar = struct {
159     store: ItemVariationStore,
160     advance_map: ?DeltaSetIndexMap,
161 
162     pub fn init(data: []const u8, hvar_table: Table, axis_count: u16) FontError!Hvar {
163         if (hvar_table.len < 20) return error.InvalidVariations;
164         const major = try readU16(data, hvar_table.offset);
165         const minor = try readU16(data, hvar_table.offset + 2);
166         if (major != 1 or minor != 0) return error.InvalidVariations;
167 
168         const store_table = try variationChildTable(hvar_table, try readU32(data, hvar_table.offset + 4), 8);
169         const store = try ItemVariationStore.init(data, store_table, axis_count);
170 
171         const advance_map = try optionalDeltaSetIndexMap(data, hvar_table, try readU32(data, hvar_table.offset + 8));
172         _ = try optionalDeltaSetIndexMap(data, hvar_table, try readU32(data, hvar_table.offset + 12));
173         _ = try optionalDeltaSetIndexMap(data, hvar_table, try readU32(data, hvar_table.offset + 16));
174 
175         return .{
176             .store = store,
177             .advance_map = advance_map,
178         };
179     }
180 
181     pub fn advanceWidthDelta(self: Hvar, data: []const u8, face: anytype, settings: []const VariationSetting, glyph_id: u32) FontError!i32 {
182         const delta_index = if (self.advance_map) |mapping|
183             try mapping.index(data, glyph_id)
184         else
185             DeltaSetIndex{ .outer = 0, .inner = glyph_id };
186         return try self.store.delta(data, face, settings, delta_index);
187     }
188 };
189 
190 pub const Vvar = struct {
191     store: ItemVariationStore,
192     advance_map: ?DeltaSetIndexMap,
193 
194     pub fn init(data: []const u8, vvar_table: Table, axis_count: u16) FontError!Vvar {
195         if (vvar_table.len < 24) return error.InvalidVariations;
196         const major = try readU16(data, vvar_table.offset);
197         const minor = try readU16(data, vvar_table.offset + 2);
198         if (major != 1 or minor != 0) return error.InvalidVariations;
199 
200         const store_table = try variationChildTable(vvar_table, try readU32(data, vvar_table.offset + 4), 8);
201         const store = try ItemVariationStore.init(data, store_table, axis_count);
202 
203         const advance_map = try optionalDeltaSetIndexMap(data, vvar_table, try readU32(data, vvar_table.offset + 8));
204         _ = try optionalDeltaSetIndexMap(data, vvar_table, try readU32(data, vvar_table.offset + 12));
205         _ = try optionalDeltaSetIndexMap(data, vvar_table, try readU32(data, vvar_table.offset + 16));
206         _ = try optionalDeltaSetIndexMap(data, vvar_table, try readU32(data, vvar_table.offset + 20));
207 
208         return .{
209             .store = store,
210             .advance_map = advance_map,
211         };
212     }
213 
214     pub fn advanceHeightDelta(self: Vvar, data: []const u8, face: anytype, settings: []const VariationSetting, glyph_id: u32) FontError!i32 {
215         const delta_index = if (self.advance_map) |mapping|
216             try mapping.index(data, glyph_id)
217         else
218             DeltaSetIndex{ .outer = 0, .inner = glyph_id };
219         return try self.store.delta(data, face, settings, delta_index);
220     }
221 };
222 
223 pub const Mvar = struct {
224     store: ?ItemVariationStore,
225     records_offset: usize,
226     value_record_size: u16,
227     value_record_count: u16,
228 
229     pub fn init(data: []const u8, mvar_table: Table, axis_count: u16) FontError!Mvar {
230         if (mvar_table.len < 12) return error.InvalidVariations;
231         const major = try readU16(data, mvar_table.offset);
232         const minor = try readU16(data, mvar_table.offset + 2);
233         const reserved = try readU16(data, mvar_table.offset + 4);
234         if (major != 1 or minor != 0 or reserved != 0) return error.InvalidVariations;
235 
236         const value_record_size = try readU16(data, mvar_table.offset + 6);
237         const value_record_count = try readU16(data, mvar_table.offset + 8);
238         const store_offset = try readU16(data, mvar_table.offset + 10);
239         if (value_record_size < 8) return error.InvalidVariations;
240 
241         const records_len = @as(u64, value_record_count) * @as(u64, value_record_size);
242         if (records_len > @as(u64, mvar_table.len - 12)) return error.InvalidVariations;
243         const records_len_usize: usize = @intCast(records_len);
244 
245         const store = if (value_record_count == 0) blk: {
246             if (store_offset != 0) return error.InvalidVariations;
247             break :blk null;
248         } else blk: {
249             if (store_offset == 0) return error.InvalidVariations;
250             const store_offset_usize: usize = store_offset;
251             if (store_offset_usize < 12 + records_len_usize) return error.InvalidVariations;
252             break :blk try ItemVariationStore.init(
253                 data,
254                 try variationChildTable(mvar_table, store_offset, 8),
255                 axis_count,
256             );
257         };
258 
259         const result = Mvar{
260             .store = store,
261             .records_offset = mvar_table.offset + 12,
262             .value_record_size = value_record_size,
263             .value_record_count = value_record_count,
264         };
265         var previous: ?Tag = null;
266         for (0..value_record_count) |record_index| {
267             const current = try readU32(data, result.recordOffset(record_index));
268             if (previous) |previous_tag| {
269                 if (current <= previous_tag) return error.InvalidVariations;
270             }
271             previous = current;
272         }
273         return result;
274     }
275 
276     pub fn valueDelta(self: Mvar, data: []const u8, face: anytype, settings: []const VariationSetting, metric_tag: Tag) FontError!i32 {
277         const store = self.store orelse return 0;
278         var left: usize = 0;
279         var right: usize = self.value_record_count;
280         while (left < right) {
281             const mid = left + (right - left) / 2;
282             const offset = self.recordOffset(mid);
283             const current = try readU32(data, offset);
284             if (metric_tag < current) {
285                 right = mid;
286             } else if (metric_tag > current) {
287                 left = mid + 1;
288             } else {
289                 return try store.delta(data, face, settings, .{
290                     .outer = try readU16(data, offset + 4),
291                     .inner = try readU16(data, offset + 6),
292                 });
293             }
294         }
295         return 0;
296     }
297 
298     fn recordOffset(self: Mvar, record_index: usize) usize {
299         return self.records_offset + record_index * @as(usize, self.value_record_size);
300     }
301 };
302 
303 const DeltaSetIndexMap = struct {
304     table: Table,
305     map_count: u32,
306     data_offset: usize,
307     entry_size: u8,
308     inner_bit_count: u5,
309 
310     pub fn init(data: []const u8, map_table: Table) FontError!DeltaSetIndexMap {
311         if (map_table.len < 4) return error.InvalidVariations;
312         const format = data[map_table.offset];
313         const entry_format = data[map_table.offset + 1];
314         if ((entry_format & 0xc0) != 0) return error.InvalidVariations;
315         const entry_size: u8 = ((entry_format >> 4) & 0x03) + 1;
316         const inner_bit_count: u5 = @intCast((entry_format & 0x0f) + 1);
317 
318         const header_len: usize = switch (format) {
319             0 => 4,
320             1 => 6,
321             else => return error.InvalidVariations,
322         };
323         if (map_table.len < header_len) return error.InvalidVariations;
324         const map_count = switch (format) {
325             0 => @as(u32, try readU16(data, map_table.offset + 2)),
326             1 => try readU32(data, map_table.offset + 2),
327             else => unreachable,
328         };
329         if (map_count == 0) return error.InvalidVariations;
330         if (@as(u64, map_count) * @as(u64, entry_size) > @as(u64, map_table.len - header_len)) return error.InvalidVariations;
331 
332         return .{
333             .table = map_table,
334             .map_count = map_count,
335             .data_offset = map_table.offset + header_len,
336             .entry_size = entry_size,
337             .inner_bit_count = inner_bit_count,
338         };
339     }
340 
341     pub fn index(self: DeltaSetIndexMap, data: []const u8, glyph_id: u32) FontError!DeltaSetIndex {
342         const entry_index = if (glyph_id < self.map_count) glyph_id else self.map_count - 1;
343         const entry_offset = self.data_offset + @as(usize, @intCast(entry_index)) * @as(usize, self.entry_size);
344         if (entry_offset < self.table.offset or entry_offset > self.table.offset + self.table.len or @as(usize, self.entry_size) > self.table.offset + self.table.len - entry_offset) return error.InvalidVariations;
345         var packed_index: u32 = 0;
346         for (0..self.entry_size) |byte_index| {
347             packed_index = (packed_index << 8) | @as(u32, data[entry_offset + byte_index]);
348         }
349         const inner_mask = (@as(u32, 1) << self.inner_bit_count) - 1;
350         return .{
351             .outer = packed_index >> self.inner_bit_count,
352             .inner = packed_index & inner_mask,
353         };
354     }
355 };
356 
357 pub const ItemVariationStore = struct {
358     table: Table,
359     region_list: VariationRegionList,
360     data_count: u16,
361     data_offsets_offset: usize,
362 
363     pub fn init(data: []const u8, store_table: Table, axis_count: u16) FontError!ItemVariationStore {
364         if (store_table.len < 8) return error.InvalidVariations;
365         if (try readU16(data, store_table.offset) != 1) return error.InvalidVariations;
366         const region_list = try VariationRegionList.init(
367             data,
368             try variationChildTable(store_table, try readU32(data, store_table.offset + 2), 4),
369             axis_count,
370         );
371         const data_count = try readU16(data, store_table.offset + 6);
372         if (@as(u64, data_count) * 4 > @as(u64, store_table.len - 8)) return error.InvalidVariations;
373 
374         const result = ItemVariationStore{
375             .table = store_table,
376             .region_list = region_list,
377             .data_count = data_count,
378             .data_offsets_offset = store_table.offset + 8,
379         };
380         for (0..data_count) |data_index| {
381             const item_data = try result.itemVariationData(data, data_index);
382             if (item_data) |subtable| _ = try ItemVariationData.init(data, subtable, region_list.region_count);
383         }
384         return result;
385     }
386 
387     pub fn delta(self: ItemVariationStore, data: []const u8, face: anytype, settings: []const VariationSetting, index: DeltaSetIndex) FontError!i32 {
388         if (index.outer >= self.data_count) return 0;
389         const item_data_table = (try self.itemVariationData(data, @intCast(index.outer))) orelse return 0;
390         const item_data = try ItemVariationData.init(data, item_data_table, self.region_list.region_count);
391         return try item_data.delta(data, self.region_list, face, settings, index.inner);
392     }
393 
394     pub fn itemVariationData(self: ItemVariationStore, data: []const u8, data_index: usize) FontError!?Table {
395         if (data_index >= self.data_count) return null;
396         const relative_offset = try readU32(data, self.data_offsets_offset + data_index * 4);
397         if (relative_offset == 0) return null;
398         return try variationChildTable(self.table, relative_offset, 6);
399     }
400 };
401 
402 const VariationRegionList = struct {
403     table: Table,
404     axis_count: u16,
405     region_count: u16,
406 
407     pub fn init(data: []const u8, region_table: Table, expected_axis_count: u16) FontError!VariationRegionList {
408         if (region_table.len < 4) return error.InvalidVariations;
409         const axis_count = try readU16(data, region_table.offset);
410         const region_count = try readU16(data, region_table.offset + 2);
411         if (axis_count != expected_axis_count) return error.InvalidVariations;
412         if ((region_count & 0x8000) != 0) return error.InvalidVariations;
413         const region_record_len = @as(usize, axis_count) * 6;
414         if (@as(u64, region_count) * @as(u64, region_record_len) > @as(u64, region_table.len - 4)) return error.InvalidVariations;
415         return .{
416             .table = region_table,
417             .axis_count = axis_count,
418             .region_count = region_count,
419         };
420     }
421 
422     pub fn scalar(self: VariationRegionList, data: []const u8, face: anytype, settings: []const VariationSetting, region_index: u16) FontError!i32 {
423         if (region_index >= self.region_count) return error.InvalidVariations;
424         const region_offset = self.table.offset + 4 + @as(usize, region_index) * @as(usize, self.axis_count) * 6;
425         var region_scalar: i64 = variationScalarOne;
426         for (0..self.axis_count) |axis_index| {
427             const axis_offset = region_offset + axis_index * 6;
428             const axis_scalar = variationAxisScalar(
429                 try readI16(data, axis_offset),
430                 try readI16(data, axis_offset + 2),
431                 try readI16(data, axis_offset + 4),
432                 try face.normalizedCoordinateAt(settings, axis_index),
433             );
434             region_scalar = @divTrunc(region_scalar * axis_scalar, variationScalarOne);
435             if (region_scalar == 0) return 0;
436         }
437         return @intCast(region_scalar);
438     }
439 };
440 
441 const ItemVariationData = struct {
442     item_count: u16,
443     word_delta_count: u16,
444     region_index_count: u16,
445     long_words: bool,
446     region_indexes_offset: usize,
447     deltas_offset: usize,
448     row_len: usize,
449 
450     pub fn init(data: []const u8, item_table: Table, region_count: u16) FontError!ItemVariationData {
451         if (item_table.len < 6) return error.InvalidVariations;
452         const item_count = try readU16(data, item_table.offset);
453         const packed_word_delta_count = try readU16(data, item_table.offset + 2);
454         const long_words = (packed_word_delta_count & 0x8000) != 0;
455         const word_delta_count = packed_word_delta_count & 0x7fff;
456         const region_index_count = try readU16(data, item_table.offset + 4);
457         if (word_delta_count > region_index_count) return error.InvalidVariations;
458 
459         const region_indexes_offset = item_table.offset + 6;
460         const region_indexes_len = @as(usize, region_index_count) * 2;
461         if (region_indexes_len > item_table.len - 6) return error.InvalidVariations;
462         for (0..region_index_count) |region_index_offset| {
463             const region_index = try readU16(data, region_indexes_offset + region_index_offset * 2);
464             if (region_index >= region_count) return error.InvalidVariations;
465         }
466 
467         const deltas_offset = region_indexes_offset + region_indexes_len;
468         const row_units = @as(usize, region_index_count) + @as(usize, word_delta_count);
469         const row_len = if (long_words) row_units * 2 else row_units;
470         if (@as(u64, item_count) * @as(u64, row_len) > @as(u64, item_table.offset + item_table.len - deltas_offset)) return error.InvalidVariations;
471 
472         return .{
473             .item_count = item_count,
474             .word_delta_count = word_delta_count,
475             .region_index_count = region_index_count,
476             .long_words = long_words,
477             .region_indexes_offset = region_indexes_offset,
478             .deltas_offset = deltas_offset,
479             .row_len = row_len,
480         };
481     }
482 
483     pub fn delta(self: ItemVariationData, data: []const u8, region_list: VariationRegionList, face: anytype, settings: []const VariationSetting, inner_index: u32) FontError!i32 {
484         if (inner_index >= self.item_count) return 0;
485         const row_offset = self.deltas_offset + @as(usize, @intCast(inner_index)) * self.row_len;
486         var accumulated: i64 = 0;
487         for (0..self.region_index_count) |delta_index| {
488             const delta_value = try self.deltaValue(data, row_offset, delta_index);
489             if (delta_value == 0) continue;
490             const region_index = try readU16(data, self.regionIndexesOffset(delta_index));
491             const scalar = try region_list.scalar(data, face, settings, region_index);
492             accumulated += @as(i64, delta_value) * @as(i64, scalar);
493         }
494         return roundVariationDelta(accumulated);
495     }
496 
497     fn deltaValue(self: ItemVariationData, data: []const u8, row_offset: usize, delta_index: usize) FontError!i32 {
498         if (self.long_words) {
499             if (delta_index < self.word_delta_count) return try readI32(data, row_offset + delta_index * 4);
500             const offset = row_offset + @as(usize, self.word_delta_count) * 4 + (delta_index - @as(usize, self.word_delta_count)) * 2;
501             return try readI16(data, offset);
502         }
503         if (delta_index < self.word_delta_count) return try readI16(data, row_offset + delta_index * 2);
504         const offset = row_offset + @as(usize, self.word_delta_count) * 2 + (delta_index - @as(usize, self.word_delta_count));
505         return try readI8(data, offset);
506     }
507 
508     fn regionIndexesOffset(self: ItemVariationData, region_index: usize) usize {
509         return self.region_indexes_offset + region_index * 2;
510     }
511 };
512 
513 fn optionalDeltaSetIndexMap(data: []const u8, parent: Table, relative_offset: u32) FontError!?DeltaSetIndexMap {
514     if (relative_offset == 0) return null;
515     return try DeltaSetIndexMap.init(data, try variationChildTable(parent, relative_offset, 4));
516 }
517 
518 pub fn variationChildTable(parent: Table, relative_offset: u32, min_len: usize) FontError!Table {
519     const offset = try variationChildOffset(parent, relative_offset, min_len);
520     return .{ .offset = offset, .len = parent.offset + parent.len - offset };
521 }
522 
523 pub fn variationChildOffset(parent: Table, relative_offset: u32, min_len: usize) FontError!usize {
524     if (relative_offset == 0) return error.InvalidVariations;
525     if (relative_offset > std.math.maxInt(usize)) return error.InvalidVariations;
526     const relative: usize = @intCast(relative_offset);
527     if (relative > parent.len or min_len > parent.len - relative) return error.InvalidVariations;
528     return parent.offset + relative;
529 }
530 
531 pub fn fixed16FromFloat(value: f32) FontError!i32 {
532     if (!std.math.isFinite(value)) return error.InvalidVariations;
533     const scaled = @as(f64, value) * 65536.0;
534     if (scaled > @as(f64, @floatFromInt(std.math.maxInt(i32)))) return error.InvalidVariations;
535     if (scaled < @as(f64, @floatFromInt(std.math.minInt(i32)))) return error.InvalidVariations;
536     const rounded = if (scaled >= 0) @floor(scaled + 0.5) else @ceil(scaled - 0.5);
537     return @intFromFloat(rounded);
538 }
539 
540 pub fn normalizeVariationAxis(axis: VariationAxis, user_value: i32) i32 {
541     const clamped_user = std.math.clamp(user_value, axis.min_value, axis.max_value);
542     if (clamped_user < axis.default_value) {
543         const denominator = @as(i64, axis.default_value) - @as(i64, axis.min_value);
544         if (denominator == 0) return 0;
545         const numerator = (@as(i64, axis.default_value) - @as(i64, clamped_user)) * 65536;
546         return clampFixed16(-@divTrunc(numerator, denominator));
547     }
548     if (clamped_user > axis.default_value) {
549         const denominator = @as(i64, axis.max_value) - @as(i64, axis.default_value);
550         if (denominator == 0) return 0;
551         const numerator = (@as(i64, clamped_user) - @as(i64, axis.default_value)) * 65536;
552         return clampFixed16(@divTrunc(numerator, denominator));
553     }
554     return 0;
555 }
556 
557 pub fn fixed16ToF2Dot14(value: i32) i16 {
558     const clamped = std.math.clamp(value, -65536, 65536);
559     return @intCast((clamped + 2) >> 2);
560 }
561 
562 fn f2Dot14ToFixed16(value: i16) i32 {
563     return @as(i32, value) << 2;
564 }
565 
566 fn clampFixed16(value: i64) i32 {
567     if (value > 65536) return 65536;
568     if (value < -65536) return -65536;
569     return @intCast(value);
570 }
571 
572 const variationScalarOne: i32 = 65536;
573 const variationScalarHalf: i64 = variationScalarOne / 2;
574 
575 fn roundVariationDelta(value: i64) i32 {
576     const rounded = @divFloor(value + variationScalarHalf, variationScalarOne);
577     return clampI32(rounded);
578 }
579 
580 fn variationAxisScalar(start: i16, peak: i16, end: i16, instance: i16) i32 {
581     if (start > peak or peak > end) return variationScalarOne;
582     if (start < 0 and end > 0 and peak != 0) return variationScalarOne;
583     if (peak == 0) return variationScalarOne;
584     if (instance < start or instance > end) return 0;
585     if (instance == peak) return variationScalarOne;
586     if (instance < peak) {
587         const numerator = (@as(i64, instance) - @as(i64, start)) * variationScalarOne;
588         const denominator = @as(i64, peak) - @as(i64, start);
589         if (denominator <= 0) return 0;
590         return @intCast(@divTrunc(numerator, denominator));
591     }
592     const numerator = (@as(i64, end) - @as(i64, instance)) * variationScalarOne;
593     const denominator = @as(i64, end) - @as(i64, peak);
594     if (denominator <= 0) return 0;
595     return @intCast(@divTrunc(numerator, denominator));
596 }
597 
598 pub fn clampDesignMetric(base: i32, delta: i32) i32 {
599     const value = @as(i64, base) + @as(i64, delta);
600     if (value <= 0) return 0;
601     if (value > std.math.maxInt(i32)) return std.math.maxInt(i32);
602     return @intCast(value);
603 }
604 
605 pub fn clampI32(value: i64) i32 {
606     if (value > std.math.maxInt(i32)) return std.math.maxInt(i32);
607     if (value < std.math.minInt(i32)) return std.math.minInt(i32);
608     return @intCast(value);
609 }
610 
611 fn avarMapAt(data: []const u8, records: usize, map_index: usize) FontError!AvarMap {
612     const offset = records + map_index * 4;
613     return .{
614         .from = f2Dot14ToFixed16(try readI16(data, offset)),
615         .to = f2Dot14ToFixed16(try readI16(data, offset + 2)),
616     };
617 }
618 
619 fn avarHasRequiredMaps(data: []const u8, records: usize, count: u16) FontError!bool {
620     var has_min = false;
621     var has_default = false;
622     var has_max = false;
623     for (0..count) |map_index| {
624         const map_record = try avarMapAt(data, records, map_index);
625         if (map_record.from == -65536 and map_record.to == -65536) has_min = true;
626         if (map_record.from == 0 and map_record.to == 0) has_default = true;
627         if (map_record.from == 65536 and map_record.to == 65536) has_max = true;
628     }
629     return has_min and has_default and has_max;
630 }
631 
632 test "item variation deltas round at use" {
633     try std.testing.expectEqual(@as(i32, 0), roundVariationDelta(variationScalarHalf - 1));
634     try std.testing.expectEqual(@as(i32, 1), roundVariationDelta(variationScalarHalf));
635     try std.testing.expectEqual(@as(i32, 2), roundVariationDelta(variationScalarOne + variationScalarHalf));
636     try std.testing.expectEqual(@as(i32, 0), roundVariationDelta(-variationScalarHalf));
637     try std.testing.expectEqual(@as(i32, -1), roundVariationDelta(-variationScalarHalf - 1));
638     try std.testing.expectEqual(@as(i32, -1), roundVariationDelta(-variationScalarOne - variationScalarHalf));
639     try std.testing.expectEqual(@as(i32, -2), roundVariationDelta(-variationScalarOne - variationScalarHalf - 1));
640 }