we (web engine): Experimental web browser project to understand the limits of Claude
1//! `kern` — Kerning table.
2//!
3//! Contains kerning pair adjustments for glyph spacing.
4//! Supports the classic Microsoft kern table (version 0) with format 0 subtables.
5//! Reference: <https://learn.microsoft.com/en-us/typography/opentype/spec/kern>
6
7use crate::font::parse::Reader;
8use crate::font::FontError;
9
10/// A single kerning pair.
11#[derive(Debug, Clone, Copy)]
12struct KernPair {
13 /// Left glyph ID.
14 left: u16,
15 /// Right glyph ID.
16 right: u16,
17 /// Kerning value in font units (positive = move apart, negative = move together).
18 value: i16,
19}
20
21/// Parsed `kern` table.
22#[derive(Debug)]
23pub struct KernTable {
24 /// Sorted list of kerning pairs (for binary search).
25 pairs: Vec<KernPair>,
26}
27
28impl KernTable {
29 /// Create an empty kern table (no pairs).
30 pub fn empty() -> KernTable {
31 KernTable { pairs: Vec::new() }
32 }
33
34 /// Parse the `kern` table from raw bytes.
35 pub fn parse(data: &[u8]) -> Result<KernTable, FontError> {
36 let r = Reader::new(data);
37 if r.len() < 4 {
38 return Err(FontError::MalformedTable("kern"));
39 }
40
41 let version = r.u16(0)?;
42
43 match version {
44 0 => Self::parse_version0(data),
45 _ => {
46 // Version 1 (Apple AAT) or unknown — try parsing as version 0
47 // since some fonts mislabel the version. If that fails, return
48 // an empty table (kerning is optional, not critical).
49 Ok(KernTable { pairs: Vec::new() })
50 }
51 }
52 }
53
54 /// Parse a version 0 kern table (Microsoft format).
55 fn parse_version0(data: &[u8]) -> Result<KernTable, FontError> {
56 let r = Reader::new(data);
57 let n_tables = r.u16(2)? as usize;
58
59 let mut pairs = Vec::new();
60 let mut offset = 4; // Skip version + nTables
61
62 for _ in 0..n_tables {
63 if offset + 6 > r.len() {
64 break;
65 }
66
67 let _subtable_version = r.u16(offset)?;
68 let subtable_length = r.u16(offset + 2)? as usize;
69 let coverage = r.u16(offset + 4)?;
70
71 // Coverage field:
72 // Bit 0: 1 = horizontal kerning
73 // Bit 1: 1 = minimum values (not kerning values)
74 // Bit 2: 1 = cross-stream
75 // Bits 8-15: format number
76 let is_horizontal = coverage & 0x0001 != 0;
77 let is_minimum = coverage & 0x0002 != 0;
78 let is_cross_stream = coverage & 0x0004 != 0;
79 let format = (coverage >> 8) as u8;
80
81 // We only support horizontal kerning, format 0, non-minimum, non-cross-stream.
82 if format == 0 && is_horizontal && !is_minimum && !is_cross_stream {
83 Self::parse_format0(data, offset + 6, &mut pairs)?;
84 }
85
86 // Advance to next subtable.
87 if subtable_length == 0 {
88 break;
89 }
90 offset += subtable_length;
91 }
92
93 // Sort pairs for binary search.
94 pairs.sort_by(|a, b| a.left.cmp(&b.left).then_with(|| a.right.cmp(&b.right)));
95
96 Ok(KernTable { pairs })
97 }
98
99 /// Parse a format 0 subtable (sorted pairs).
100 fn parse_format0(
101 data: &[u8],
102 offset: usize,
103 pairs: &mut Vec<KernPair>,
104 ) -> Result<(), FontError> {
105 let r = Reader::new(data);
106 if offset + 8 > r.len() {
107 return Err(FontError::MalformedTable("kern"));
108 }
109
110 let n_pairs = r.u16(offset)? as usize;
111 // Skip searchRange(2), entrySelector(2), rangeShift(2) = 6 bytes.
112 let pair_offset = offset + 8;
113
114 for i in 0..n_pairs {
115 let base = pair_offset + i * 6;
116 if base + 6 > r.len() {
117 break;
118 }
119 let left = r.u16(base)?;
120 let right = r.u16(base + 2)?;
121 let value = r.i16(base + 4)?;
122 pairs.push(KernPair { left, right, value });
123 }
124
125 Ok(())
126 }
127
128 /// Look up the kerning value for a pair of glyph IDs.
129 ///
130 /// Returns the kerning adjustment in font units, or 0 if no pair exists.
131 pub fn kern_value(&self, left: u16, right: u16) -> i16 {
132 self.pairs
133 .binary_search_by(|pair| pair.left.cmp(&left).then_with(|| pair.right.cmp(&right)))
134 .map(|idx| self.pairs[idx].value)
135 .unwrap_or(0)
136 }
137
138 /// Returns the number of kerning pairs.
139 pub fn num_pairs(&self) -> usize {
140 self.pairs.len()
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn empty_kern_table() {
150 // Version 0, 0 subtables.
151 let data = [0u8, 0, 0, 0];
152 let kern = KernTable::parse(&data).unwrap();
153 assert_eq!(kern.num_pairs(), 0);
154 assert_eq!(kern.kern_value(1, 2), 0);
155 }
156
157 #[test]
158 fn kern_value_lookup() {
159 // Build a minimal version 0 kern table with format 0 subtable.
160 let mut data = Vec::new();
161
162 // Header: version=0, nTables=1
163 data.extend_from_slice(&[0, 0, 0, 1]);
164
165 // Subtable header: version=0, length=20, coverage=0x0001 (horizontal, format 0)
166 data.extend_from_slice(&[0, 0, 0, 20, 0, 1]);
167
168 // Format 0 header: nPairs=1, searchRange=6, entrySelector=0, rangeShift=0
169 data.extend_from_slice(&[0, 1, 0, 6, 0, 0, 0, 0]);
170
171 // One pair: left=10, right=20, value=-50
172 data.extend_from_slice(&10u16.to_be_bytes());
173 data.extend_from_slice(&20u16.to_be_bytes());
174 data.extend_from_slice(&(-50i16).to_be_bytes());
175
176 let kern = KernTable::parse(&data).unwrap();
177 assert_eq!(kern.num_pairs(), 1);
178 assert_eq!(kern.kern_value(10, 20), -50);
179 assert_eq!(kern.kern_value(10, 21), 0);
180 assert_eq!(kern.kern_value(11, 20), 0);
181 }
182}