lib/filigree/src/layout/class.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 const binary = @import("binary.zig");
4 const LayoutError = binary.LayoutError;
5 const hasBytes = binary.hasBytes;
6 const readU16 = binary.readU16;
7
8 pub const ClassDef = union(enum) {
9 format_1: ClassDefFormat1,
10 format_2: ClassDefFormat2,
11
12 pub fn init(data: []const u8, offset: usize) LayoutError!ClassDef {
13 if (!hasBytes(data, offset, 2)) return error.InvalidLayout;
14 return switch (try readU16(data, offset)) {
15 1 => .{ .format_1 = try ClassDefFormat1.init(data, offset) },
16 2 => .{ .format_2 = try ClassDefFormat2.init(data, offset) },
17 else => error.UnsupportedClassDef,
18 };
19 }
20
21 pub fn class(self: ClassDef, data: []const u8, glyph_id: u32) LayoutError!u16 {
22 return switch (self) {
23 .format_1 => |format| format.class(data, glyph_id),
24 .format_2 => |format| format.class(data, glyph_id),
25 };
26 }
27 };
28
29 pub const ClassDefFormat1 = struct {
30 start_glyph_id: u16,
31 class_value_offset: usize,
32 glyph_count: u16,
33
34 fn init(data: []const u8, offset: usize) LayoutError!ClassDefFormat1 {
35 if (!hasBytes(data, offset, 6)) return error.InvalidLayout;
36 const glyph_count = try readU16(data, offset + 4);
37 const array_offset = offset + 6;
38 const array_len = @as(usize, glyph_count) * 2;
39 if (!hasBytes(data, array_offset, array_len)) return error.InvalidLayout;
40 return .{
41 .start_glyph_id = try readU16(data, offset + 2),
42 .class_value_offset = array_offset,
43 .glyph_count = glyph_count,
44 };
45 }
46
47 fn class(self: ClassDefFormat1, data: []const u8, glyph_id: u32) LayoutError!u16 {
48 if (glyph_id > std.math.maxInt(u16)) return 0;
49 const glyph: u16 = @intCast(glyph_id);
50 if (glyph < self.start_glyph_id) return 0;
51 const index = @as(usize, glyph - self.start_glyph_id);
52 if (index >= self.glyph_count) return 0;
53 return try readU16(data, self.class_value_offset + index * 2);
54 }
55 };
56
57 pub const ClassDefFormat2 = struct {
58 class_range_array_offset: usize,
59 class_range_count: u16,
60
61 fn init(data: []const u8, offset: usize) LayoutError!ClassDefFormat2 {
62 if (!hasBytes(data, offset, 4)) return error.InvalidLayout;
63 const range_count = try readU16(data, offset + 2);
64 const array_offset = offset + 4;
65 const array_len = @as(usize, range_count) * 6;
66 if (!hasBytes(data, array_offset, array_len)) return error.InvalidLayout;
67 var previous_end: ?u16 = null;
68 for (0..range_count) |i| {
69 const record = array_offset + i * 6;
70 const start = try readU16(data, record);
71 const end = try readU16(data, record + 2);
72 if (start > end) return error.InvalidLayout;
73 if (previous_end) |seen| {
74 if (start <= seen) return error.InvalidLayout;
75 }
76 previous_end = end;
77 }
78 return .{
79 .class_range_array_offset = array_offset,
80 .class_range_count = range_count,
81 };
82 }
83
84 fn class(self: ClassDefFormat2, data: []const u8, glyph_id: u32) LayoutError!u16 {
85 if (glyph_id > std.math.maxInt(u16)) return 0;
86 const glyph: u16 = @intCast(glyph_id);
87 var left: usize = 0;
88 var right: usize = self.class_range_count;
89 while (left < right) {
90 const mid = left + (right - left) / 2;
91 const record = self.class_range_array_offset + mid * 6;
92 const start = try readU16(data, record);
93 const end = try readU16(data, record + 2);
94 if (glyph < start) {
95 right = mid;
96 } else if (glyph > end) {
97 left = mid + 1;
98 } else {
99 return try readU16(data, record + 4);
100 }
101 }
102 return 0;
103 }
104 };
105
106 test "ClassDef format 1 maps consecutive glyph classes" {
107 const data = [_]u8{
108 0, 1,
109 0, 5,
110 0, 3,
111 0, 1,
112 0, 7,
113 0, 2,
114 };
115 const class_def = try ClassDef.init(&data, 0);
116 try std.testing.expectEqual(@as(u16, 0), try class_def.class(&data, 4));
117 try std.testing.expectEqual(@as(u16, 1), try class_def.class(&data, 5));
118 try std.testing.expectEqual(@as(u16, 7), try class_def.class(&data, 6));
119 try std.testing.expectEqual(@as(u16, 2), try class_def.class(&data, 7));
120 try std.testing.expectEqual(@as(u16, 0), try class_def.class(&data, 8));
121 }
122
123 test "ClassDef format 2 maps class ranges" {
124 const data = [_]u8{
125 0, 2,
126 0, 2,
127 0, 10,
128 0, 12,
129 0, 4,
130 0, 30,
131 0, 31,
132 0, 8,
133 };
134 const class_def = try ClassDef.init(&data, 0);
135 try std.testing.expectEqual(@as(u16, 4), try class_def.class(&data, 10));
136 try std.testing.expectEqual(@as(u16, 4), try class_def.class(&data, 12));
137 try std.testing.expectEqual(@as(u16, 8), try class_def.class(&data, 31));
138 try std.testing.expectEqual(@as(u16, 0), try class_def.class(&data, 13));
139 try std.testing.expectEqual(@as(u16, 0), try class_def.class(&data, 0x1_0000));
140 }
141
142 test "ClassDef honors nonzero table offsets" {
143 const data = [_]u8{
144 0xaa, 0xbb,
145 0, 1,
146 0, 7,
147 0, 1,
148 0, 5,
149 };
150 const class_def = try ClassDef.init(&data, 2);
151 try std.testing.expectEqual(@as(u16, 5), try class_def.class(&data, 7));
152 }
153
154 test "ClassDef rejects unsupported format" {
155 const data = [_]u8{ 0, 3 };
156 try std.testing.expectError(error.UnsupportedClassDef, ClassDef.init(&data, 0));
157 }