we (web engine): Experimental web browser project to understand the limits of Claude
at encoding-sniffing 182 lines 5.8 kB view raw
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}