//! `kern` — Kerning table. //! //! Contains kerning pair adjustments for glyph spacing. //! Supports the classic Microsoft kern table (version 0) with format 0 subtables. //! Reference: use crate::font::parse::Reader; use crate::font::FontError; /// A single kerning pair. #[derive(Debug, Clone, Copy)] struct KernPair { /// Left glyph ID. left: u16, /// Right glyph ID. right: u16, /// Kerning value in font units (positive = move apart, negative = move together). value: i16, } /// Parsed `kern` table. #[derive(Debug)] pub struct KernTable { /// Sorted list of kerning pairs (for binary search). pairs: Vec, } impl KernTable { /// Create an empty kern table (no pairs). pub fn empty() -> KernTable { KernTable { pairs: Vec::new() } } /// Parse the `kern` table from raw bytes. pub fn parse(data: &[u8]) -> Result { let r = Reader::new(data); if r.len() < 4 { return Err(FontError::MalformedTable("kern")); } let version = r.u16(0)?; match version { 0 => Self::parse_version0(data), _ => { // Version 1 (Apple AAT) or unknown — try parsing as version 0 // since some fonts mislabel the version. If that fails, return // an empty table (kerning is optional, not critical). Ok(KernTable { pairs: Vec::new() }) } } } /// Parse a version 0 kern table (Microsoft format). fn parse_version0(data: &[u8]) -> Result { let r = Reader::new(data); let n_tables = r.u16(2)? as usize; let mut pairs = Vec::new(); let mut offset = 4; // Skip version + nTables for _ in 0..n_tables { if offset + 6 > r.len() { break; } let _subtable_version = r.u16(offset)?; let subtable_length = r.u16(offset + 2)? as usize; let coverage = r.u16(offset + 4)?; // Coverage field: // Bit 0: 1 = horizontal kerning // Bit 1: 1 = minimum values (not kerning values) // Bit 2: 1 = cross-stream // Bits 8-15: format number let is_horizontal = coverage & 0x0001 != 0; let is_minimum = coverage & 0x0002 != 0; let is_cross_stream = coverage & 0x0004 != 0; let format = (coverage >> 8) as u8; // We only support horizontal kerning, format 0, non-minimum, non-cross-stream. if format == 0 && is_horizontal && !is_minimum && !is_cross_stream { Self::parse_format0(data, offset + 6, &mut pairs)?; } // Advance to next subtable. if subtable_length == 0 { break; } offset += subtable_length; } // Sort pairs for binary search. pairs.sort_by(|a, b| a.left.cmp(&b.left).then_with(|| a.right.cmp(&b.right))); Ok(KernTable { pairs }) } /// Parse a format 0 subtable (sorted pairs). fn parse_format0( data: &[u8], offset: usize, pairs: &mut Vec, ) -> Result<(), FontError> { let r = Reader::new(data); if offset + 8 > r.len() { return Err(FontError::MalformedTable("kern")); } let n_pairs = r.u16(offset)? as usize; // Skip searchRange(2), entrySelector(2), rangeShift(2) = 6 bytes. let pair_offset = offset + 8; for i in 0..n_pairs { let base = pair_offset + i * 6; if base + 6 > r.len() { break; } let left = r.u16(base)?; let right = r.u16(base + 2)?; let value = r.i16(base + 4)?; pairs.push(KernPair { left, right, value }); } Ok(()) } /// Look up the kerning value for a pair of glyph IDs. /// /// Returns the kerning adjustment in font units, or 0 if no pair exists. pub fn kern_value(&self, left: u16, right: u16) -> i16 { self.pairs .binary_search_by(|pair| pair.left.cmp(&left).then_with(|| pair.right.cmp(&right))) .map(|idx| self.pairs[idx].value) .unwrap_or(0) } /// Returns the number of kerning pairs. pub fn num_pairs(&self) -> usize { self.pairs.len() } } #[cfg(test)] mod tests { use super::*; #[test] fn empty_kern_table() { // Version 0, 0 subtables. let data = [0u8, 0, 0, 0]; let kern = KernTable::parse(&data).unwrap(); assert_eq!(kern.num_pairs(), 0); assert_eq!(kern.kern_value(1, 2), 0); } #[test] fn kern_value_lookup() { // Build a minimal version 0 kern table with format 0 subtable. let mut data = Vec::new(); // Header: version=0, nTables=1 data.extend_from_slice(&[0, 0, 0, 1]); // Subtable header: version=0, length=20, coverage=0x0001 (horizontal, format 0) data.extend_from_slice(&[0, 0, 0, 20, 0, 1]); // Format 0 header: nPairs=1, searchRange=6, entrySelector=0, rangeShift=0 data.extend_from_slice(&[0, 1, 0, 6, 0, 0, 0, 0]); // One pair: left=10, right=20, value=-50 data.extend_from_slice(&10u16.to_be_bytes()); data.extend_from_slice(&20u16.to_be_bytes()); data.extend_from_slice(&(-50i16).to_be_bytes()); let kern = KernTable::parse(&data).unwrap(); assert_eq!(kern.num_pairs(), 1); assert_eq!(kern.kern_value(10, 20), -50); assert_eq!(kern.kern_value(10, 21), 0); assert_eq!(kern.kern_value(11, 20), 0); } }