//! Installed system fonts: found by family name by every lookup, whatever ran before it (#130). use std::path::{Path, PathBuf}; use std::sync::Arc; use super::*; const FAMILY: &str = "Sysfont Sans3"; /// A bundled Source Sans 3 file renamed [`FAMILY`] (as long as the original name). fn renamed(file: &str) -> Vec { renamed_to(file, FAMILY) } /// A bundled Source Sans 3 file renamed `family`, which is as long as the original name. fn renamed_to(file: &str, family: &str) -> Vec { assert_eq!(family.len(), "Source Sans 3".len()); let mut data = std::fs::read(Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets/fonts").join(file)).unwrap(); let utf16 = |s: &str| s.encode_utf16().flat_map(u16::to_be_bytes).collect::>(); for (from, to) in [(utf16("Source Sans 3"), utf16(family)), (b"Source Sans 3".to_vec(), family.as_bytes().to_vec())] { let mut i = 0; while let Some(at) = data[i..].windows(from.len()).position(|w| w == from) { data[i + at..i + at + to.len()].copy_from_slice(&to); i += at + to.len(); } } data } /// The fonts in `faces` as one collection (TTC) file. fn collection(faces: &[Vec]) -> Vec { let mut out = b"ttcf".to_vec(); out.extend_from_slice(&0x0001_0000_u32.to_be_bytes()); out.extend_from_slice(&(faces.len() as u32).to_be_bytes()); let mut base = 12 + 4 * faces.len(); for f in faces { out.extend_from_slice(&(base as u32).to_be_bytes()); base += f.len(); } for f in faces { // Table offsets count from the start of the collection. let start = out.len() as u32; let mut f = f.clone(); let tables = u16::from_be_bytes([f[4], f[5]]) as usize; for r in 0..tables { let at = 12 + r * 16 + 8; let off = u32::from_be_bytes(f[at..at + 4].try_into().unwrap()) + start; f[at..at + 4].copy_from_slice(&off.to_be_bytes()); } out.extend_from_slice(&f); } out } /// A font folder (with a subfolder, a damaged font and a file that isn't a font) holding /// [`FAMILY`] Regular and Bold. fn font_dir(name: &str) -> PathBuf { let dir = std::env::temp_dir().join(format!("vc-sysfonts-{}-{name}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(dir.join("Sub")).unwrap(); std::fs::write(dir.join("Sysfont-Regular.ttf"), renamed("SourceSans3-Regular.ttf")).unwrap(); std::fs::write(dir.join("Sub/Sysfont-Bold.TTF"), renamed("SourceSans3-Bold.ttf")).unwrap(); std::fs::write(dir.join("damaged.ttf"), b"ttcf\0\x01\0\0\xff\xff\xff\xff").unwrap(); std::fs::write(dir.join("readme.txt"), FAMILY).unwrap(); dir } fn has(list: &[String], family: &str) -> bool { list.iter().any(|f| f == family) } #[test] fn every_lookup_by_name_finds_installed_fonts_in_a_fresh_database() { let dir = font_dir("fresh"); // Each lookup is the first thing a new database is asked. A folder listed twice is read once. let db = || FontDb::with_font_dirs(vec![dir.clone(), dir.join("Sub/..")]); let f = db().face("sysfont sans3", "Bold").unwrap(); assert_eq!((f.family.as_str(), f.style.as_str()), (FAMILY, "Bold")); assert_eq!(f.path().map(std::fs::canonicalize).unwrap().unwrap(), std::fs::canonicalize(dir.join("Sub/Sysfont-Bold.TTF")).unwrap()); assert!(db().has_family(FAMILY)); assert!(has(&db().families(), FAMILY)); assert_eq!(db().styles(FAMILY), ["Regular", "Bold"]); assert_eq!(db().load_system_fonts(), 2); assert_eq!(db().find_family("SysfontSans3").as_deref(), Some(FAMILY), "a PostScript-style name"); // Missing fonts stay missing. let db = db(); assert!(!db.has_family("No Such Font")); assert_eq!(db.face("No Such Font", "Regular").unwrap().family, FALLBACK_FAMILY); } #[test] fn the_scan_reads_collections_and_loads_fonts_only_when_used() { let dir = font_dir("collection"); std::fs::remove_file(dir.join("Sysfont-Regular.ttf")).unwrap(); std::fs::remove_file(dir.join("Sub/Sysfont-Bold.TTF")).unwrap(); std::fs::write(dir.join("Sysfont.ttc"), collection(&[renamed("SourceSans3-Regular.ttf"), renamed("SourceSans3-Bold.ttf")])).unwrap(); let db = FontDb::with_font_dirs(vec![dir.clone()]); assert_eq!(db.styles(FAMILY), ["Regular", "Bold"]); // Cataloged, not loaded. assert!(!db.is_loaded(FAMILY)); let bold = db.face(FAMILY, "Bold").unwrap(); assert_eq!((bold.style.as_str(), bold.face_index()), ("Bold", 1)); assert_eq!(db.find(FAMILY, "Regular").unwrap().face_index(), 0, "the collection's faces load together"); } #[test] fn rescanning_finds_fonts_installed_since() { let dir = font_dir("rescan"); let later = dir.join("Later"); let db = FontDb::with_font_dirs(vec![later.clone()]); assert!(!db.has_family(FAMILY)); let (generation, families) = (db.generation(), db.family_list()); std::fs::create_dir_all(&later).unwrap(); std::fs::copy(dir.join("Sysfont-Regular.ttf"), later.join("Sysfont-Regular.ttf")).unwrap(); assert!(!db.has_family(FAMILY), "the folders are scanned once, not on every miss"); assert_eq!(db.load_system_fonts(), 1); assert!(db.has_family(FAMILY) && has(&db.family_list(), FAMILY) && !has(&families, FAMILY)); assert_ne!(db.generation(), generation); } #[test] fn a_background_scan_serves_the_first_lookup() { let dir = font_dir("background"); let db: &'static FontDb = Box::leak(Box::new(FontDb::with_font_dirs(vec![dir]))); db.scan_in_background(); // Waits for the scan when it is still running. assert!(has(&db.families(), FAMILY)); assert_eq!(db.load_system_fonts(), 2, "a rescan catalogs the two faces again"); } #[test] fn the_family_list_is_shared_until_fonts_change() { let db = FontDb::with_font_dirs(vec![]); let a = db.family_list(); assert!(Arc::ptr_eq(&a, &db.family_list())); assert!(db.add_font(renamed("SourceSans3-Regular.ttf")) > 0); let b = db.family_list(); assert!(!Arc::ptr_eq(&a, &b) && has(&b, FAMILY) && !has(&a, FAMILY)); } #[test] fn font_lists_leave_out_hidden_system_families_which_still_resolve() { // macOS names the faces it keeps for its own interface with a leading "." (".SF NS"). const HIDDEN: &str = ".Sysfont Sans"; const DOTTED: &str = "Sysfont.Sans3"; let dir = font_dir("hidden"); std::fs::write(dir.join("Hidden.ttf"), renamed_to("SourceSans3-Regular.ttf", HIDDEN)).unwrap(); std::fs::write(dir.join("Dotted.ttf"), renamed_to("SourceSans3-Regular.ttf", DOTTED)).unwrap(); let db = FontDb::with_font_dirs(vec![dir]); let listed = db.menu_family_list(); assert!(has(&listed, FAMILY) && has(&listed, FALLBACK_FAMILY)); assert!(!has(&listed, HIDDEN), "a leading dot hides a family: {listed:?}"); assert!(has(&listed, DOTTED), "a dot elsewhere doesn't"); assert_eq!(listed.len() + 1, db.family_list().len(), "only the hidden family is left out"); assert!(Arc::ptr_eq(&listed, &db.menu_family_list()), "shared until the fonts change"); // Documents and fallbacks that name it still find it. assert!(has(&db.families(), HIDDEN) && db.has_family(HIDDEN)); assert_eq!(db.find_family(HIDDEN).as_deref(), Some(HIDDEN)); assert_eq!(db.face(HIDDEN, "Regular").unwrap().family, HIDDEN); // Loading it changed the fonts: the list is built again, still without it. let again = db.menu_family_list(); assert!(!Arc::ptr_eq(&listed, &again) && *listed == *again); } #[test] fn installed_styles_of_a_loaded_family_load_when_asked_for() { let dir = font_dir("styles"); let db = FontDb::with_font_dirs(vec![dir]); // The family is loaded with only its Regular face (as when a bundled family is also installed). assert_eq!(db.add_font(renamed("SourceSans3-Regular.ttf")), 1); assert_eq!(db.face(FAMILY, "Bold").unwrap().style, "Bold"); // A style nobody has still gets the closest one. assert_eq!(db.face(FAMILY, "Black").unwrap().style, "Bold"); } /// Documents name fonts by PostScript name, and families can hold hyphens: the installed face of /// that exact name gives the family and style, however the name splits. #[test] fn installed_faces_are_found_by_postscript_name() { let dir = font_dir("postscript"); let db = FontDb::with_font_dirs(vec![dir]); // The renamed Source Sans 3 files keep their PostScript names. assert_eq!(db.by_postscript_name("SourceSans3-Bold"), Some((FAMILY.to_string(), "Bold".to_string()))); assert_eq!(db.by_postscript_name("sourcesans3-regular"), Some((FAMILY.to_string(), "Regular".to_string())), "any case"); assert_eq!(db.by_postscript_name("Rounded-X-Mplus-1c-black"), None); }