1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
//! `kern` — legacy kerning table (predates GPOS).
//!
//! Two on-disk header variants coexist:
//!
//! - **Microsoft / OpenType `kern`** (used by every Windows-authored
//! TTF and most Adobe / Google fonts): `u16 version` followed by
//! `u16 nTables`. The `version` field is `0`, so the first 16 bits
//! of the table read as zero.
//! - **Apple `kern`** (used by macOS-bundled TTFs and most Apple-
//! authored fonts): `u32 version` followed by `u32 nTables`. The
//! `version` field is `0x00010000`, so the first 16 bits read as
//! `0x0001` (NOT zero) — this is what distinguishes the two
//! variants at parse time.
//!
//! Per-subtable layouts differ between the two variants. The
//! Microsoft per-subtable header is `u16 version, u16 length, u16
//! coverage` (coverage's high byte carries the format, low byte the
//! flags). Apple's per-subtable header is `u32 length, u16 coverage,
//! u16 tupleIndex` and its coverage byte order is mirrored (format in
//! the low byte, flags in the high byte) — the byte-level details
//! aren't fully covered by the staged spec docs, so this parser
//! accepts the Apple header at the table level but does not decode
//! the Apple subtable bodies; an Apple-headered `kern` parses as a
//! valid table with zero pairs (lookup → 0) rather than being
//! rejected outright. Round 1 + this round therefore expose
//! Microsoft-format Format-0 kerning only.
use crate::parser::{read_i16, read_u16, read_u32};
use crate::Error;
#[derive(Debug, Clone)]
pub struct KernTable<'a> {
/// All format-0 pair lists collected at parse time, sorted by
/// `(left << 16 | right)` for binary search.
pairs: Vec<KernPair>,
/// Which on-disk header variant the input used. Distinguishing
/// the two at parse time matters because subtable layouts differ;
/// the field is also surfaced via [`KernTable::header_variant`]
/// for callers that want to know whether the source font ships an
/// Apple-format table whose per-subtable bodies this crate does
/// not decode.
variant: HeaderVariant,
_phantom: core::marker::PhantomData<&'a ()>,
}
/// Which `kern` header layout the input table uses. Exposed so callers
/// can tell apart Microsoft-format fonts (whose Format-0 subtables this
/// crate decodes) from Apple-format fonts (whose subtable bodies are
/// currently surfaced as "no kerning pairs available" rather than
/// rejected at parse time).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HeaderVariant {
/// Microsoft / OpenType layout: `u16 version` (= 0), `u16 nTables`,
/// then `nTables` subtables. Per-subtable header is `u16 version,
/// u16 length, u16 coverage`. This crate decodes Format-0
/// horizontal kerning subtables.
Microsoft,
/// Apple layout: `u32 version` (= 0x00010000), `u32 nTables`, then
/// `nTables` subtables with a different per-subtable header
/// layout. The subtable bodies are not decoded by this crate;
/// callers that need Apple-kern data should hold the fixed Apple
/// `kerx` clean-room reference and submit a follow-up.
Apple,
}
#[derive(Debug, Clone, Copy)]
struct KernPair {
key: u32,
value: i16,
}
impl<'a> KernTable<'a> {
pub fn parse(bytes: &'a [u8]) -> Result<Self, Error> {
if bytes.len() < 4 {
return Err(Error::UnexpectedEof);
}
// Sniff version. Microsoft format: `u16 version` (= 0) — first
// 16 bits read as 0. Apple format: `u32 version` (= 0x00010000,
// big-endian → bytes 00 01 00 00) — first 16 bits read as
// 0x0001 (NOT zero). The two are mutually exclusive at the
// first u16: any other value is malformed.
let v0 = read_u16(bytes, 0)?;
let (mut off, n_subtables, variant) = match v0 {
0 => {
// Microsoft layout: u16 version, u16 nTables.
let n = read_u16(bytes, 2)?;
(4usize, n as u32, HeaderVariant::Microsoft)
}
1 => {
// Apple layout: u32 version (= 0x00010000), u32 nTables.
// Confirm the low half of the version u32 is also zero
// to defuse fonts that mis-encode the field.
if bytes.len() < 8 {
return Err(Error::UnexpectedEof);
}
let v_lo = read_u16(bytes, 2)?;
if v_lo != 0 {
return Err(Error::BadStructure("kern: bad version"));
}
let n = read_u32(bytes, 4)?;
(8usize, n, HeaderVariant::Apple)
}
_ => return Err(Error::BadStructure("kern: bad version")),
};
let mut pairs = Vec::new();
if matches!(variant, HeaderVariant::Apple) {
// Apple per-subtable layout is not covered by the spec docs
// staged under `docs/text/opentype/`. Accept the table
// structurally (so the host font still parses) but do not
// walk the subtable list — the `length` field placement
// differs from the Microsoft variant and a mis-parsed walk
// would either fabricate bogus pairs or panic.
let _ = n_subtables;
let _ = off;
return Ok(Self {
pairs,
variant,
_phantom: core::marker::PhantomData,
});
}
for _ in 0..n_subtables {
// Subtable header (Microsoft format):
// u16 version, u16 length, u16 coverage.
// Coverage low byte: bit 0 = horizontal, bit 1 = minimum
// (else kerning), bit 2 = cross-stream, bit 3 = override.
// High byte: format (0..3).
if off + 6 > bytes.len() {
return Err(Error::UnexpectedEof);
}
let _sub_version = read_u16(bytes, off)?;
let length = read_u16(bytes, off + 2)? as usize;
let coverage = read_u16(bytes, off + 4)?;
let format = (coverage >> 8) & 0xFF;
// Sanity-check sub-table length so we always advance.
if length < 6 || off + length > bytes.len() {
// Malformed — bail out of the loop rather than spin.
break;
}
let next_off = off + length;
// Only horizontal kerning, only format 0, skip "minimum"
// tables (those provide a floor, not a delta).
let horizontal = (coverage & 1) != 0;
let is_kerning = (coverage & 2) == 0;
if format == 0 && horizontal && is_kerning {
parse_format0(bytes, off + 6, &mut pairs)?;
}
off = next_off;
}
pairs.sort_by_key(|p| p.key);
Ok(Self {
pairs,
variant,
_phantom: core::marker::PhantomData,
})
}
/// Which on-disk header layout the input table used. Useful for
/// callers that want to report "this font ships an Apple-format
/// `kern` whose subtable bodies are not decoded".
pub fn header_variant(&self) -> HeaderVariant {
self.variant
}
/// Number of decoded kerning pairs available for [`Self::lookup`].
/// Returns `0` for Apple-headered tables (whose subtable bodies
/// this crate does not decode) and for Microsoft-headered tables
/// that ship only non-horizontal / non-Format-0 subtables.
pub fn pair_count(&self) -> usize {
self.pairs.len()
}
/// Look up the kerning between an ordered glyph pair, in font units.
/// Returns 0 when no rule matches.
pub fn lookup(&self, left: u16, right: u16) -> i16 {
let key = ((left as u32) << 16) | right as u32;
match self.pairs.binary_search_by_key(&key, |p| p.key) {
Ok(i) => self.pairs[i].value,
Err(_) => 0,
}
}
}
fn parse_format0(bytes: &[u8], start: usize, out: &mut Vec<KernPair>) -> Result<(), Error> {
// Format-0 subtable body:
// u16 nPairs, u16 searchRange/entrySelector/rangeShift (3 * u16 — ignored).
// nPairs * (u16 left, u16 right, FWord value).
if start + 8 > bytes.len() {
return Err(Error::UnexpectedEof);
}
let n_pairs = read_u16(bytes, start)? as usize;
let mut p = start + 8;
for _ in 0..n_pairs {
if p + 6 > bytes.len() {
return Err(Error::UnexpectedEof);
}
let l = read_u16(bytes, p)?;
let r = read_u16(bytes, p + 2)?;
let v = read_i16(bytes, p + 4)?;
out.push(KernPair {
key: ((l as u32) << 16) | r as u32,
value: v,
});
p += 6;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn build_kern_with_one_pair(l: u16, r: u16, v: i16) -> Vec<u8> {
// Microsoft header.
let mut t = vec![0u8; 4];
t[0..2].copy_from_slice(&0u16.to_be_bytes()); // version
t[2..4].copy_from_slice(&1u16.to_be_bytes()); // nTables
// Subtable (header 6 + body 8 + 1*6 = 20 bytes).
let mut sub = vec![0u8; 20];
sub[0..2].copy_from_slice(&0u16.to_be_bytes()); // sub-version
sub[2..4].copy_from_slice(&20u16.to_be_bytes()); // length
// coverage = 0x0001 (horizontal, format 0)
sub[4..6].copy_from_slice(&1u16.to_be_bytes());
// body: nPairs=1
sub[6..8].copy_from_slice(&1u16.to_be_bytes());
// 6 bytes searchRange/entrySelector/rangeShift skipped
sub[14..16].copy_from_slice(&l.to_be_bytes());
sub[16..18].copy_from_slice(&r.to_be_bytes());
sub[18..20].copy_from_slice(&v.to_be_bytes());
t.extend_from_slice(&sub);
t
}
#[test]
fn round_trips_one_pair() {
let bytes = build_kern_with_one_pair(38, 57, -100);
let k = KernTable::parse(&bytes).unwrap();
assert_eq!(k.lookup(38, 57), -100);
assert_eq!(k.lookup(38, 58), 0);
assert_eq!(k.header_variant(), HeaderVariant::Microsoft);
assert_eq!(k.pair_count(), 1);
}
/// Apple-format `kern` (the layout shipped by every macOS-bundled
/// `.ttf` — Helvetica, Lucida, Times, etc.). The previous version
/// of the header sniffer matched both Microsoft and Apple on
/// `first u16 == 0` and dispatched both into the Microsoft body
/// walker; Apple's u32-wide `version` field has high u16 = `0x0001`
/// (NOT zero), so the correct dispatch picks it up here, accepts
/// the table without rejecting the host font, and exposes zero
/// kerning pairs (the subtable body layout differs from the
/// Microsoft variant and isn't decoded by this crate yet).
#[test]
fn apple_header_parses_as_empty_table() {
let mut bytes = vec![0u8; 8];
// u32 version = 0x00010000 (big-endian bytes 00 01 00 00).
bytes[0..4].copy_from_slice(&0x0001_0000u32.to_be_bytes());
// u32 nTables = 0.
bytes[4..8].copy_from_slice(&0u32.to_be_bytes());
let k = KernTable::parse(&bytes).unwrap();
assert_eq!(k.header_variant(), HeaderVariant::Apple);
assert_eq!(k.pair_count(), 0);
// Any lookup returns the no-data sentinel (0), so consumer-
// crate shapers degrade to "no legacy kerning" rather than
// panicking on an out-of-bounds slice into a misparsed body.
assert_eq!(k.lookup(38, 57), 0);
assert_eq!(k.lookup(0, 0), 0);
}
/// An Apple-headered table that claims a non-zero subtable count
/// also parses cleanly: this crate doesn't walk the Apple subtable
/// list so the bogus nTables field is harmless. The point of the
/// test is to prove the header sniff doesn't crash on the field —
/// real-world Apple `kern` tables routinely list 2-3 subtables.
#[test]
fn apple_header_with_nonzero_n_tables_parses() {
let mut bytes = vec![0u8; 8];
bytes[0..4].copy_from_slice(&0x0001_0000u32.to_be_bytes());
bytes[4..8].copy_from_slice(&3u32.to_be_bytes());
let k = KernTable::parse(&bytes).unwrap();
assert_eq!(k.header_variant(), HeaderVariant::Apple);
assert_eq!(k.pair_count(), 0);
}
/// Truncated Apple header — version reads as 0x0001 but the table
/// ends before the u32 nTables field. The parser must surface
/// `UnexpectedEof` instead of indexing out of bounds.
#[test]
fn apple_header_truncated_returns_eof() {
// Only 4 bytes — high half of the version is there (forcing
// the Apple branch), but nTables and the rest are missing.
let mut bytes = vec![0u8; 4];
bytes[0..2].copy_from_slice(&0x0001u16.to_be_bytes());
bytes[2..4].copy_from_slice(&0u16.to_be_bytes()); // version low half
assert!(matches!(
KernTable::parse(&bytes),
Err(Error::UnexpectedEof)
));
}
/// A first-u16 sentinel that's neither 0 (Microsoft) nor 0x0001
/// (Apple's version high-half) is malformed. Reject with a typed
/// `BadStructure` rather than mis-dispatching into one of the two
/// walkers and corrupting state.
#[test]
fn unknown_version_rejected() {
let mut bytes = vec![0u8; 8];
bytes[0..2].copy_from_slice(&0x1234u16.to_be_bytes());
let r = KernTable::parse(&bytes);
assert!(matches!(r, Err(Error::BadStructure(_))));
}
/// Apple version high-half matches (0x0001) but the low half of
/// the u32 version is non-zero — i.e. the value on disk is some
/// 0x0001XXXX where XXXX != 0. The real Apple `kern` table version
/// is exactly 0x00010000, so anything else is malformed and we
/// reject it as a structural error rather than dispatching into
/// the Apple body path.
#[test]
fn apple_header_with_dirty_low_half_rejected() {
let mut bytes = vec![0u8; 8];
bytes[0..2].copy_from_slice(&0x0001u16.to_be_bytes());
bytes[2..4].copy_from_slice(&0xBEEFu16.to_be_bytes()); // dirty low half
bytes[4..8].copy_from_slice(&0u32.to_be_bytes());
assert!(matches!(
KernTable::parse(&bytes),
Err(Error::BadStructure(_))
));
}
}