···1515pub use tables::head::HeadTable;
1616pub use tables::hhea::HheaTable;
1717pub use tables::hmtx::HmtxTable;
1818+pub use tables::kern::KernTable;
1819pub use tables::loca::LocaTable;
1920pub use tables::maxp::MaxpTable;
2021pub use tables::name::NameTable;
2122pub use tables::os2::Os2Table;
2323+2424+/// A positioned glyph in a shaped text run.
2525+#[derive(Debug, Clone)]
2626+pub struct ShapedGlyph {
2727+ /// Glyph ID in the font.
2828+ pub glyph_id: u16,
2929+ /// Horizontal position in pixels.
3030+ pub x_offset: f32,
3131+ /// Vertical offset in pixels (usually 0 for basic horizontal shaping).
3232+ pub y_offset: f32,
3333+ /// Horizontal advance in pixels.
3434+ pub x_advance: f32,
3535+}
22362337/// Errors that can occur during font parsing.
2438#[derive(Debug)]
···205219 .table_data(b"OS/2")
206220 .ok_or(FontError::MissingTable("OS/2"))?;
207221 Os2Table::parse(data)
222222+ }
223223+224224+ /// Parse the `kern` table, if present.
225225+ ///
226226+ /// Returns an empty `KernTable` (zero pairs) if the font has no kern table.
227227+ pub fn kern(&self) -> Result<KernTable, FontError> {
228228+ match self.table_data(b"kern") {
229229+ Some(data) => KernTable::parse(data),
230230+ None => Ok(KernTable::empty()),
231231+ }
232232+ }
233233+234234+ /// Look up the raw kerning value for a pair of glyph IDs (in font units).
235235+ ///
236236+ /// Returns 0 if the font has no kern table or the pair has no adjustment.
237237+ pub fn kern_pair(&self, left: u16, right: u16) -> i16 {
238238+ self.kern().map(|k| k.kern_value(left, right)).unwrap_or(0)
239239+ }
240240+241241+ /// Shape a text string: map characters to glyphs and compute positions.
242242+ ///
243243+ /// Returns a list of positioned glyphs with coordinates in pixels.
244244+ pub fn shape_text(&self, text: &str, size_px: f32) -> Vec<ShapedGlyph> {
245245+ let head = match self.head() {
246246+ Ok(h) => h,
247247+ Err(_) => return Vec::new(),
248248+ };
249249+ let scale = size_px / head.units_per_em as f32;
250250+251251+ let cmap = match self.cmap() {
252252+ Ok(c) => c,
253253+ Err(_) => return Vec::new(),
254254+ };
255255+ let hmtx = match self.hmtx() {
256256+ Ok(h) => h,
257257+ Err(_) => return Vec::new(),
258258+ };
259259+ let kern = self.kern().unwrap_or_else(|_| KernTable::empty());
260260+261261+ // Map characters to glyph IDs.
262262+ let glyph_ids: Vec<u16> = text
263263+ .chars()
264264+ .map(|ch| cmap.glyph_index(ch as u32).unwrap_or(0))
265265+ .collect();
266266+267267+ let mut result = Vec::with_capacity(glyph_ids.len());
268268+ let mut cursor_x: f32 = 0.0;
269269+270270+ for (i, &gid) in glyph_ids.iter().enumerate() {
271271+ let advance_fu = hmtx.advances.get(gid as usize).copied().unwrap_or(0);
272272+ let x_advance = advance_fu as f32 * scale;
273273+274274+ // Apply kerning adjustment with the next glyph.
275275+ let kern_adjust = if i + 1 < glyph_ids.len() {
276276+ kern.kern_value(gid, glyph_ids[i + 1]) as f32 * scale
277277+ } else {
278278+ 0.0
279279+ };
280280+281281+ result.push(ShapedGlyph {
282282+ glyph_id: gid,
283283+ x_offset: cursor_x,
284284+ y_offset: 0.0,
285285+ x_advance,
286286+ });
287287+288288+ cursor_x += x_advance + kern_adjust;
289289+ }
290290+291291+ result
208292 }
209293210294 /// Map a Unicode code point to a glyph index using the cmap table.
···556640 let result = font.glyph_outline(0);
557641 assert!(result.is_ok(), ".notdef outline parse should not error");
558642 // .notdef may or may not have an outline — just verify no crash.
643643+ }
644644+645645+ #[test]
646646+ fn kern_table_loads() {
647647+ let font = test_font();
648648+ // kern table is optional — just verify parsing doesn't error.
649649+ let kern = font.kern();
650650+ assert!(kern.is_ok(), "kern() should not error: {:?}", kern.err());
651651+ }
652652+653653+ #[test]
654654+ fn kern_pair_no_crash() {
655655+ let font = test_font();
656656+ let gid_a = font.glyph_index(0x0041).unwrap().unwrap_or(0);
657657+ let gid_v = font.glyph_index(0x0056).unwrap().unwrap_or(0);
658658+ // Just verify no crash; value may be 0 if font has no kern table.
659659+ let _val = font.kern_pair(gid_a, gid_v);
660660+ }
661661+662662+ #[test]
663663+ fn shape_text_basic() {
664664+ let font = test_font();
665665+ let shaped = font.shape_text("Hello", 16.0);
666666+667667+ assert_eq!(shaped.len(), 5, "should have 5 glyphs for 'Hello'");
668668+669669+ // All glyph IDs should be nonzero (font has Latin glyphs).
670670+ for (i, g) in shaped.iter().enumerate() {
671671+ assert!(g.glyph_id > 0, "glyph {} should have nonzero ID", i);
672672+ }
673673+674674+ // Positions should be monotonically increasing.
675675+ for i in 1..shaped.len() {
676676+ assert!(
677677+ shaped[i].x_offset > shaped[i - 1].x_offset,
678678+ "glyph {} x_offset ({}) should be > glyph {} x_offset ({})",
679679+ i,
680680+ shaped[i].x_offset,
681681+ i - 1,
682682+ shaped[i - 1].x_offset
683683+ );
684684+ }
685685+686686+ // First glyph should start at 0.
687687+ assert_eq!(shaped[0].x_offset, 0.0, "first glyph should start at x=0");
688688+689689+ // All advances should be positive.
690690+ for (i, g) in shaped.iter().enumerate() {
691691+ assert!(
692692+ g.x_advance > 0.0,
693693+ "glyph {} advance ({}) should be positive",
694694+ i,
695695+ g.x_advance
696696+ );
697697+ }
698698+ }
699699+700700+ #[test]
701701+ fn shape_text_empty() {
702702+ let font = test_font();
703703+ let shaped = font.shape_text("", 16.0);
704704+ assert!(shaped.is_empty(), "empty text should produce no glyphs");
705705+ }
706706+707707+ #[test]
708708+ fn shape_text_single_char() {
709709+ let font = test_font();
710710+ let shaped = font.shape_text("A", 16.0);
711711+ assert_eq!(shaped.len(), 1);
712712+ assert_eq!(shaped[0].x_offset, 0.0);
713713+ assert!(shaped[0].x_advance > 0.0);
714714+ }
715715+716716+ #[test]
717717+ fn shape_text_space_has_advance() {
718718+ let font = test_font();
719719+ let shaped = font.shape_text(" ", 16.0);
720720+ assert_eq!(shaped.len(), 1);
721721+ // Space should have an advance width (it occupies horizontal space).
722722+ assert!(
723723+ shaped[0].x_advance > 0.0,
724724+ "space should have positive advance: {}",
725725+ shaped[0].x_advance
726726+ );
727727+ }
728728+729729+ #[test]
730730+ fn shape_text_scales_with_size() {
731731+ let font = test_font();
732732+ let small = font.shape_text("A", 12.0);
733733+ let large = font.shape_text("A", 24.0);
734734+735735+ // At double the size, the advance should be double.
736736+ let ratio = large[0].x_advance / small[0].x_advance;
737737+ assert!(
738738+ (ratio - 2.0).abs() < 0.01,
739739+ "advance ratio should be ~2.0, got {}",
740740+ ratio
741741+ );
742742+ }
743743+744744+ #[test]
745745+ fn shape_text_fonts_without_kern_work() {
746746+ let font = test_font();
747747+ // Even if the font has no kern table, shaping should work fine.
748748+ let shaped = font.shape_text("AV", 16.0);
749749+ assert_eq!(shaped.len(), 2);
750750+ assert!(shaped[1].x_offset > 0.0);
559751 }
560752}
+182
crates/text/src/font/tables/kern.rs
···11+//! `kern` — Kerning table.
22+//!
33+//! Contains kerning pair adjustments for glyph spacing.
44+//! Supports the classic Microsoft kern table (version 0) with format 0 subtables.
55+//! Reference: <https://learn.microsoft.com/en-us/typography/opentype/spec/kern>
66+77+use crate::font::parse::Reader;
88+use crate::font::FontError;
99+1010+/// A single kerning pair.
1111+#[derive(Debug, Clone, Copy)]
1212+struct KernPair {
1313+ /// Left glyph ID.
1414+ left: u16,
1515+ /// Right glyph ID.
1616+ right: u16,
1717+ /// Kerning value in font units (positive = move apart, negative = move together).
1818+ value: i16,
1919+}
2020+2121+/// Parsed `kern` table.
2222+#[derive(Debug)]
2323+pub struct KernTable {
2424+ /// Sorted list of kerning pairs (for binary search).
2525+ pairs: Vec<KernPair>,
2626+}
2727+2828+impl KernTable {
2929+ /// Create an empty kern table (no pairs).
3030+ pub fn empty() -> KernTable {
3131+ KernTable { pairs: Vec::new() }
3232+ }
3333+3434+ /// Parse the `kern` table from raw bytes.
3535+ pub fn parse(data: &[u8]) -> Result<KernTable, FontError> {
3636+ let r = Reader::new(data);
3737+ if r.len() < 4 {
3838+ return Err(FontError::MalformedTable("kern"));
3939+ }
4040+4141+ let version = r.u16(0)?;
4242+4343+ match version {
4444+ 0 => Self::parse_version0(data),
4545+ _ => {
4646+ // Version 1 (Apple AAT) or unknown — try parsing as version 0
4747+ // since some fonts mislabel the version. If that fails, return
4848+ // an empty table (kerning is optional, not critical).
4949+ Ok(KernTable { pairs: Vec::new() })
5050+ }
5151+ }
5252+ }
5353+5454+ /// Parse a version 0 kern table (Microsoft format).
5555+ fn parse_version0(data: &[u8]) -> Result<KernTable, FontError> {
5656+ let r = Reader::new(data);
5757+ let n_tables = r.u16(2)? as usize;
5858+5959+ let mut pairs = Vec::new();
6060+ let mut offset = 4; // Skip version + nTables
6161+6262+ for _ in 0..n_tables {
6363+ if offset + 6 > r.len() {
6464+ break;
6565+ }
6666+6767+ let _subtable_version = r.u16(offset)?;
6868+ let subtable_length = r.u16(offset + 2)? as usize;
6969+ let coverage = r.u16(offset + 4)?;
7070+7171+ // Coverage field:
7272+ // Bit 0: 1 = horizontal kerning
7373+ // Bit 1: 1 = minimum values (not kerning values)
7474+ // Bit 2: 1 = cross-stream
7575+ // Bits 8-15: format number
7676+ let is_horizontal = coverage & 0x0001 != 0;
7777+ let is_minimum = coverage & 0x0002 != 0;
7878+ let is_cross_stream = coverage & 0x0004 != 0;
7979+ let format = (coverage >> 8) as u8;
8080+8181+ // We only support horizontal kerning, format 0, non-minimum, non-cross-stream.
8282+ if format == 0 && is_horizontal && !is_minimum && !is_cross_stream {
8383+ Self::parse_format0(data, offset + 6, &mut pairs)?;
8484+ }
8585+8686+ // Advance to next subtable.
8787+ if subtable_length == 0 {
8888+ break;
8989+ }
9090+ offset += subtable_length;
9191+ }
9292+9393+ // Sort pairs for binary search.
9494+ pairs.sort_by(|a, b| a.left.cmp(&b.left).then_with(|| a.right.cmp(&b.right)));
9595+9696+ Ok(KernTable { pairs })
9797+ }
9898+9999+ /// Parse a format 0 subtable (sorted pairs).
100100+ fn parse_format0(
101101+ data: &[u8],
102102+ offset: usize,
103103+ pairs: &mut Vec<KernPair>,
104104+ ) -> Result<(), FontError> {
105105+ let r = Reader::new(data);
106106+ if offset + 8 > r.len() {
107107+ return Err(FontError::MalformedTable("kern"));
108108+ }
109109+110110+ let n_pairs = r.u16(offset)? as usize;
111111+ // Skip searchRange(2), entrySelector(2), rangeShift(2) = 6 bytes.
112112+ let pair_offset = offset + 8;
113113+114114+ for i in 0..n_pairs {
115115+ let base = pair_offset + i * 6;
116116+ if base + 6 > r.len() {
117117+ break;
118118+ }
119119+ let left = r.u16(base)?;
120120+ let right = r.u16(base + 2)?;
121121+ let value = r.i16(base + 4)?;
122122+ pairs.push(KernPair { left, right, value });
123123+ }
124124+125125+ Ok(())
126126+ }
127127+128128+ /// Look up the kerning value for a pair of glyph IDs.
129129+ ///
130130+ /// Returns the kerning adjustment in font units, or 0 if no pair exists.
131131+ pub fn kern_value(&self, left: u16, right: u16) -> i16 {
132132+ self.pairs
133133+ .binary_search_by(|pair| pair.left.cmp(&left).then_with(|| pair.right.cmp(&right)))
134134+ .map(|idx| self.pairs[idx].value)
135135+ .unwrap_or(0)
136136+ }
137137+138138+ /// Returns the number of kerning pairs.
139139+ pub fn num_pairs(&self) -> usize {
140140+ self.pairs.len()
141141+ }
142142+}
143143+144144+#[cfg(test)]
145145+mod tests {
146146+ use super::*;
147147+148148+ #[test]
149149+ fn empty_kern_table() {
150150+ // Version 0, 0 subtables.
151151+ let data = [0u8, 0, 0, 0];
152152+ let kern = KernTable::parse(&data).unwrap();
153153+ assert_eq!(kern.num_pairs(), 0);
154154+ assert_eq!(kern.kern_value(1, 2), 0);
155155+ }
156156+157157+ #[test]
158158+ fn kern_value_lookup() {
159159+ // Build a minimal version 0 kern table with format 0 subtable.
160160+ let mut data = Vec::new();
161161+162162+ // Header: version=0, nTables=1
163163+ data.extend_from_slice(&[0, 0, 0, 1]);
164164+165165+ // Subtable header: version=0, length=20, coverage=0x0001 (horizontal, format 0)
166166+ data.extend_from_slice(&[0, 0, 0, 20, 0, 1]);
167167+168168+ // Format 0 header: nPairs=1, searchRange=6, entrySelector=0, rangeShift=0
169169+ data.extend_from_slice(&[0, 1, 0, 6, 0, 0, 0, 0]);
170170+171171+ // One pair: left=10, right=20, value=-50
172172+ data.extend_from_slice(&10u16.to_be_bytes());
173173+ data.extend_from_slice(&20u16.to_be_bytes());
174174+ data.extend_from_slice(&(-50i16).to_be_bytes());
175175+176176+ let kern = KernTable::parse(&data).unwrap();
177177+ assert_eq!(kern.num_pairs(), 1);
178178+ assert_eq!(kern.kern_value(10, 20), -50);
179179+ assert_eq!(kern.kern_value(10, 21), 0);
180180+ assert_eq!(kern.kern_value(11, 20), 0);
181181+ }
182182+}
+1
crates/text/src/font/tables/mod.rs
···55pub mod head;
66pub mod hhea;
77pub mod hmtx;
88+pub mod kern;
89pub mod loca;
910pub mod maxp;
1011pub mod name;