Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974//! OTF/TTF font file parser.//!//! Parses the OpenType/TrueType table directory and individual tables needed//! for text rendering: head, maxp, hhea, hmtx, cmap, name, loca.
use std::cell::RefCell;use std::fmt;
pub mod cache;mod parse;pub mod rasterizer;pub mod registry;mod tables;
pub use cache::GlyphCache;pub use rasterizer::GlyphBitmap;pub use registry::{FontEntry, FontRegistry};pub use tables::cmap::CmapTable;pub use tables::glyf::{Contour, GlyphOutline, Point};pub use tables::head::HeadTable;pub use tables::hhea::HheaTable;pub use tables::hmtx::HmtxTable;pub use tables::kern::KernTable;pub use tables::loca::LocaTable;pub use tables::maxp::MaxpTable;pub use tables::name::NameTable;pub use tables::os2::Os2Table;
/// A positioned glyph in a shaped text run.#[derive(Debug, Clone)]pub struct ShapedGlyph { /// Glyph ID in the font. pub glyph_id: u16, /// Horizontal position in pixels. pub x_offset: f32, /// Vertical offset in pixels (usually 0 for basic horizontal shaping). pub y_offset: f32, /// Horizontal advance in pixels. pub x_advance: f32,}
/// A positioned glyph with its rasterized bitmap, ready for rendering.#[derive(Debug, Clone)]pub struct PositionedGlyph { /// Glyph ID in the font. pub glyph_id: u16, /// Horizontal position in pixels (left edge of the glyph's origin). pub x: f32, /// Vertical position in pixels (baseline). pub y: f32, /// The rasterized bitmap, or `None` for glyphs with no outline (e.g., space). pub bitmap: Option<GlyphBitmap>,}
/// Errors that can occur during font parsing.#[derive(Debug)]pub enum FontError { /// The data is too short to contain the expected structure. UnexpectedEof, /// The font file has an unrecognized magic number / sfVersion. InvalidMagic(u32), /// A required table is missing. MissingTable(&'static str), /// A table's data is malformed. MalformedTable(&'static str),}
impl fmt::Display for FontError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { FontError::UnexpectedEof => write!(f, "unexpected end of font data"), FontError::InvalidMagic(v) => write!(f, "invalid font magic: 0x{:08X}", v), FontError::MissingTable(t) => write!(f, "missing required table: {}", t), FontError::MalformedTable(t) => write!(f, "malformed table: {}", t), } }}
/// A record in the table directory describing one font table.#[derive(Debug, Clone)]pub struct TableRecord { /// Four-byte tag (e.g. b"head", b"cmap"). pub tag: [u8; 4], /// Checksum of the table. pub checksum: u32, /// Offset from the beginning of the font file. pub offset: u32, /// Length of the table in bytes. pub length: u32,}
impl TableRecord { /// Return the tag as a string (for display/debugging). pub fn tag_str(&self) -> &str { std::str::from_utf8(&self.tag).unwrap_or("????") }}
/// A parsed OpenType/TrueType font.pub struct Font { /// Raw font data (owned). data: Vec<u8>, /// Offset subtable version (0x00010000 for TrueType, 0x4F54544F for CFF). pub sf_version: u32, /// Table directory records. pub tables: Vec<TableRecord>, /// Cache of rasterized glyph bitmaps, keyed by (glyph_id, size_px). glyph_cache: RefCell<GlyphCache>,}
impl fmt::Debug for Font { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Font") .field("sf_version", &self.sf_version) .field("tables", &self.tables) .field("glyph_cache_size", &self.glyph_cache.borrow().len()) .finish() }}
impl Font { /// Parse a font from raw file bytes. pub fn parse(data: Vec<u8>) -> Result<Font, FontError> { let r = parse::Reader::new(&data);
let sf_version = r.u32(0)?; match sf_version { 0x00010000 => {} // TrueType 0x4F54544F => {} // CFF (OpenType with PostScript outlines) 0x74727565 => {} // 'true' — old Apple TrueType _ => return Err(FontError::InvalidMagic(sf_version)), }
let num_tables = r.u16(4)? as usize; // skip searchRange(2), entrySelector(2), rangeShift(2) = 6 bytes let mut tables = Vec::with_capacity(num_tables); for i in 0..num_tables { let base = 12 + i * 16; let tag = r.tag(base)?; let checksum = r.u32(base + 4)?; let offset = r.u32(base + 8)?; let length = r.u32(base + 12)?; tables.push(TableRecord { tag, checksum, offset, length, }); }
Ok(Font { data, sf_version, tables, glyph_cache: RefCell::new(GlyphCache::new()), }) }
/// Load a font from a file path. pub fn from_file(path: &std::path::Path) -> Result<Font, FontError> { let data = std::fs::read(path).map_err(|_| FontError::UnexpectedEof)?; Font::parse(data) }
/// Find a table record by its 4-byte tag. pub fn table_record(&self, tag: &[u8; 4]) -> Option<&TableRecord> { self.tables.iter().find(|t| &t.tag == tag) }
/// Get the raw bytes for a table. pub fn table_data(&self, tag: &[u8; 4]) -> Option<&[u8]> { let rec = self.table_record(tag)?; let start = rec.offset as usize; let end = start + rec.length as usize; if end <= self.data.len() { Some(&self.data[start..end]) } else { None } }
/// Parse the `head` table. pub fn head(&self) -> Result<HeadTable, FontError> { let data = self .table_data(b"head") .ok_or(FontError::MissingTable("head"))?; HeadTable::parse(data) }
/// Parse the `maxp` table. pub fn maxp(&self) -> Result<MaxpTable, FontError> { let data = self .table_data(b"maxp") .ok_or(FontError::MissingTable("maxp"))?; MaxpTable::parse(data) }
/// Parse the `hhea` table. pub fn hhea(&self) -> Result<HheaTable, FontError> { let data = self .table_data(b"hhea") .ok_or(FontError::MissingTable("hhea"))?; HheaTable::parse(data) }
/// Parse the `hmtx` table. /// /// Requires `maxp` and `hhea` to determine dimensions. pub fn hmtx(&self) -> Result<HmtxTable, FontError> { let maxp = self.maxp()?; let hhea = self.hhea()?; let data = self .table_data(b"hmtx") .ok_or(FontError::MissingTable("hmtx"))?; HmtxTable::parse(data, hhea.num_long_hor_metrics, maxp.num_glyphs) }
/// Parse the `cmap` table. pub fn cmap(&self) -> Result<CmapTable, FontError> { let data = self .table_data(b"cmap") .ok_or(FontError::MissingTable("cmap"))?; CmapTable::parse(data) }
/// Parse the `name` table. pub fn name(&self) -> Result<NameTable, FontError> { let data = self .table_data(b"name") .ok_or(FontError::MissingTable("name"))?; NameTable::parse(data) }
/// Parse the `loca` table. /// /// Requires `head` (for index format) and `maxp` (for glyph count). pub fn loca(&self) -> Result<LocaTable, FontError> { let head = self.head()?; let maxp = self.maxp()?; let data = self .table_data(b"loca") .ok_or(FontError::MissingTable("loca"))?; LocaTable::parse(data, head.index_to_loc_format, maxp.num_glyphs) }
/// Parse the `OS/2` table. pub fn os2(&self) -> Result<Os2Table, FontError> { let data = self .table_data(b"OS/2") .ok_or(FontError::MissingTable("OS/2"))?; Os2Table::parse(data) }
/// Parse the `kern` table, if present. /// /// Returns an empty `KernTable` (zero pairs) if the font has no kern table. pub fn kern(&self) -> Result<KernTable, FontError> { match self.table_data(b"kern") { Some(data) => KernTable::parse(data), None => Ok(KernTable::empty()), } }
/// Look up the raw kerning value for a pair of glyph IDs (in font units). /// /// Returns 0 if the font has no kern table or the pair has no adjustment. pub fn kern_pair(&self, left: u16, right: u16) -> i16 { self.kern().map(|k| k.kern_value(left, right)).unwrap_or(0) }
/// Shape a text string: map characters to glyphs and compute positions. /// /// Returns a list of positioned glyphs with coordinates in pixels. pub fn shape_text(&self, text: &str, size_px: f32) -> Vec<ShapedGlyph> { let head = match self.head() { Ok(h) => h, Err(_) => return Vec::new(), }; let scale = size_px / head.units_per_em as f32;
let cmap = match self.cmap() { Ok(c) => c, Err(_) => return Vec::new(), }; let hmtx = match self.hmtx() { Ok(h) => h, Err(_) => return Vec::new(), }; let kern = self.kern().unwrap_or_else(|_| KernTable::empty());
// Map characters to glyph IDs. let glyph_ids: Vec<u16> = text .chars() .map(|ch| cmap.glyph_index(ch as u32).unwrap_or(0)) .collect();
let mut result = Vec::with_capacity(glyph_ids.len()); let mut cursor_x: f32 = 0.0;
for (i, &gid) in glyph_ids.iter().enumerate() { let advance_fu = hmtx.advances.get(gid as usize).copied().unwrap_or(0); let x_advance = advance_fu as f32 * scale;
// Apply kerning adjustment with the next glyph. let kern_adjust = if i + 1 < glyph_ids.len() { kern.kern_value(gid, glyph_ids[i + 1]) as f32 * scale } else { 0.0 };
result.push(ShapedGlyph { glyph_id: gid, x_offset: cursor_x, y_offset: 0.0, x_advance, });
cursor_x += x_advance + kern_adjust; }
result }
/// Map a Unicode code point to a glyph index using the cmap table. pub fn glyph_index(&self, codepoint: u32) -> Result<Option<u16>, FontError> { let cmap = self.cmap()?; Ok(cmap.glyph_index(codepoint)) }
/// Extract the outline for a glyph by its glyph ID. /// /// Returns `None` for glyphs with no outline (e.g., space). /// Requires the `glyf` and `loca` tables. pub fn glyph_outline(&self, glyph_id: u16) -> Result<Option<GlyphOutline>, FontError> { let loca = self.loca()?; let glyf_data = self .table_data(b"glyf") .ok_or(FontError::MissingTable("glyf"))?; tables::glyf::parse_glyph(glyph_id, glyf_data, &loca) }
/// Rasterize a glyph outline into an anti-aliased bitmap at the given pixel size. pub fn rasterize_glyph(&self, glyph_id: u16, size_px: f32) -> Option<GlyphBitmap> { let head = self.head().ok()?; let scale = size_px / head.units_per_em as f32; let outline = self.glyph_outline(glyph_id).ok()??; rasterizer::rasterize(&outline, scale) }
/// Get a rasterized glyph bitmap, using the cache to avoid re-rasterization. /// /// The pixel size is quantized to the nearest integer to bound cache size. /// Returns `None` for glyphs with no outline (e.g., space). pub fn get_glyph_bitmap(&self, glyph_id: u16, size_px: f32) -> Option<GlyphBitmap> { let size_key = GlyphCache::quantize_size(size_px);
// Check cache first. if let Some(bitmap) = self.glyph_cache.borrow().get(glyph_id, size_key) { return Some(bitmap.clone()); }
// Cache miss: rasterize using the quantized size for consistency. let bitmap = self.rasterize_glyph(glyph_id, size_key as f32)?;
self.glyph_cache .borrow_mut() .insert(glyph_id, size_key, bitmap.clone()); Some(bitmap) }
/// Render a text string: shape, rasterize, and position glyphs. /// /// Combines text shaping (advance widths + kerning) with cached glyph /// rasterization. Each `PositionedGlyph` contains its screen position /// and the rasterized bitmap data. pub fn render_text(&self, text: &str, size_px: f32) -> Vec<PositionedGlyph> { let shaped = self.shape_text(text, size_px);
shaped .iter() .map(|sg| { let bitmap = self.get_glyph_bitmap(sg.glyph_id, size_px); PositionedGlyph { glyph_id: sg.glyph_id, x: sg.x_offset, y: sg.y_offset, bitmap, } }) .collect() }
/// Number of cached glyph bitmaps. pub fn glyph_cache_len(&self) -> usize { self.glyph_cache.borrow().len() }
/// Returns true if this is a TrueType font (vs CFF/PostScript outlines). pub fn is_truetype(&self) -> bool { self.sf_version == 0x00010000 || self.sf_version == 0x74727565 }}
/// Load the first available system font from standard macOS paths.////// Tries these fonts in order: Geneva.ttf, Helvetica.ttc, Monaco.ttf./// For `.ttc` (TrueType Collection) files, only the first font is parsed.pub fn load_system_font() -> Result<Font, FontError> { let candidates = [ "/System/Library/Fonts/Geneva.ttf", "/System/Library/Fonts/Monaco.ttf", ]; for path in &candidates { let p = std::path::Path::new(path); if p.exists() { return Font::from_file(p); } } Err(FontError::MissingTable("no system font found"))}
#[cfg(test)]mod tests { use super::*;
fn test_font() -> Font { // Try several common macOS fonts. let paths = [ "/System/Library/Fonts/Geneva.ttf", "/System/Library/Fonts/Monaco.ttf", "/System/Library/Fonts/Keyboard.ttf", ]; for path in &paths { let p = std::path::Path::new(path); if p.exists() { return Font::from_file(p).expect("failed to parse font"); } } panic!("no test font found — need a .ttf file in /System/Library/Fonts/"); }
#[test] fn parse_table_directory() { let font = test_font(); assert!(font.is_truetype()); assert!(!font.tables.is_empty()); // Every font must have these tables. assert!(font.table_record(b"head").is_some(), "missing head table"); assert!(font.table_record(b"cmap").is_some(), "missing cmap table"); assert!(font.table_record(b"maxp").is_some(), "missing maxp table"); }
#[test] fn parse_head_table() { let font = test_font(); let head = font.head().expect("failed to parse head"); assert!( head.units_per_em > 0, "units_per_em should be positive: {}", head.units_per_em ); assert!( head.units_per_em >= 16 && head.units_per_em <= 16384, "units_per_em out of range: {}", head.units_per_em ); }
#[test] fn parse_maxp_table() { let font = test_font(); let maxp = font.maxp().expect("failed to parse maxp"); assert!(maxp.num_glyphs > 0, "font should have at least one glyph"); }
#[test] fn parse_hhea_table() { let font = test_font(); let hhea = font.hhea().expect("failed to parse hhea"); assert!(hhea.ascent > 0, "ascent should be positive"); assert!(hhea.num_long_hor_metrics > 0, "should have metrics"); }
#[test] fn parse_hmtx_table() { let font = test_font(); let hmtx = font.hmtx().expect("failed to parse hmtx"); let maxp = font.maxp().unwrap(); assert_eq!( hmtx.advances.len(), maxp.num_glyphs as usize, "should have one advance per glyph" ); assert_eq!( hmtx.lsbs.len(), maxp.num_glyphs as usize, "should have one lsb per glyph" ); // Glyph 0 (.notdef) typically has a nonzero advance. assert!(hmtx.advances[0] > 0, "glyph 0 advance should be nonzero"); }
#[test] fn parse_cmap_table() { let font = test_font(); let cmap = font.cmap().expect("failed to parse cmap");
// Look up ASCII 'A' (U+0041) — every Latin font should have it. let glyph_a = cmap.glyph_index(0x0041); assert!( glyph_a.is_some() && glyph_a.unwrap() > 0, "should find a glyph for 'A'" );
// Look up space (U+0020). let glyph_space = cmap.glyph_index(0x0020); assert!(glyph_space.is_some(), "should find a glyph for space"); }
#[test] fn parse_name_table() { let font = test_font(); let name = font.name().expect("failed to parse name"); let family = name.family_name(); assert!(family.is_some(), "should have a family name"); let family = family.unwrap(); assert!(!family.is_empty(), "family name should not be empty"); }
#[test] fn parse_loca_table() { let font = test_font(); let loca = font.loca().expect("failed to parse loca"); let maxp = font.maxp().unwrap(); // loca has num_glyphs + 1 entries. assert_eq!( loca.offsets.len(), maxp.num_glyphs as usize + 1, "loca should have num_glyphs + 1 entries" ); }
#[test] fn glyph_index_lookup() { let font = test_font(); // 'A' should map to a nonzero glyph. let gid = font.glyph_index(0x0041).expect("glyph_index failed"); assert!(gid.is_some() && gid.unwrap() > 0);
// A private-use code point likely has no glyph. let gid_pua = font.glyph_index(0xFFFD).expect("glyph_index failed"); // FFFD (replacement char) might or might not exist — just check no crash. let _ = gid_pua; }
#[test] fn load_system_font_works() { // This test may fail in CI where no fonts are installed, // but should pass on macOS. if std::path::Path::new("/System/Library/Fonts/Geneva.ttf").exists() || std::path::Path::new("/System/Library/Fonts/Monaco.ttf").exists() { let font = load_system_font().expect("should load a system font"); assert!(!font.tables.is_empty()); } }
#[test] fn parse_os2_table() { let font = test_font(); let os2 = font.os2().expect("failed to parse OS/2"); // Weight class should be in valid range (100–900). assert!( os2.us_weight_class >= 100 && os2.us_weight_class <= 900, "weight class out of range: {}", os2.us_weight_class ); // Width class should be 1–9. assert!( os2.us_width_class >= 1 && os2.us_width_class <= 9, "width class out of range: {}", os2.us_width_class ); // Typo ascender should be positive for normal fonts. assert!( os2.s_typo_ascender > 0, "sTypoAscender should be positive: {}", os2.s_typo_ascender ); // Typo descender should be negative or zero. assert!( os2.s_typo_descender <= 0, "sTypoDescender should be <= 0: {}", os2.s_typo_descender ); }
#[test] fn parse_os2_version_2_fields() { let font = test_font(); let os2 = font.os2().expect("failed to parse OS/2"); if os2.version >= 2 { // sxHeight and sCapHeight should be non-negative for version >= 2. assert!( os2.sx_height >= 0, "sxHeight should be >= 0: {}", os2.sx_height ); assert!( os2.s_cap_height >= 0, "sCapHeight should be >= 0: {}", os2.s_cap_height ); } }
#[test] fn glyph_outline_simple() { let font = test_font(); // Get glyph ID for 'A'. let gid = font .glyph_index(0x0041) .expect("glyph_index failed") .expect("no glyph for 'A'"); let outline = font .glyph_outline(gid) .expect("glyph_outline failed") .expect("'A' should have an outline");
// 'A' should have at least one contour. assert!( !outline.contours.is_empty(), "'A' should have at least 1 contour" ); // Bounding box should be valid. assert!( outline.x_max >= outline.x_min, "x_max ({}) should be >= x_min ({})", outline.x_max, outline.x_min ); assert!( outline.y_max >= outline.y_min, "y_max ({}) should be >= y_min ({})", outline.y_max, outline.y_min ); // Each contour should have points. for contour in &outline.contours { assert!( !contour.points.is_empty(), "contour should have at least one point" ); } }
#[test] fn glyph_outline_space_has_no_outline() { let font = test_font(); // Space should map to a glyph but have no outline. let gid = font .glyph_index(0x0020) .expect("glyph_index failed") .expect("no glyph for space"); let outline = font.glyph_outline(gid).expect("glyph_outline failed"); assert!( outline.is_none(), "space should have no outline (got {:?})", outline ); }
#[test] fn glyph_outline_multiple_glyphs() { let font = test_font(); // Parse outlines for several ASCII characters. for &cp in &[0x0042u32, 0x0043, 0x004F, 0x0053] { // B, C, O, S let gid = font.glyph_index(cp).expect("glyph_index failed"); if let Some(gid) = gid { let result = font.glyph_outline(gid); assert!( result.is_ok(), "failed to parse outline for U+{:04X}: {:?}", cp, result.err() ); } } }
#[test] fn glyph_outline_has_on_curve_points() { let font = test_font(); let gid = font .glyph_index(0x0041) .expect("glyph_index failed") .expect("no glyph for 'A'"); let outline = font .glyph_outline(gid) .expect("glyph_outline failed") .expect("'A' should have an outline");
// Every contour should have at least some on-curve points. for contour in &outline.contours { let on_curve_count = contour.points.iter().filter(|p| p.on_curve).count(); assert!( on_curve_count > 0, "contour should have at least one on-curve point" ); } }
#[test] fn glyph_outline_notdef() { let font = test_font(); // Glyph 0 (.notdef) usually has an outline (a rectangle or empty box). let result = font.glyph_outline(0); assert!(result.is_ok(), ".notdef outline parse should not error"); // .notdef may or may not have an outline — just verify no crash. }
#[test] fn kern_table_loads() { let font = test_font(); // kern table is optional — just verify parsing doesn't error. let kern = font.kern(); assert!(kern.is_ok(), "kern() should not error: {:?}", kern.err()); }
#[test] fn kern_pair_no_crash() { let font = test_font(); let gid_a = font.glyph_index(0x0041).unwrap().unwrap_or(0); let gid_v = font.glyph_index(0x0056).unwrap().unwrap_or(0); // Just verify no crash; value may be 0 if font has no kern table. let _val = font.kern_pair(gid_a, gid_v); }
#[test] fn shape_text_basic() { let font = test_font(); let shaped = font.shape_text("Hello", 16.0);
assert_eq!(shaped.len(), 5, "should have 5 glyphs for 'Hello'");
// All glyph IDs should be nonzero (font has Latin glyphs). for (i, g) in shaped.iter().enumerate() { assert!(g.glyph_id > 0, "glyph {} should have nonzero ID", i); }
// Positions should be monotonically increasing. for i in 1..shaped.len() { assert!( shaped[i].x_offset > shaped[i - 1].x_offset, "glyph {} x_offset ({}) should be > glyph {} x_offset ({})", i, shaped[i].x_offset, i - 1, shaped[i - 1].x_offset ); }
// First glyph should start at 0. assert_eq!(shaped[0].x_offset, 0.0, "first glyph should start at x=0");
// All advances should be positive. for (i, g) in shaped.iter().enumerate() { assert!( g.x_advance > 0.0, "glyph {} advance ({}) should be positive", i, g.x_advance ); } }
#[test] fn shape_text_empty() { let font = test_font(); let shaped = font.shape_text("", 16.0); assert!(shaped.is_empty(), "empty text should produce no glyphs"); }
#[test] fn shape_text_single_char() { let font = test_font(); let shaped = font.shape_text("A", 16.0); assert_eq!(shaped.len(), 1); assert_eq!(shaped[0].x_offset, 0.0); assert!(shaped[0].x_advance > 0.0); }
#[test] fn shape_text_space_has_advance() { let font = test_font(); let shaped = font.shape_text(" ", 16.0); assert_eq!(shaped.len(), 1); // Space should have an advance width (it occupies horizontal space). assert!( shaped[0].x_advance > 0.0, "space should have positive advance: {}", shaped[0].x_advance ); }
#[test] fn shape_text_scales_with_size() { let font = test_font(); let small = font.shape_text("A", 12.0); let large = font.shape_text("A", 24.0);
// At double the size, the advance should be double. let ratio = large[0].x_advance / small[0].x_advance; assert!( (ratio - 2.0).abs() < 0.01, "advance ratio should be ~2.0, got {}", ratio ); }
#[test] fn shape_text_fonts_without_kern_work() { let font = test_font(); // Even if the font has no kern table, shaping should work fine. let shaped = font.shape_text("AV", 16.0); assert_eq!(shaped.len(), 2); assert!(shaped[1].x_offset > 0.0); }
#[test] fn get_glyph_bitmap_caches() { let font = test_font(); let gid = font.glyph_index(0x0041).unwrap().expect("no glyph for 'A'");
assert_eq!(font.glyph_cache_len(), 0, "cache should start empty");
// First call: cache miss → rasterize. let bm1 = font.get_glyph_bitmap(gid, 16.0).expect("should rasterize"); assert_eq!(font.glyph_cache_len(), 1, "cache should have 1 entry");
// Second call: cache hit → same result, no new entry. let bm2 = font.get_glyph_bitmap(gid, 16.0).expect("should hit cache"); assert_eq!(font.glyph_cache_len(), 1, "cache size should not change"); assert_eq!(bm1, bm2, "cached bitmap should be identical"); }
#[test] fn get_glyph_bitmap_different_sizes() { let font = test_font(); let gid = font.glyph_index(0x0041).unwrap().expect("no glyph for 'A'");
let _bm16 = font.get_glyph_bitmap(gid, 16.0); let _bm32 = font.get_glyph_bitmap(gid, 32.0);
assert_eq!( font.glyph_cache_len(), 2, "different sizes should be cached independently" ); }
#[test] fn get_glyph_bitmap_quantizes_size() { let font = test_font(); let gid = font.glyph_index(0x0041).unwrap().expect("no glyph for 'A'");
// 16.3 and 15.7 both round to 16. let bm1 = font.get_glyph_bitmap(gid, 16.3); let bm2 = font.get_glyph_bitmap(gid, 15.7);
assert_eq!( font.glyph_cache_len(), 1, "quantized sizes should share a cache entry" ); assert_eq!(bm1, bm2, "same quantized size should produce same bitmap"); }
#[test] fn get_glyph_bitmap_space_returns_none() { let font = test_font(); let gid = font .glyph_index(0x0020) .unwrap() .expect("no glyph for space");
let bitmap = font.get_glyph_bitmap(gid, 16.0); assert!(bitmap.is_none(), "space should have no bitmap"); }
#[test] fn render_text_basic() { let font = test_font(); let glyphs = font.render_text("Hi", 16.0);
assert_eq!(glyphs.len(), 2, "should have 2 glyphs for 'Hi'");
// First glyph should start at x=0. assert_eq!(glyphs[0].x, 0.0, "first glyph should start at x=0");
// Second glyph should be to the right. assert!(glyphs[1].x > 0.0, "second glyph should be offset right");
// 'H' and 'i' should have bitmaps. assert!(glyphs[0].bitmap.is_some(), "'H' should have a bitmap"); assert!(glyphs[1].bitmap.is_some(), "'i' should have a bitmap"); }
#[test] fn render_text_uses_cache() { let font = test_font();
// Render "AA" — same glyph twice, should only rasterize once. let glyphs = font.render_text("AA", 16.0);
assert_eq!(glyphs.len(), 2); // Both should have the same bitmap (from cache). assert_eq!( glyphs[0].bitmap, glyphs[1].bitmap, "repeated glyph should return identical bitmaps from cache" ); // Only one entry in the cache for 'A' at 16px. // (There may be more entries if the font maps 'A' to multiple glyphs, // but typically it's just one.) assert!( font.glyph_cache_len() >= 1, "cache should have at least 1 entry" ); }
#[test] fn render_text_empty() { let font = test_font(); let glyphs = font.render_text("", 16.0); assert!(glyphs.is_empty(), "empty text should produce no glyphs"); }
#[test] fn render_text_with_space() { let font = test_font(); let glyphs = font.render_text("A B", 16.0);
assert_eq!(glyphs.len(), 3, "should have 3 glyphs for 'A B'");
// Space glyph should have no bitmap. assert!( glyphs[1].bitmap.is_none(), "space glyph should have no bitmap" );
// But it should still advance the cursor. assert!( glyphs[2].x > glyphs[0].x, "'B' should be further right than 'A'" ); }
#[test] fn render_text_positions_match_shaping() { let font = test_font(); let shaped = font.shape_text("Hello", 16.0); let rendered = font.render_text("Hello", 16.0);
assert_eq!(shaped.len(), rendered.len());
for (s, r) in shaped.iter().zip(rendered.iter()) { assert_eq!(s.glyph_id, r.glyph_id, "glyph IDs should match"); assert_eq!(s.x_offset, r.x, "x positions should match shaping"); assert_eq!(s.y_offset, r.y, "y positions should match shaping"); } }}