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 }