Files
wordcraft/crates/fonts/src/fontdb.rs
T
2026-11-05 03:36:06 +01:00

197 lines
9.1 KiB
Rust

//! Font database: bundled OFL fonts, user fonts, the installed system fonts (cataloged the first
//! time a lookup needs them, native only), outline cache.
use std::collections::HashMap;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use kurbo::BezPath;
use skrifa::instance::{Location, LocationRef, Size};
use skrifa::outline::{
DrawSettings, Engine, GlyphStyles, Hinting, HintingInstance, HintingOptions, OutlineGlyphFormat, OutlinePen, SmoothMode, Target,
};
use skrifa::raw::FileRef;
use skrifa::string::StringId;
use skrifa::{GlyphId, MetadataProvider};
pub(crate) mod user;
pub use user::{FONT_FILE_EXTS, FontFormat, Installed, MAX_FONT_FILE_BYTES, UserFont, UserFontError, decode_font_file, is_font_file_name, sniff};
mod catalog;
mod face;
mod fallback;
mod fallback_data;
mod lookup;
mod names;
use face::{BUNDLED, FontBytes};
pub use face::{FALLBACK_FAMILY, FaceRef, FontFace, bundled};
#[cfg(not(target_arch = "wasm32"))]
use fallback_data::{CoverageIndex, SysFallback};
use fallback_data::{FallbackCache, RunFallbackCache};
#[cfg(test)]
use fallback_data::{RUN_FALLBACK_CACHE_CHARS, coverage_ranges, ranges_hold};
#[cfg(not(target_arch = "wasm32"))]
use names::CatalogEntry;
pub(crate) use names::face_names;
pub use names::{LocalName, base_style, name_language_matches, system_font_dirs};
use names::{MAX_FILE_FACES, enumerate_faces, make_face};
#[cfg(test)]
use names::{MAX_NAME_CHARS, file_face_names, font_faces, mini_font, name};
#[cfg(test)]
pub(crate) use names::{last_resort_face, legacy_family};
static GLOBAL: std::sync::OnceLock<FontDb> = std::sync::OnceLock::new();
/// Process-wide font database.
pub struct FontDb {
faces: RwLock<Vec<Arc<FontFace>>>,
outlines: Mutex<HashMap<(u32, u32), Arc<BezPath>>>,
/// Screen-hinted outlines per (face, glyph, ppem bits); `None` where the face can't be hinted.
hinted: Mutex<HashMap<(u32, u32, u32), Option<Arc<BezPath>>>>,
/// Light hinting instances per (face, ppem bits); `None` where the face can't be hinted.
hinters: Mutex<HashMap<(u32, u32), Option<Arc<HintingInstance>>>>,
/// Fallback faces found per (character, excluded face), valid while the face count is the
/// one stored (faces are only ever added). Layout asks once per uncovered character, and the
/// search sorts every loaded face, so without this text in a script the requested font lacks
/// (Thai, #59) got slower with every font loaded.
fallbacks: RwLock<FallbackCache>,
run_fallbacks: RwLock<RunFallbackCache>,
#[cfg(not(target_arch = "wasm32"))]
catalog: RwLock<Vec<CatalogEntry>>,
/// Fonts the user added at run time, and the folder they are stored in (#582).
user: RwLock<user::UserFonts>,
/// Bumped whenever user fonts are added or removed ([`FontDb::generation`]).
generation: AtomicU64,
/// The folders the system font scan reads (the platform's font folders for [`FontDb::global`]).
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
font_dirs: Vec<std::path::PathBuf>,
/// Set once the font folders have been scanned. Lookups by family name wait for the first
/// scan, so what they find doesn't depend on what ran before them.
#[cfg(not(target_arch = "wasm32"))]
cataloged: std::sync::OnceLock<()>,
/// Held while an installed family's files load, so two lookups don't load it twice.
#[cfg(not(target_arch = "wasm32"))]
loading: Mutex<()>,
/// System fallback state: enabled, and characters no system font covers.
#[cfg(not(target_arch = "wasm32"))]
sys: Mutex<SysFallback>,
/// Held through a system fallback search, so searches run one at a time. Without it a search
/// racing another that was still loading a preferred family ("Padauk" for Myanmar) skipped
/// that family as already tried and loaded whatever the coverage index found first (only
/// Padauk Bold), so which face a character fell back to depended on thread timing.
#[cfg(not(target_arch = "wasm32"))]
searching: Mutex<()>,
/// The characters each installed font file maps, read the first time a fallback search
/// needs that file. Held through a search, so two searches never read a file twice.
#[cfg(not(target_arch = "wasm32"))]
coverage: Mutex<CoverageIndex>,
/// Counts the font folder scans, so the coverage index notices a rescan.
#[cfg(not(target_arch = "wasm32"))]
catalog_gen: AtomicU32,
}
impl FontDb {
/// A database holding the bundled fonts, then the craft-fonts Japanese faces (Mincho first,
/// so they are the fallback for Japanese text after the requested and bundled fonts), then the
/// other craft-fonts faces: Simplified Chinese, for the ideographs the Japanese fonts lack, and
/// Arabic, so Arabic and Persian text draws without system fonts (#1652) (all empty without
/// `CRAFT_FONTS_DIR`), whose system font scan reads `font_dirs`.
pub fn with_font_dirs(font_dirs: Vec<std::path::PathBuf>) -> Self {
let others = crate::CRAFT_FONTS.iter().filter(|f| !f.scripts.contains(&"Jpan"));
let craft = crate::japanese_document_fonts().into_iter().chain(others).map(|f| f.bytes);
Self::with_static_fonts(font_dirs, BUNDLED.iter().copied().chain(craft))
}
fn with_static_fonts(font_dirs: Vec<std::path::PathBuf>, fonts: impl Iterator<Item = &'static [u8]>) -> Self {
let mut faces = Vec::new();
for data in fonts {
for f in enumerate_faces(data, MAX_FILE_FACES) {
if let Some(mut face) = make_face(FontBytes::Static(data), f.index, f.family, f.style, f.legacy, f.coords) {
face.local_names = f.local;
faces.push(Arc::new(face));
}
}
}
Self {
faces: RwLock::new(faces),
outlines: Mutex::new(HashMap::new()),
hinted: Mutex::new(HashMap::new()),
hinters: Mutex::new(HashMap::new()),
fallbacks: RwLock::new(FallbackCache::default()),
run_fallbacks: RwLock::new(RunFallbackCache::default()),
#[cfg(not(target_arch = "wasm32"))]
catalog: RwLock::new(Vec::new()),
user: RwLock::new(user::UserFonts::default()),
generation: AtomicU64::new(0),
font_dirs,
#[cfg(not(target_arch = "wasm32"))]
cataloged: std::sync::OnceLock::new(),
#[cfg(not(target_arch = "wasm32"))]
loading: Mutex::new(()),
#[cfg(not(target_arch = "wasm32"))]
sys: Mutex::new(SysFallback { enabled: true, ..Default::default() }),
#[cfg(not(target_arch = "wasm32"))]
searching: Mutex::new(()),
#[cfg(not(target_arch = "wasm32"))]
coverage: Mutex::new(CoverageIndex::default()),
#[cfg(not(target_arch = "wasm32"))]
catalog_gen: AtomicU32::new(0),
}
}
/// Enable or disable the lazy system-font fallback for characters the loaded fonts lack
/// (native only; on by default).
pub fn set_system_fallback(&self, on: bool) {
#[cfg(not(target_arch = "wasm32"))]
{
self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner).enabled = on;
}
// Misses remembered while it was off may be found now.
self.fallbacks.write().unwrap_or_else(std::sync::PoisonError::into_inner).map.clear();
*self.run_fallbacks.write().unwrap_or_else(std::sync::PoisonError::into_inner) = RunFallbackCache::default();
#[cfg(target_arch = "wasm32")]
let _ = on;
}
/// Process-wide database: the bundled fonts, then the installed system fonts, cataloged the
/// first time a lookup by family name needs them (or ahead of time by
/// [`FontDb::scan_in_background`]).
pub fn global() -> &'static FontDb {
GLOBAL.get_or_init(|| FontDb::with_font_dirs(system_font_dirs()))
}
/// Initialize a test process with only the repository's pinned bundled fonts: no optional
/// craft-fonts, user fonts or system font directories. Call before any global font lookup.
/// Returns false if a normal global database was already initialized; never replaces it.
/// Repeated/concurrent calls are safe. Merely enabling `test-support` changes nothing.
#[cfg(feature = "test-support")]
pub fn init_test_fonts() -> bool {
static INIT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*INIT.get_or_init(|| {
let mut installed = false;
GLOBAL.get_or_init(|| {
installed = true;
let db = Self::with_static_fonts(Vec::new(), BUNDLED.iter().copied());
db.set_system_fallback(false);
db
});
installed
})
}
fn read_faces(&self) -> std::sync::RwLockReadGuard<'_, Vec<Arc<FontFace>>> {
self.faces.read().unwrap_or_else(std::sync::PoisonError::into_inner)
}
/// The system font catalog, scanned first if it hasn't been yet.
#[cfg(not(target_arch = "wasm32"))]
fn read_catalog(&self) -> std::sync::RwLockReadGuard<'_, Vec<CatalogEntry>> {
self.ensure_catalog();
self.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
#[cfg(test)]
pub(crate) mod tests;