lib/filigree/src/font/description.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const binary = @import("binary.zig");
3 const directory = @import("directory.zig");
4 const model = @import("model.zig");
5 const names = @import("names.zig");
6 const cmap_owner = @import("cmap.zig");
7
8 /// Coverage beyond the four OS/2 Unicode-range words.
9 pub const extended_emoji_word: usize = 4;
10 pub const extended_emoji_bit: u5 = 0;
11
12 pub const Description = struct {
13 family: []u8,
14 weight_class: u16,
15 width_class: u16,
16 selection: u16,
17 units_per_em: u16,
18 ascent: f32,
19 descent: f32,
20 line_gap: f32,
21 coverage: [8]u32,
22 fixed_pitch: bool,
23 color: bool,
24 face_index: u32,
25
26 pub fn deinit(self: *Description, allocator: std.mem.Allocator) void {
27 allocator.free(self.family);
28 self.* = undefined;
29 }
30 };
31
32 pub fn describeAlloc(allocator: std.mem.Allocator, data: []const u8) (model.FontError || error{OutOfMemory})!Description {
33 return describeAtAlloc(allocator, data, 0);
34 }
35
36 pub fn describeAtAlloc(allocator: std.mem.Allocator, data: []const u8, face_index: u32) (model.FontError || error{OutOfMemory})!Description {
37 const offset = try directory.fontDirectoryOffset(data, face_index);
38 const os2 = try directory.tableAt(data, offset, "OS/2");
39 if (os2.len < 64) return error.InvalidFont;
40 const head = try directory.tableAt(data, offset, "head");
41 const hhea = try directory.tableAt(data, offset, "hhea");
42 if (head.len < 54 or hhea.len < 10) return error.InvalidFont;
43 const units_per_em = try binary.readU16(data, head.offset + 18);
44 if (units_per_em == 0) return error.InvalidFont;
45 const selection = try binary.readU16(data, os2.offset + 62);
46 const metric_source = if (selection & 0x80 != 0) os2 else hhea;
47 if (selection & 0x80 != 0 and os2.len < 78) return error.InvalidFont;
48 const metric_offset: usize = if (selection & 0x80 != 0) 68 else 4;
49 const scale = @as(f32, @floatFromInt(units_per_em));
50 const cmap_table = try directory.tableAt(data, offset, "cmap");
51 const cmap = try cmap_owner.Cmap.init(data, cmap_table);
52 var coverage: [8]u32 = @splat(0);
53 inline for (0..4) |word| {
54 coverage[word] = try binary.readU32(data, os2.offset + 42 + word * 4);
55 }
56 if (try hasEmoji(data, cmap)) coverage[extended_emoji_word] |= @as(u32, 1) << extended_emoji_bit;
57 const post = directory.tableAt(data, offset, "post") catch |err| switch (err) {
58 error.MissingTable => null,
59 else => return err,
60 };
61 const name_table = try directory.tableAt(data, offset, "name");
62 const record = (try names.bestNameRecord(data, name_table, 16)) orelse
63 (try names.bestNameRecord(data, name_table, 1)) orelse return error.InvalidFont;
64 const family = names.utf16BeToUtf8Alloc(allocator, record.bytes) catch |err| switch (err) {
65 error.OutOfMemory => return error.OutOfMemory,
66 else => return error.InvalidFont,
67 };
68 errdefer allocator.free(family);
69 return .{
70 .family = family,
71 .weight_class = try binary.readU16(data, os2.offset + 4),
72 .width_class = try binary.readU16(data, os2.offset + 6),
73 .selection = selection,
74 .units_per_em = units_per_em,
75 .ascent = @as(f32, @floatFromInt(try binary.readI16(data, metric_source.offset + metric_offset))) / scale,
76 .descent = @as(f32, @floatFromInt(try binary.readI16(data, metric_source.offset + metric_offset + 2))) / scale,
77 .line_gap = @as(f32, @floatFromInt(try binary.readI16(data, metric_source.offset + metric_offset + 4))) / scale,
78 .coverage = coverage,
79 .fixed_pitch = if (post) |table| try readFixedPitch(data, table) else false,
80 .color = directory.optionalAt(data, offset, "COLR") != null or
81 directory.optionalAt(data, offset, "CBDT") != null or
82 directory.optionalAt(data, offset, "sbix") != null or
83 directory.optionalAt(data, offset, "SVG ") != null,
84 .face_index = face_index,
85 };
86 }
87
88 fn hasEmoji(data: []const u8, cmap: cmap_owner.Cmap) model.FontError!bool {
89 for (0x1f300..0x1fb00) |codepoint| {
90 if (try cmap.map(data, @intCast(codepoint)) != 0) return true;
91 }
92 return false;
93 }
94
95 fn readFixedPitch(data: []const u8, table: binary.Table) model.FontError!bool {
96 if (table.len < 16) return error.InvalidFont;
97 return (try binary.readU32(data, table.offset + 12)) != 0;
98 }
99
100 test "face description reads family and OS/2 fields" {
101 const fixture = @import("../fixture/root.zig");
102 const allocator = std.testing.allocator;
103 const bytes = try fixture.createWithMetricVariations(allocator);
104 defer allocator.free(bytes);
105 const os2 = try directory.table(bytes, "OS/2");
106 std.mem.writeInt(u16, bytes[os2.offset + 4 ..][0..2], 700, .big);
107 std.mem.writeInt(u16, bytes[os2.offset + 6 ..][0..2], 3, .big);
108 std.mem.writeInt(u32, bytes[os2.offset + 42 ..][0..4], 0x201, .big);
109 std.mem.writeInt(u16, bytes[os2.offset + 62 ..][0..2], 0x21, .big);
110 var result = try describeAlloc(allocator, bytes);
111 defer result.deinit(allocator);
112 try std.testing.expectEqualStrings("Tiny Fixture", result.family);
113 try std.testing.expectEqual(@as(u16, 700), result.weight_class);
114 try std.testing.expectEqual(@as(u16, 3), result.width_class);
115 try std.testing.expectEqual(@as(u16, 0x21), result.selection);
116 try std.testing.expectEqual(@as(u32, 0x201), result.coverage[0]);
117 try std.testing.expectEqual(@as(u16, 1000), result.units_per_em);
118 try std.testing.expectEqual(@as(f32, 0.8), result.ascent);
119 try std.testing.expectEqual(@as(f32, -0.2), result.descent);
120 try std.testing.expectEqual(@as(f32, 0), result.line_gap);
121 try std.testing.expect(!result.fixed_pitch);
122 try std.testing.expect(!result.color);
123 try std.testing.expectEqual(@as(u32, 0), result.face_index);
124 }
125
126 test "face description selects OS/2 typo metrics when requested" {
127 const fixture = @import("../fixture/root.zig");
128 const allocator = std.testing.allocator;
129 const bytes = try fixture.createWithMetricVariations(allocator);
130 defer allocator.free(bytes);
131 var result = try describeAlloc(allocator, bytes);
132 defer result.deinit(allocator);
133 try std.testing.expectEqual(@as(f32, 0.8), result.ascent);
134 try std.testing.expectEqual(@as(f32, -0.2), result.descent);
135 try std.testing.expectEqual(@as(f32, 0.04), result.line_gap);
136 }
137
138 test "face description derives extended emoji coverage from cmap" {
139 var bytes: [40]u8 = @splat(0);
140 std.mem.writeInt(u16, bytes[2..4], 1, .big);
141 std.mem.writeInt(u16, bytes[4..6], 3, .big);
142 std.mem.writeInt(u16, bytes[6..8], 10, .big);
143 std.mem.writeInt(u32, bytes[8..12], 12, .big);
144 std.mem.writeInt(u16, bytes[12..14], 12, .big);
145 std.mem.writeInt(u32, bytes[16..20], 28, .big);
146 std.mem.writeInt(u32, bytes[24..28], 1, .big);
147 std.mem.writeInt(u32, bytes[28..32], 0x1f600, .big);
148 std.mem.writeInt(u32, bytes[32..36], 0x1f600, .big);
149 std.mem.writeInt(u32, bytes[36..40], 1, .big);
150 const cmap = try cmap_owner.Cmap.init(&bytes, .{ .offset = 0, .len = bytes.len });
151 try std.testing.expect(try hasEmoji(&bytes, cmap));
152 }
153
154 test "face description reads post fixed pitch and rejects a short table" {
155 var bytes: [16]u8 = @splat(0);
156 std.mem.writeInt(u32, bytes[12..16], 1, .big);
157 try std.testing.expect(try readFixedPitch(&bytes, .{ .offset = 0, .len = bytes.len }));
158 try std.testing.expectError(error.InvalidFont, readFixedPitch(&bytes, .{ .offset = 0, .len = 15 }));
159 }
160
161 test "face description rejects missing and truncated OS/2" {
162 const fixture = @import("../fixture/root.zig");
163 const allocator = std.testing.allocator;
164 const missing = try fixture.create(allocator);
165 defer allocator.free(missing);
166 try std.testing.expectError(error.MissingTable, describeAlloc(allocator, missing));
167 const bytes = try fixture.createWithMetricVariations(allocator);
168 defer allocator.free(bytes);
169 const os2 = try directory.table(bytes, "OS/2");
170 const records = try binary.readU16(bytes, 4);
171 for (0..records) |index| {
172 const row = 12 + index * 16;
173 if (try binary.readU32(bytes, row) != model.tag("OS/2")) continue;
174 std.mem.writeInt(u32, bytes[row + 12 ..][0..4], 63, .big);
175 break;
176 }
177 _ = os2;
178 try std.testing.expectError(error.InvalidFont, describeAlloc(allocator, bytes));
179 }