//! Installed fonts per text (plan F7): the faces of the system's font //! database, and drawing and measuring a string in one of them by name. //! //! Fig 3.2 knows fonts only as numbers; a text that uses an installed font //! keeps the nearest Fig font as its number and the font's name beside it //! (see `rustfig_model::Text::local_font`). figbase picks faces by Fig //! number only, so this module keeps its own `fontdb` database (system //! fonts and the folders given to [`add_font_dirs`]) and lays glyphs out //! the way figbase does (kerning from the `kern` table, 1.2 line spacing). //! With the built-in fonts switched on (tests, wasm) the database is empty, //! unless a test injects a family list with [`override_local_faces`]. use crate::{Generic, Metrics, PathOp, Shaped, FIG_PER_POINT, LINE_SPACING, MAX_CHARS}; use fontdb::{Database, Style, Weight, ID}; use std::path::PathBuf; use std::sync::{Mutex, MutexGuard}; use ttf_parser::{Face, GlyphId, OutlineBuilder}; /// One face of an installed family. #[derive(Debug, Clone, PartialEq, Eq)] pub struct LocalFace { pub family: String, /// `Regular`, `Bold`, `Italic`, `Semibold Italic`, ... pub label: String, /// What a text stores and the chooser shows: the family for the regular /// face, `Family Label` for the others. pub name: String, pub generic: Generic, pub bold: bool, pub italic: bool, } struct State { extra: Vec, built: Option, injected: Option>, } struct Built { builtin: bool, db: Database, faces: Vec<(LocalFace, ID)>, } static STATE: Mutex = Mutex::new(State { extra: Vec::new(), built: None, injected: None, }); fn lock() -> MutexGuard<'static, State> { STATE.lock().unwrap_or_else(|p| p.into_inner()) } /// Add folders to search for fonts (also for figbase's lookup). pub fn add_font_dirs(dirs: &[PathBuf]) { figbase::text::add_font_dirs(dirs); let mut g = lock(); for d in dirs { if !g.extra.contains(d) { g.extra.push(d.clone()); } } g.built = None; } /// Replace the family list with a fixed one (`None`: the installed fonts /// again). For tests and hosts without a font database; such faces have no /// glyphs, so texts set in them fall back to their Fig font. pub fn override_local_faces(faces: Option>) { lock().injected = faces; } fn weight_label(w: u16) -> &'static str { match w { 0..=150 => "Thin", 151..=250 => "ExtraLight", 251..=350 => "Light", 351..=450 => "", 451..=550 => "Medium", 551..=650 => "Semibold", 651..=750 => "Bold", 751..=850 => "ExtraBold", _ => "Black", } } fn is_serif_name(n: &str) -> bool { let n = n.to_lowercase(); if [ "sans", "gothic", "grotesk", "grotesque", "helvet", "arial", "verdana", "tahoma", "calibri", ] .iter() .any(|k| n.contains(k)) { return false; } [ "serif", "roman", "times", "georgia", "garamond", "palatino", "book", "minion", "caslon", "baskerville", "didot", "cambria", "century", "charter", "cmu", "latin modern", "termes", "antiqua", ] .iter() .any(|k| n.contains(k)) } /// macOS's private system fonts (".SF NS", ".Apple Color Emoji UI"): their /// names start with a dot, they are not meant for documents and can change /// with every system update, so the list leaves them out. fn is_hidden_family(family: &str) -> bool { family.starts_with('.') } /// Whether the face has the Latin letters. RustFig lays text out left to /// right without shaping (F6), so a font for Arabic, Hebrew or an Indic /// script would come out wrong; the list offers only fonts with Latin /// letters. fn has_latin(db: &Database, id: ID) -> bool { db.with_face_data(id, |data, index| { let face = Face::parse(data, index).ok()?; Some( ('a'..='z') .chain('A'..='Z') .all(|c| face.glyph_index(c).is_some_and(|g| g.0 != 0)), ) }) .flatten() .unwrap_or(false) } fn describe(db: &Database) -> Vec<(LocalFace, ID)> { let mut out: Vec<(LocalFace, ID)> = Vec::new(); for info in db.faces() { let Some((family, _)) = info .families .iter() .find(|(_, l)| *l == fontdb::Language::English_UnitedStates) .or_else(|| info.families.first()) else { continue; }; if is_hidden_family(family) || !has_latin(db, info.id) { continue; } let italic = info.style != Style::Normal; let mut parts: Vec<&str> = Vec::new(); let w = weight_label(info.weight.0); if !w.is_empty() { parts.push(w); } match info.stretch { fontdb::Stretch::Normal => {} fontdb::Stretch::UltraCondensed | fontdb::Stretch::ExtraCondensed => parts.push("ExtraCondensed"), fontdb::Stretch::Condensed | fontdb::Stretch::SemiCondensed => parts.push("Condensed"), _ => parts.push("Expanded"), } if italic { parts.push(if info.style == Style::Oblique { "Oblique" } else { "Italic" }); } let label = if parts.is_empty() { "Regular".to_string() } else { parts.join(" ") }; let name = if label == "Regular" { family.clone() } else { format!("{family} {label}") }; if out.iter().any(|(f, _)| f.name == name) { continue; } let generic = if info.monospaced { Generic::Mono } else if is_serif_name(family) { Generic::Serif } else { Generic::Sans }; out.push(( LocalFace { family: family.clone(), label, name, generic, bold: info.weight >= Weight::SEMIBOLD, italic, }, info.id, )); } out.sort_by_key(|(f, _)| { ( f.family.to_lowercase(), u8::from(f.bold) * 2 + u8::from(f.italic), f.name.clone(), ) }); out } fn built(g: &mut State) -> &Built { let builtin = crate::builtin_fonts(); if g.built.as_ref().is_some_and(|b| b.builtin != builtin) { g.built = None; } let extra = g.extra.clone(); g.built.get_or_insert_with(|| { // Only the native build adds the system's fonts. #[cfg_attr(target_arch = "wasm32", allow(unused_mut))] let mut db = Database::new(); if !builtin { #[cfg(not(target_arch = "wasm32"))] { db.load_system_fonts(); for d in &extra { db.load_fonts_dir(d); } } #[cfg(target_arch = "wasm32")] let _ = &extra; } let faces = describe(&db); Built { builtin, db, faces } }) } /// Every installed face, by family (empty with the built-in fonts). pub fn local_faces() -> Vec { let mut g = lock(); if let Some(v) = &g.injected { return v.clone(); } built(&mut g).faces.iter().map(|(f, _)| f.clone()).collect() } /// The face a stored name stands for (case-insensitive), if it is in the /// list. pub fn find_local_face(name: &str) -> Option { let name = name.trim().to_lowercase(); local_faces().into_iter().find(|f| f.name.to_lowercase() == name) } /// The file bytes and face index of the regular face of an installed family /// (case-insensitive), for hosts that need the font itself (the UI's /// fallback fonts). `None` with the built-in fonts or an unknown family. pub fn local_font_bytes(family: &str) -> Option<(Vec, u32)> { let mut g = lock(); let key = family.trim().to_lowercase(); let b = built(&mut g); let id = b.faces.iter().find(|(f, _)| f.name.to_lowercase() == key)?.1; b.db.with_face_data(id, |data, index| (data.to_vec(), index)) } /// Whether text in `name` can be drawn: the face is installed. pub fn local_font_available(name: &str) -> bool { let mut g = lock(); let key = name.trim().to_lowercase(); built(&mut g).faces.iter().any(|(f, _)| f.name.to_lowercase() == key) } struct Builder<'a> { ops: &'a mut Vec, scale: f64, dx: f64, dy: f64, } impl Builder<'_> { fn map(&self, x: f32, y: f32) -> (f64, f64) { (self.dx + f64::from(x) * self.scale, self.dy - f64::from(y) * self.scale) } } impl OutlineBuilder for Builder<'_> { fn move_to(&mut self, x: f32, y: f32) { let (x, y) = self.map(x, y); self.ops.push(PathOp::Move(x, y)); } fn line_to(&mut self, x: f32, y: f32) { let (x, y) = self.map(x, y); self.ops.push(PathOp::Line(x, y)); } fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { let (a, b) = self.map(x1, y1); let (x, y) = self.map(x, y); self.ops.push(PathOp::Quad(a, b, x, y)); } fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { let (a, b) = self.map(x1, y1); let (c, d) = self.map(x2, y2); let (x, y) = self.map(x, y); self.ops.push(PathOp::Cubic(a, b, c, d, x, y)); } fn close(&mut self) { self.ops.push(PathOp::Close); } } fn layout(face: &Face<'_>, size_pt: f64, text: &str, outlines: bool) -> Shaped { let size = if size_pt.is_finite() { size_pt.clamp(0.1, 5000.0) } else { 12.0 }; let em = size * FIG_PER_POINT; let scale = em / f64::from(face.units_per_em().max(1)); let ascent = f64::from(face.ascender()) * scale; let descent = -f64::from(face.descender()) * scale; let mut out = Shaped::default(); let mut count = 0usize; let mut y = 0.0; let mut nlines = 0usize; for line in text.split('\n') { nlines += 1; let mut ops = Vec::new(); let mut x = 0.0; let mut prev: Option = None; for ch in line.chars() { count += 1; if count > MAX_CHARS { break; } let gid = face.glyph_index(ch).unwrap_or(GlyphId(0)); if let (Some(p), Some(kern)) = (prev, face.tables().kern) { for st in kern.subtables { if st.horizontal { if let Some(k) = st.glyphs_kerning(p, gid) { x += f64::from(k) * scale; break; } } } } if outlines { let mut b = Builder { ops: &mut ops, scale, dx: x, dy: y, }; let _ = face.outline_glyph(gid, &mut b); } x += f64::from(face.glyph_hor_advance(gid).unwrap_or(0)) * scale; prev = Some(gid); } out.line_widths.push(x); out.lines.push(ops); y += em * LINE_SPACING; } let nlines = nlines.max(1); out.metrics = Metrics { width: out.line_widths.iter().copied().fold(0.0, f64::max), ascent, descent: descent + (nlines - 1) as f64 * em * LINE_SPACING, lines: nlines, }; out } fn run(name: &str, size_pt: f64, text: &str, outlines: bool) -> Option { let mut g = lock(); let key = name.trim().to_lowercase(); let b = built(&mut g); let id = b.faces.iter().find(|(f, _)| f.name.to_lowercase() == key)?.1; b.db.with_face_data(id, |data, index| { let face = Face::parse(data, index).ok()?; Some(layout(&face, size_pt, text, outlines)) })? } /// Glyph outlines of `text` in the installed face `name`; `None` when it /// is not installed (the caller falls back to the Fig font). pub fn shape_local(name: &str, size_pt: f64, text: &str) -> Option { run(name, size_pt, text, true) } /// The extent of `text` in the installed face `name`. pub fn measure_local(name: &str, size_pt: f64, text: &str) -> Option { run(name, size_pt, text, false).map(|s| s.metrics) } #[cfg(test)] pub(crate) mod tests { use super::*; /// The tests that touch the global font state (the list, built-in fonts /// only) run one at a time, also with other modules' tests. pub(crate) static SERIAL: Mutex<()> = Mutex::new(()); #[test] fn lays_out_and_scales() { let data = epaint_default_fonts::HACK_REGULAR; let face = Face::parse(data, 0).unwrap(); let a = layout(&face, 12.0, "Hi\nthere", true); let b = layout(&face, 24.0, "Hi\nthere", true); assert_eq!(a.metrics.lines, 2); assert!(a.metrics.width > 0.0); assert!((b.metrics.width / a.metrics.width - 2.0).abs() < 1e-6); assert!(a.lines.iter().all(|l| !l.is_empty())); // Hack is monospaced: five letters are 2.5 times two. assert!((a.line_widths[1] / a.line_widths[0] - 2.5).abs() < 1e-6); } #[test] fn builtin_mode_has_no_installed_faces() { let _s = SERIAL.lock().unwrap_or_else(|p| p.into_inner()); crate::set_builtin_fonts(true); override_local_faces(None); assert!(local_faces().is_empty()); assert!(shape_local("Anything", 12.0, "x").is_none()); assert!(!local_font_available("Anything")); } #[test] fn injected_list_is_listed() { let _s = SERIAL.lock().unwrap_or_else(|p| p.into_inner()); let f = LocalFace { family: "Fake".into(), label: "Regular".into(), name: "Fake".into(), generic: Generic::Serif, bold: false, italic: false, }; override_local_faces(Some(vec![f.clone()])); assert_eq!(find_local_face("fake"), Some(f)); override_local_faces(None); } #[test] fn dot_families_are_hidden() { assert!(is_hidden_family(".SF NS")); assert!(is_hidden_family(".Apple Color Emoji UI")); assert!(!is_hidden_family("Helvetica")); } /// A font found in a folder is listed, drawn and measured by its name /// (the font file is written by the test, from the built-in fonts). #[test] fn a_font_folder_gives_a_family_by_name() { let _s = SERIAL.lock().unwrap_or_else(|p| p.into_inner()); let dir = std::env::temp_dir().join(format!("rustfig-local-fonts-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join("hack-test.ttf"), epaint_default_fonts::HACK_REGULAR).unwrap(); crate::set_builtin_fonts(false); override_local_faces(None); add_font_dirs(std::slice::from_ref(&dir)); let face = local_faces() .into_iter() .find(|f| f.family == "Hack") .expect("Hack is listed"); assert_eq!(face.generic, Generic::Mono); assert_eq!(face.name, "Hack"); assert!(local_font_available("hack")); let m = measure_local("Hack", 12.0, "abcde").unwrap(); let s = shape_local("hack", 12.0, "abcde").unwrap(); assert!((m.width - s.metrics.width).abs() < 1e-9 && m.width > 0.0); assert!(s.lines[0].len() > 5); assert!(shape_local("Not Installed", 12.0, "x").is_none()); // Back to the built-in fonts only. crate::set_builtin_fonts(true); assert!(!local_font_available("Hack")); let _ = std::fs::remove_dir_all(&dir); } #[test] fn serif_guess() { assert!(is_serif_name("Minion Pro")); assert!(!is_serif_name("DejaVu Sans")); } } .