Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483//! Font discovery and registry.//!//! Scans system font directories, builds an index by family name and style,//! and provides font selection with fallback.
use super::{Font, FontError};use crate::font::parse::Reader;use std::collections::HashMap;use std::path::{Path, PathBuf};
/// Metadata about a single font face discovered on the system.#[derive(Debug, Clone)]pub struct FontEntry { /// File path to the font. pub path: PathBuf, /// Byte offset within the file (0 for standalone .ttf/.otf, nonzero for .ttc fonts). pub offset: u32, /// Font family name (e.g., "Helvetica"). pub family: String, /// Subfamily name (e.g., "Regular", "Bold", "Italic", "Bold Italic"). pub subfamily: String, /// True if this face is bold (from macStyle bit 0 or weight class >= 700). pub bold: bool, /// True if this face is italic (from macStyle bit 1 or subfamily heuristic). pub italic: bool,}
/// A registry of system fonts, indexed by family name.pub struct FontRegistry { /// Map from lowercase family name to a list of font entries. families: HashMap<String, Vec<FontEntry>>,}
impl Default for FontRegistry { fn default() -> Self { Self::new() }}
impl FontRegistry { /// Scan system font directories and build the registry. /// /// Scans `/System/Library/Fonts/` and `/Library/Fonts/` for .ttf, .otf, /// and .ttc files. Errors in individual files are silently skipped. pub fn new() -> FontRegistry { let mut families: HashMap<String, Vec<FontEntry>> = HashMap::new();
let dirs = [ Path::new("/System/Library/Fonts"), Path::new("/Library/Fonts"), ];
for dir in &dirs { let entries = match std::fs::read_dir(dir) { Ok(e) => e, Err(_) => continue, };
for entry in entries { let entry = match entry { Ok(e) => e, Err(_) => continue, };
let path = entry.path(); let ext = path .extension() .and_then(|e| e.to_str()) .unwrap_or("") .to_ascii_lowercase();
match ext.as_str() { "ttf" | "otf" => { if let Some(fe) = probe_single_font(&path, 0) { let key = fe.family.to_ascii_lowercase(); families.entry(key).or_default().push(fe); } } "ttc" => { if let Ok(offsets) = parse_ttc_offsets(&path) { for offset in offsets { if let Some(fe) = probe_single_font(&path, offset) { let key = fe.family.to_ascii_lowercase(); families.entry(key).or_default().push(fe); } } } } _ => {} } } }
FontRegistry { families } }
/// Find a font by family name. Returns the first match (prefers Regular). /// /// The family name match is case-insensitive. pub fn find_font(&self, family: &str) -> Option<Font> { let key = family.to_ascii_lowercase(); let entries = self.families.get(&key)?;
// Prefer the Regular face. let entry = entries .iter() .find(|e| !e.bold && !e.italic) .or_else(|| entries.first())?;
load_font_at_offset(&entry.path, entry.offset).ok() }
/// Find a font by family name with bold/italic style preference. /// /// Falls back through: exact match -> any with same bold -> any in family. pub fn find_font_with_style(&self, family: &str, bold: bool, italic: bool) -> Option<Font> { let key = family.to_ascii_lowercase(); let entries = self.families.get(&key)?;
// Exact style match. let entry = entries .iter() .find(|e| e.bold == bold && e.italic == italic) // Fallback: match bold, ignore italic. .or_else(|| entries.iter().find(|e| e.bold == bold)) // Fallback: any face in the family. .or_else(|| entries.first())?;
load_font_at_offset(&entry.path, entry.offset).ok() }
/// List all discovered font family names (sorted alphabetically). pub fn list_families(&self) -> Vec<String> { // Return the original-case family name from the first entry of each family. let mut names: Vec<String> = self .families .values() .filter_map(|entries| entries.first().map(|e| e.family.clone())) .collect(); names.sort(); names }
/// Get all font entries for a given family name (case-insensitive). pub fn family_entries(&self, family: &str) -> Option<&[FontEntry]> { let key = family.to_ascii_lowercase(); self.families.get(&key).map(|v| v.as_slice()) }
/// Find any available font, preferring common system defaults. /// /// Tries: Helvetica, Arial, Geneva, then any available font. pub fn find_fallback(&self) -> Option<Font> { for preferred in &["Helvetica", "Arial", "Geneva", "Lucida Grande"] { if let Some(font) = self.find_font(preferred) { return Some(font); } }
// Last resort: pick the first font in the registry. for entries in self.families.values() { if let Some(entry) = entries.first() { if let Ok(font) = load_font_at_offset(&entry.path, entry.offset) { return Some(font); } } }
None }
/// Number of distinct font families in the registry. pub fn family_count(&self) -> usize { self.families.len() }}
/// Parse a TTC (TrueType Collection) file header to get individual font offsets.fn parse_ttc_offsets(path: &Path) -> Result<Vec<u32>, FontError> { let data = std::fs::read(path).map_err(|_| FontError::UnexpectedEof)?; if data.len() < 12 { return Err(FontError::UnexpectedEof); }
let r = Reader::new(&data); let tag = r.tag(0)?;
// TTC header: tag must be "ttcf". if &tag != b"ttcf" { return Err(FontError::InvalidMagic(u32::from_be_bytes(tag))); }
// version(4) + numFonts(4) let num_fonts = r.u32(8)? as usize; let mut offsets = Vec::with_capacity(num_fonts);
for i in 0..num_fonts { let offset = r.u32(12 + i * 4)?; offsets.push(offset); }
Ok(offsets)}
/// Probe a single font at the given byte offset within a file.////// Reads just enough to extract family name, subfamily, and style flags./// Returns `None` if the font can't be parsed.fn probe_single_font(path: &Path, offset: u32) -> Option<FontEntry> { let data = std::fs::read(path).ok()?; let font = parse_font_at_offset(data, offset).ok()?;
let name = font.name().ok()?; let family = name.family_name()?.to_owned(); let subfamily = name.subfamily_name().unwrap_or("Regular").to_owned();
// Determine bold/italic from head.macStyle and name heuristics. let (bold, italic) = detect_style(&font, &subfamily);
Some(FontEntry { path: path.to_owned(), offset, family, subfamily, bold, italic, })}
/// Detect bold/italic from head.macStyle flags, OS/2 weight class, and subfamily name.fn detect_style(font: &Font, subfamily: &str) -> (bool, bool) { let sub_lower = subfamily.to_ascii_lowercase();
// Start with head.macStyle bits. let (mut bold, mut italic) = if let Ok(head) = font.head() { (head.mac_style & 1 != 0, head.mac_style & 2 != 0) } else { (false, false) };
// Also consider OS/2 weight class. if let Ok(os2) = font.os2() { if os2.us_weight_class >= 700 { bold = true; } }
// Subfamily name heuristics as fallback. if sub_lower.contains("bold") { bold = true; } if sub_lower.contains("italic") || sub_lower.contains("oblique") { italic = true; }
(bold, italic)}
/// Parse a font from raw data at a given byte offset.////// For standalone fonts, offset is 0. For TTC fonts, offset points to/// the individual font's offset table within the collection.fn parse_font_at_offset(data: Vec<u8>, offset: u32) -> Result<Font, FontError> { if offset == 0 { return Font::parse(data); }
// For TTC: we need to parse the table directory starting at `offset`. let off = offset as usize; let r = Reader::new(&data);
let sf_version = r.u32(off)?; match sf_version { 0x00010000 | 0x4F54544F | 0x74727565 => {} _ => return Err(FontError::InvalidMagic(sf_version)), }
let num_tables = r.u16(off + 4)? as usize; let mut tables = Vec::with_capacity(num_tables);
for i in 0..num_tables { let base = off + 12 + i * 16; let tag = r.tag(base)?; let checksum = r.u32(base + 4)?; let table_offset = r.u32(base + 8)?; let length = r.u32(base + 12)?; tables.push(super::TableRecord { tag, checksum, offset: table_offset, length, }); }
Ok(Font { data, sf_version, tables, glyph_cache: std::cell::RefCell::new(super::cache::GlyphCache::new()), })}
/// Load a font from a file at the given byte offset.pub fn load_font_at_offset(path: &Path, offset: u32) -> Result<Font, FontError> { let data = std::fs::read(path).map_err(|_| FontError::UnexpectedEof)?; parse_font_at_offset(data, offset)}
#[cfg(test)]mod tests { use super::*;
fn has_system_fonts() -> bool { Path::new("/System/Library/Fonts").exists() }
#[test] fn registry_discovers_fonts() { if !has_system_fonts() { return; } let reg = FontRegistry::new(); assert!( reg.family_count() > 0, "should discover at least one font family" ); }
#[test] fn registry_list_families() { if !has_system_fonts() { return; } let reg = FontRegistry::new(); let families = reg.list_families(); assert!(!families.is_empty(), "should list font families");
// Families should be sorted. for i in 1..families.len() { assert!( families[i] >= families[i - 1], "families should be sorted: '{}' < '{}'", families[i], families[i - 1] ); } }
#[test] fn registry_find_font_case_insensitive() { if !has_system_fonts() { return; } let reg = FontRegistry::new(); let families = reg.list_families();
// Find the first family and try a case-insensitive lookup. if let Some(family) = families.first() { let upper = family.to_ascii_uppercase(); let font = reg.find_font(&upper); assert!( font.is_some(), "should find '{}' via uppercase '{}'", family, upper ); } }
#[test] fn registry_find_fallback() { if !has_system_fonts() { return; } let reg = FontRegistry::new(); let font = reg.find_fallback(); assert!(font.is_some(), "should find at least one fallback font"); }
#[test] fn registry_ttc_parsing() { // Check that TTC files in /System/Library/Fonts/ are parsed. if !has_system_fonts() { return; } let reg = FontRegistry::new();
// Courier.ttc exists on all macOS versions. let courier_path = Path::new("/System/Library/Fonts/Courier.ttc"); if courier_path.exists() { let font = reg.find_font("Courier"); assert!(font.is_some(), "should find Courier from .ttc file"); } }
#[test] fn registry_find_with_style() { if !has_system_fonts() { return; } let reg = FontRegistry::new();
// Try to find a font family that has multiple styles. let families = reg.list_families(); for family in &families { if let Some(entries) = reg.family_entries(family) { if entries.len() > 1 { // This family has multiple faces — test style selection. let _regular = reg.find_font_with_style(family, false, false); let _bold = reg.find_font_with_style(family, true, false); // Just verify no crash. return; } } } }
#[test] fn parse_ttc_offsets_courier() { let path = Path::new("/System/Library/Fonts/Courier.ttc"); if !path.exists() { return; } let offsets = parse_ttc_offsets(path).expect("should parse TTC offsets"); assert!( !offsets.is_empty(), "Courier.ttc should contain at least one font" ); // First offset should be a valid position (after the TTC header). assert!( offsets[0] >= 12, "first font offset should be past TTC header" ); }
#[test] fn font_entry_has_valid_metadata() { if !has_system_fonts() { return; } let reg = FontRegistry::new(); let families = reg.list_families(); if let Some(family) = families.first() { let entries = reg.family_entries(family).unwrap(); for entry in entries { assert!(!entry.family.is_empty(), "family name should not be empty"); assert!( !entry.subfamily.is_empty(), "subfamily name should not be empty" ); assert!(entry.path.exists(), "font file should exist"); } } }
#[test] fn load_font_at_offset_ttc() { let path = Path::new("/System/Library/Fonts/Courier.ttc"); if !path.exists() { return; } let offsets = parse_ttc_offsets(path).expect("should parse TTC"); if let Some(&offset) = offsets.first() { let font = load_font_at_offset(path, offset).expect("should load font from TTC"); // Verify we can parse basic tables. let name = font.name().expect("should parse name table"); assert!(name.family_name().is_some(), "should have a family name"); } }
#[test] fn registry_nonexistent_family_returns_none() { if !has_system_fonts() { return; } let reg = FontRegistry::new(); assert!( reg.find_font("NonexistentFontFamily12345").is_none(), "should return None for nonexistent family" ); }}