lib/filigree/src/font/cmap.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 readU16 = binary.readU16;
8 const readU24 = binary.readU24;
9 const readU32 = binary.readU32;
10
11 const CmapFormat = enum {
12 format_4,
13 format_12,
14 };
15
16 pub const Cmap = struct {
17 offset: usize,
18 len: usize,
19 format: CmapFormat,
20 variation: ?Table = null,
21
22 pub fn init(data: []const u8, cmap_table: Table) FontError!Cmap {
23 if (cmap_table.len < 4) return error.InvalidCmap;
24 const count = try readU16(data, cmap_table.offset + 2);
25 if (4 + @as(usize, count) * 8 > cmap_table.len) return error.InvalidCmap;
26
27 var best_score: u16 = 0;
28 var best: ?Cmap = null;
29 var variation: ?Table = null;
30 for (0..count) |i| {
31 const record = cmap_table.offset + 4 + i * 8;
32 const platform = try readU16(data, record);
33 const encoding = try readU16(data, record + 2);
34 const sub_offset = try readU32(data, record + 4);
35 if (sub_offset >= cmap_table.len) continue;
36 const absolute = cmap_table.offset + @as(usize, @intCast(sub_offset));
37 if (absolute + 2 > data.len) continue;
38 const format = try readU16(data, absolute);
39 if (format == 14 and platform == 0 and encoding == 5) {
40 variation = validateFormat14(data, absolute) catch variation;
41 continue;
42 }
43 const candidate = switch (format) {
44 4 => validateFormat4(data, absolute) catch continue,
45 12 => validateFormat12(data, absolute) catch continue,
46 else => continue,
47 };
48 const score = scoreSubtable(platform, encoding, candidate.format);
49 if (score > best_score) {
50 best_score = score;
51 best = candidate;
52 }
53 }
54
55 var selected = best orelse return error.UnsupportedCmap;
56 selected.variation = variation;
57 return selected;
58 }
59
60 pub fn map(self: Cmap, data: []const u8, codepoint: u32) FontError!u32 {
61 return switch (self.format) {
62 .format_4 => mapFormat4(self, data, codepoint),
63 .format_12 => mapFormat12(self, data, codepoint),
64 };
65 }
66
67 pub fn mapVariation(self: Cmap, data: []const u8, codepoint: u32, variation_selector: u32) FontError!?u32 {
68 const variation = self.variation orelse return null;
69 return try mapFormat14(self, data, variation, codepoint, variation_selector);
70 }
71 };
72
73 fn validateFormat4(data: []const u8, offset: usize) FontError!Cmap {
74 if (offset > data.len or data.len - offset < 16) return error.InvalidCmap;
75 const len: usize = try readU16(data, offset + 2);
76 if (len < 16 or len > data.len - offset) return error.InvalidCmap;
77 const seg_count_x2 = try readU16(data, offset + 6);
78 if (seg_count_x2 == 0 or seg_count_x2 % 2 != 0) return error.InvalidCmap;
79 const seg_count = @as(usize, seg_count_x2 / 2);
80 const arrays_end = 14 + seg_count * 8;
81 if (arrays_end > len) return error.InvalidCmap;
82 return .{ .offset = offset, .len = len, .format = .format_4 };
83 }
84
85 fn validateFormat12(data: []const u8, offset: usize) FontError!Cmap {
86 if (offset > data.len or data.len - offset < 16) return error.InvalidCmap;
87 const len_u32 = try readU32(data, offset + 4);
88 if (@as(u64, len_u32) > @as(u64, std.math.maxInt(usize))) return error.InvalidCmap;
89 const len: usize = @intCast(len_u32);
90 if (len < 16 or len > data.len - offset) return error.InvalidCmap;
91 const groups = try readU32(data, offset + 12);
92 const records_len = @as(u64, groups) * 12;
93 if (records_len > std.math.maxInt(usize)) return error.InvalidCmap;
94 if (records_len > @as(u64, len - 16)) return error.InvalidCmap;
95 return .{ .offset = offset, .len = len, .format = .format_12 };
96 }
97
98 fn validateFormat14(data: []const u8, offset: usize) FontError!Table {
99 if (offset > data.len or data.len - offset < 10) return error.InvalidCmap;
100 const len_u32 = try readU32(data, offset + 2);
101 if (@as(u64, len_u32) > @as(u64, std.math.maxInt(usize))) return error.InvalidCmap;
102 const len: usize = @intCast(len_u32);
103 if (len < 10 or len > data.len - offset) return error.InvalidCmap;
104 const selector_count = try readU32(data, offset + 6);
105 if (@as(u64, selector_count) * 11 > @as(u64, len - 10)) return error.InvalidCmap;
106
107 const subtable = Table{ .offset = offset, .len = len };
108 for (0..selector_count) |i| {
109 const record = offset + 10 + i * 11;
110 _ = try readU24(data, record);
111 const default_offset = try readU32(data, record + 3);
112 const non_default_offset = try readU32(data, record + 7);
113
114 if (default_offset != 0) {
115 const default_table = try format14ChildOffset(subtable, default_offset, 4);
116 const range_count = try readU32(data, default_table);
117 if (@as(u64, range_count) * 4 > @as(u64, offset + len - default_table - 4)) return error.InvalidCmap;
118 }
119
120 if (non_default_offset != 0) {
121 const non_default_table = try format14ChildOffset(subtable, non_default_offset, 4);
122 const mapping_count = try readU32(data, non_default_table);
123 if (@as(u64, mapping_count) * 5 > @as(u64, offset + len - non_default_table - 4)) return error.InvalidCmap;
124 }
125 }
126
127 return subtable;
128 }
129
130 fn mapFormat4(cmap: Cmap, data: []const u8, codepoint: u32) FontError!u32 {
131 if (codepoint > 0xffff) return 0;
132 const cp: u16 = @intCast(codepoint);
133 const seg_count = @as(usize, (try readU16(data, cmap.offset + 6)) / 2);
134 const end_codes = cmap.offset + 14;
135 const start_codes = end_codes + seg_count * 2 + 2;
136 const id_deltas = start_codes + seg_count * 2;
137 const id_range_offsets = id_deltas + seg_count * 2;
138
139 for (0..seg_count) |i| {
140 const end = try readU16(data, end_codes + i * 2);
141 if (cp > end) continue;
142 const start = try readU16(data, start_codes + i * 2);
143 if (cp < start) return 0;
144 const delta = try readU16(data, id_deltas + i * 2);
145 const range_offset_pos = id_range_offsets + i * 2;
146 const range_offset = try readU16(data, range_offset_pos);
147 if (range_offset == 0) {
148 return @as(u16, @truncate(@as(u32, cp) + @as(u32, delta)));
149 }
150 const glyph_offset = range_offset_pos + @as(usize, range_offset) + (@as(usize, cp - start) * 2);
151 if (glyph_offset + 2 > cmap.offset + cmap.len) return error.InvalidCmap;
152 const glyph = try readU16(data, glyph_offset);
153 if (glyph == 0) return 0;
154 return @as(u16, @truncate(@as(u32, glyph) + @as(u32, delta)));
155 }
156 return 0;
157 }
158
159 fn mapFormat12(cmap: Cmap, data: []const u8, codepoint: u32) FontError!u32 {
160 const groups = try readU32(data, cmap.offset + 12);
161 var left: usize = 0;
162 var right: usize = @intCast(groups);
163 while (left < right) {
164 const mid = left + (right - left) / 2;
165 const record = cmap.offset + 16 + mid * 12;
166 const start = try readU32(data, record);
167 const end = try readU32(data, record + 4);
168 if (codepoint < start) {
169 right = mid;
170 } else if (codepoint > end) {
171 left = mid + 1;
172 } else {
173 const glyph_start = try readU32(data, record + 8);
174 const delta = codepoint - start;
175 if (glyph_start > std.math.maxInt(u32) - delta) return error.InvalidCmap;
176 return glyph_start + delta;
177 }
178 }
179 return 0;
180 }
181
182 fn mapFormat14(cmap: Cmap, data: []const u8, variation: Table, codepoint: u32, variation_selector: u32) FontError!?u32 {
183 const selector_count = try readU32(data, variation.offset + 6);
184 var left: usize = 0;
185 var right: usize = @intCast(selector_count);
186 while (left < right) {
187 const mid = left + (right - left) / 2;
188 const record = variation.offset + 10 + mid * 11;
189 const selector = try readU24(data, record);
190 if (variation_selector < selector) {
191 right = mid;
192 } else if (variation_selector > selector) {
193 left = mid + 1;
194 } else {
195 const default_offset = try readU32(data, record + 3);
196 const non_default_offset = try readU32(data, record + 7);
197 if (non_default_offset != 0) {
198 if (try mapFormat14NonDefault(data, variation, non_default_offset, codepoint)) |glyph| return glyph;
199 }
200 if (default_offset != 0 and try mapFormat14Default(data, variation, default_offset, codepoint)) {
201 return try cmap.map(data, codepoint);
202 }
203 return null;
204 }
205 }
206 return null;
207 }
208
209 fn mapFormat14Default(data: []const u8, variation: Table, relative_offset: u32, codepoint: u32) FontError!bool {
210 const table_offset = try format14ChildOffset(variation, relative_offset, 4);
211 const range_count = try readU32(data, table_offset);
212 var left: usize = 0;
213 var right: usize = @intCast(range_count);
214 while (left < right) {
215 const mid = left + (right - left) / 2;
216 const record = table_offset + 4 + mid * 4;
217 const start = try readU24(data, record);
218 const additional_count = data[record + 3];
219 const end = start + additional_count;
220 if (codepoint < start) {
221 right = mid;
222 } else if (codepoint > end) {
223 left = mid + 1;
224 } else {
225 return true;
226 }
227 }
228 return false;
229 }
230
231 fn mapFormat14NonDefault(data: []const u8, variation: Table, relative_offset: u32, codepoint: u32) FontError!?u32 {
232 const table_offset = try format14ChildOffset(variation, relative_offset, 4);
233 const mapping_count = try readU32(data, table_offset);
234 var left: usize = 0;
235 var right: usize = @intCast(mapping_count);
236 while (left < right) {
237 const mid = left + (right - left) / 2;
238 const record = table_offset + 4 + mid * 5;
239 const value = try readU24(data, record);
240 if (codepoint < value) {
241 right = mid;
242 } else if (codepoint > value) {
243 left = mid + 1;
244 } else {
245 return try readU16(data, record + 3);
246 }
247 }
248 return null;
249 }
250
251 fn format14ChildOffset(subtable: Table, relative_offset: u32, min_len: usize) FontError!usize {
252 if (relative_offset == 0) return error.InvalidCmap;
253 if (relative_offset > std.math.maxInt(usize)) return error.InvalidCmap;
254 const relative: usize = @intCast(relative_offset);
255 if (relative > subtable.len or min_len > subtable.len - relative) return error.InvalidCmap;
256 return subtable.offset + relative;
257 }
258
259 fn scoreSubtable(platform: u16, encoding: u16, format: CmapFormat) u16 {
260 return switch (format) {
261 .format_12 => if (platform == 3 and encoding == 10) 600 else if (platform == 0) 500 else 100,
262 .format_4 => if (platform == 3 and encoding == 1) 400 else if (platform == 3 and encoding == 0) 350 else if (platform == 0) 300 else 50,
263 };
264 }
265
266 test "Cmap maps supplementary codepoints through format 12" {
267 var bytes = @as([40]u8, @splat(0));
268 std.mem.writeInt(u16, bytes[0..2], 0, .big);
269 std.mem.writeInt(u16, bytes[2..4], 1, .big);
270 std.mem.writeInt(u16, bytes[4..6], 3, .big);
271 std.mem.writeInt(u16, bytes[6..8], 10, .big);
272 std.mem.writeInt(u32, bytes[8..12], 12, .big);
273 std.mem.writeInt(u16, bytes[12..14], 12, .big);
274 std.mem.writeInt(u16, bytes[14..16], 0, .big);
275 std.mem.writeInt(u32, bytes[16..20], 28, .big);
276 std.mem.writeInt(u32, bytes[20..24], 0, .big);
277 std.mem.writeInt(u32, bytes[24..28], 1, .big);
278 std.mem.writeInt(u32, bytes[28..32], 0x1f600, .big);
279 std.mem.writeInt(u32, bytes[32..36], 0x1f600, .big);
280 std.mem.writeInt(u32, bytes[36..40], 77, .big);
281
282 const mapped = try Cmap.init(&bytes, .{ .offset = 0, .len = bytes.len });
283 try std.testing.expectEqual(@as(u32, 77), try mapped.map(&bytes, 0x1f600));
284 try std.testing.expectEqual(@as(u32, 0), try mapped.map(&bytes, 'A'));
285 }