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 }