use alloc::collections::btree_map::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex, RwLock};
use std::time::{Duration, Instant};
use crate::{
expand_font_families, FcFontCache, FcFontPath, FcParseFontBytes, FcPattern, FcWeight,
FontFallbackChain, FontId, FontMatch, NamedFont, OperatingSystem, PatternMatch,
UnicodeRange,
};
use crate::scoring::{
family_exists_in_patterns, find_family_paths, find_incomplete_paths,
FcBuildJob, Priority,
};
use crate::utils::normalize_family_name;
pub struct FcFontRegistry {
pub cache: FcFontCache,
pub known_paths: RwLock<BTreeMap<String, Vec<PathBuf>>>,
pub build_queue: Mutex<Vec<FcBuildJob>>,
pub queue_condvar: Condvar,
pub processed_paths: Mutex<HashSet<PathBuf>>,
pub completed_paths: Mutex<HashSet<PathBuf>>,
pub progress: Condvar,
pub scan_complete: AtomicBool,
pub build_complete: AtomicBool,
pub shutdown: AtomicBool,
pub cache_loaded: AtomicBool,
pub lazy_scout: AtomicBool,
pub os: OperatingSystem,
}
impl std::fmt::Debug for FcFontRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FcFontRegistry")
.field("scan_complete", &self.scan_complete.load(Ordering::Relaxed))
.field("build_complete", &self.build_complete.load(Ordering::Relaxed))
.field("cache_loaded", &self.cache_loaded.load(Ordering::Relaxed))
.finish()
}
}
impl FcFontRegistry {
pub fn new() -> Arc<Self> {
Arc::new(Self {
cache: FcFontCache::default(),
known_paths: RwLock::new(BTreeMap::new()),
build_queue: Mutex::new(Vec::new()),
queue_condvar: Condvar::new(),
processed_paths: Mutex::new(HashSet::new()),
completed_paths: Mutex::new(HashSet::new()),
progress: Condvar::new(),
scan_complete: AtomicBool::new(false),
build_complete: AtomicBool::new(false),
shutdown: AtomicBool::new(false),
cache_loaded: AtomicBool::new(false),
lazy_scout: AtomicBool::new(false),
os: OperatingSystem::current(),
})
}
pub fn set_scout_lazy(&self, lazy: bool) {
self.lazy_scout.store(lazy, Ordering::Release);
}
pub fn register_memory_fonts(&self, fonts: Vec<NamedFont>) {
for named_font in fonts {
let Some(parsed) = FcParseFontBytes(&named_font.bytes, &named_font.name) else {
continue;
};
self.cache.with_memory_fonts(parsed);
}
}
pub fn spawn_scout_and_builders(self: &Arc<Self>) {
let num_threads = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(2)
.saturating_sub(1)
.max(1);
let registry = Arc::clone(self);
std::thread::Builder::new()
.name("rfc-font-scout".to_string())
.spawn(move || {
registry.scout_thread();
})
.expect("failed to spawn font scout thread");
for i in 0..num_threads {
let registry = Arc::clone(self);
std::thread::Builder::new()
.name(format!("rfc-font-builder-{}", i))
.spawn(move || {
registry.builder_thread();
})
.expect("failed to spawn font builder thread");
}
}
pub fn request_fonts(
&self,
family_stacks: &[Vec<String>],
) -> Vec<FontFallbackChain> {
let deadline = Instant::now() + Duration::from_secs(5);
let mut needed_families: Vec<String> = Vec::new();
let mut expanded_stacks: Vec<Vec<String>> = Vec::new();
for stack in family_stacks {
let expanded = expand_font_families(stack, self.os, &[]);
for family in &expanded {
let normalized = normalize_family_name(family);
if !needed_families.contains(&normalized) {
needed_families.push(normalized);
}
}
expanded_stacks.push(expanded);
}
if self.cache_loaded.load(Ordering::Acquire) {
return self.resolve_chains(&expanded_stacks);
}
if !self.scan_complete.load(Ordering::Acquire) {
let Ok(mut completed) = self.completed_paths.lock() else {
return self.resolve_chains(&expanded_stacks);
};
while !self.scan_complete.load(Ordering::Acquire) {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
eprintln!(
"[rfc-font-registry] WARNING: Timed out waiting for font scout (5s). \
Proceeding with available fonts."
);
return self.resolve_chains(&expanded_stacks);
}
completed = match self.progress.wait_timeout(completed, remaining) {
Ok((c, _)) => c,
Err(_) => return self.resolve_chains(&expanded_stacks),
};
}
}
let missing: Vec<String> = {
let state = self.cache.state_read();
needed_families
.iter()
.filter(|fam| !family_exists_in_patterns(fam, state.patterns.keys()))
.cloned()
.collect()
};
let incomplete_paths = self.known_paths.read().ok()
.zip(self.completed_paths.lock().ok())
.map(|(known, completed)| find_incomplete_paths(&needed_families, &known, &completed))
.unwrap_or_default();
if missing.is_empty() && incomplete_paths.is_empty() {
return self.resolve_chains(&expanded_stacks);
}
let wait_paths: HashSet<PathBuf> = if let (Ok(known_paths), Ok(mut queue)) =
(self.known_paths.read(), self.build_queue.lock())
{
let missing_paths: Vec<_> = missing
.iter()
.flat_map(|fam| {
find_family_paths(fam, &known_paths)
.into_iter()
.map(move |p| (p, fam.clone()))
})
.collect();
for (path, family) in missing_paths.iter().chain(incomplete_paths.iter()) {
queue.push(FcBuildJob {
priority: Priority::Critical,
path: path.clone(),
font_index: None,
guessed_family: family.clone(),
});
}
queue.sort();
missing_paths
.iter()
.chain(incomplete_paths.iter())
.map(|(p, _)| p.clone())
.collect()
} else {
incomplete_paths.iter().map(|(p, _)| p.clone()).collect()
};
self.queue_condvar.notify_all();
if !wait_paths.is_empty() {
let Ok(mut completed) = self.completed_paths.lock() else {
return self.resolve_chains(&expanded_stacks);
};
loop {
if wait_paths.iter().all(|p| completed.contains(p)) {
break;
}
if self.build_complete.load(Ordering::Acquire) {
break;
}
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
eprintln!(
"[rfc-font-registry] WARNING: Timed out waiting for font files (5s). \
Proceeding with available fonts."
);
break;
}
completed = match self.progress.wait_timeout(completed, remaining) {
Ok((c, _)) => c,
Err(_) => break,
};
}
}
self.resolve_chains(&expanded_stacks)
}
pub fn get_metadata_by_id(&self, id: &FontId) -> Option<FcPattern> {
self.cache.get_metadata_by_id(id)
}
pub fn get_font_bytes(&self, id: &FontId) -> Option<std::sync::Arc<crate::FontBytes>> {
self.cache.get_font_bytes(id)
}
pub fn get_disk_font_path(&self, id: &FontId) -> Option<FcFontPath> {
self.cache.state_read().disk_fonts.get(id).cloned()
}
pub fn is_memory_font(&self, id: &FontId) -> bool {
self.cache.is_memory_font(id)
}
pub fn list(&self) -> Vec<(FcPattern, FontId)> {
self.cache.list()
}
pub fn query(&self, pattern: &FcPattern) -> Option<FontMatch> {
let mut trace = Vec::new();
self.cache.query(pattern, &mut trace)
}
pub fn resolve_font_chain(
&self,
font_families: &[String],
weight: FcWeight,
italic: PatternMatch,
oblique: PatternMatch,
) -> FontFallbackChain {
let mut trace = Vec::new();
self.cache.resolve_font_chain_with_os(
font_families, weight, italic, oblique, &mut trace, self.os,
)
}
#[cfg(feature = "std")]
pub fn request_and_resolve_with_scripts(
&self,
font_families: &[String],
weight: FcWeight,
italic: PatternMatch,
oblique: PatternMatch,
scripts_hint: Option<&[UnicodeRange]>,
) -> FontFallbackChain {
let _ = self.request_fonts(std::slice::from_ref(&font_families.to_vec()));
let mut trace = Vec::new();
self.cache.resolve_font_chain_with_scripts(
font_families, weight, italic, oblique, scripts_hint, &mut trace,
)
}
pub fn shared_cache(&self) -> FcFontCache {
self.cache.clone()
}
pub fn wait_for_scout(&self) {
use std::time::{Duration, Instant};
if self.cache_loaded.load(Ordering::Acquire) {
return;
}
if self.build_complete.load(Ordering::Acquire) {
return;
}
let deadline = Instant::now() + Duration::from_secs(5);
let Ok(mut completed) = self.completed_paths.lock() else {
return;
};
while !self.build_complete.load(Ordering::Acquire) {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
eprintln!(
"[rfc-font-registry] WARNING: wait_for_scout timed out (5s)."
);
return;
}
completed = match self.progress.wait_timeout(completed, remaining) {
Ok((c, _)) => c,
Err(_) => return,
};
}
}
pub fn shutdown(&self) {
self.shutdown.store(true, Ordering::Release);
self.queue_condvar.notify_all();
self.progress.notify_all();
}
pub fn is_scan_complete(&self) -> bool {
self.scan_complete.load(Ordering::Acquire)
}
pub fn is_build_complete(&self) -> bool {
self.build_complete.load(Ordering::Acquire)
}
pub fn is_cache_loaded(&self) -> bool {
self.cache_loaded.load(Ordering::Acquire)
}
#[cfg(all(feature = "std", feature = "parsing"))]
pub fn request_fonts_fast(
&self,
requests: &[(Vec<String>, alloc::collections::BTreeSet<char>)],
weight: FcWeight,
italic: PatternMatch,
) -> Vec<FontFallbackChain> {
use crate::{
expand_font_families, CssFallbackGroup, FcCountFontFaces, FcFontPath,
FcParseFontFaceFast, FontMatch,
};
use std::sync::atomic::Ordering;
let wait_start = Instant::now();
let mut waited = false;
let current_known_paths;
loop {
let Ok(paths) = self.known_paths.read() else {
return requests.iter().map(|(stack, _)| FontFallbackChain {
css_fallbacks: Vec::new(),
unicode_fallbacks: Vec::new(),
original_stack: stack.clone(),
}).collect();
};
let any_matches = requests.iter().any(|(stack, _)| {
let expanded = expand_font_families(stack, self.os, &[]);
expanded.iter().any(|fam| {
let fam_norm = crate::utils::normalize_family_name(fam);
!crate::scoring::find_family_paths(&fam_norm, &*paths).is_empty()
})
});
if any_matches
|| self.scan_complete.load(Ordering::Acquire)
|| wait_start.elapsed() >= Duration::from_millis(500)
{
drop(paths);
if let Ok(p) = self.known_paths.read() {
current_known_paths = p;
break;
} else {
return requests.iter().map(|(stack, _)| FontFallbackChain {
css_fallbacks: Vec::new(),
unicode_fallbacks: Vec::new(),
original_stack: stack.clone(),
}).collect();
}
}
drop(paths);
waited = true;
let Ok(completed) = self.completed_paths.lock() else {
if let Ok(p) = self.known_paths.read() {
current_known_paths = p;
break;
} else {
return Vec::new();
}
};
let remaining = Duration::from_millis(500)
.saturating_sub(wait_start.elapsed());
if remaining.is_zero() {
drop(completed);
if let Ok(p) = self.known_paths.read() {
current_known_paths = p;
break;
} else {
return Vec::new();
}
}
let _ = self.progress.wait_timeout(completed, remaining);
}
let known_paths = current_known_paths;
let scan_wait_us = wait_start.elapsed().as_micros();
static RFC_DBG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
if *RFC_DBG.get_or_init(|| std::env::var_os("RFC_REGISTRY_DEBUG").is_some()) {
eprintln!(
"[RFC] request_fonts_fast: scan_wait = {} µs (waited={})",
scan_wait_us, waited,
);
}
let want_bold = weight >= FcWeight::Bold;
let want_italic = italic == PatternMatch::True;
let mut chains = Vec::with_capacity(requests.len());
for (stack, codepoints) in requests {
let expanded = expand_font_families(stack, self.os, &[]);
let mut css_fallbacks: Vec<CssFallbackGroup> = Vec::new();
let mut uncovered: alloc::collections::BTreeSet<char> = codepoints.clone();
'families: for family in &expanded {
if uncovered.is_empty() {
break;
}
let family_norm = crate::utils::normalize_family_name(family);
let paths = crate::scoring::find_family_paths(&family_norm, &known_paths);
let mut group = CssFallbackGroup {
css_name: family.clone(),
fonts: Vec::new(),
};
for path in paths {
let path_str = path.to_string_lossy().to_string();
if let Some(cached_ids) = self.cache.lookup_paths_cached(&path_str) {
let mut picked: Option<(crate::FontId, crate::FcPattern, alloc::collections::BTreeSet<char>)> = None;
for id in cached_ids {
let Some(pattern) = self.cache.get_metadata_by_id(&id) else { continue };
let covers: alloc::collections::BTreeSet<char> = uncovered
.iter()
.copied()
.filter(|ch| {
let cp = *ch as u32;
pattern.unicode_ranges.iter().any(|r| cp >= r.start && cp <= r.end)
})
.collect();
if covers.is_empty() {
continue;
}
let is_bold = pattern.weight >= FcWeight::Bold;
let is_italic = pattern.italic == PatternMatch::True;
let style_dist = (is_bold != want_bold) as u8
+ (is_italic != want_italic) as u8;
let replace = match &picked {
None => true,
Some((_, pat, _)) => {
let pb = pat.weight >= FcWeight::Bold;
let pi = pat.italic == PatternMatch::True;
let pd = (pb != want_bold) as u8 + (pi != want_italic) as u8;
style_dist < pd
}
};
if replace {
picked = Some((id, pattern, covers));
}
}
if let Some((id, pattern, covers)) = picked {
for ch in &covers {
uncovered.remove(ch);
}
group.fonts.push(FontMatch {
id,
unicode_ranges: pattern.unicode_ranges,
fallbacks: Vec::new(),
});
if !group.fonts.is_empty() {
css_fallbacks.push(group);
}
continue 'families;
}
}
let Some(bytes) = read_or_mmap_font(&path) else { continue };
let num_faces = FcCountFontFaces(bytes.as_slice());
let bytes_hash = crate::utils::content_dedup_hash_u64(bytes.as_slice());
let face_order: Vec<usize> = if num_faces == 1 {
vec![0]
} else {
collect_face_style_order(
bytes.as_slice(),
num_faces,
want_bold,
want_italic,
)
};
for face_index in face_order {
let Some(cov) = FcParseFontFaceFast(
bytes.as_slice(), face_index, &uncovered,
) else { continue };
if cov.covered.is_empty() {
continue;
}
let mut pat = cov.pattern.clone();
let family_guessed = crate::config::guess_family_from_filename(&path);
pat.name = Some(family.clone());
pat.family = Some(family_guessed);
let id = self.cache.insert_fast_pattern(
pat.clone(),
FcFontPath {
path: path_str.clone(),
font_index: face_index,
bytes_hash,
},
);
for ch in &cov.covered {
uncovered.remove(ch);
}
group.fonts.push(FontMatch {
id,
unicode_ranges: pat.unicode_ranges,
fallbacks: Vec::new(),
});
if !group.fonts.is_empty() {
css_fallbacks.push(group);
}
continue 'families;
}
}
}
chains.push(FontFallbackChain {
css_fallbacks,
unicode_fallbacks: Vec::new(),
original_stack: stack.clone(),
});
}
chains
}
pub fn insert_font(&self, pattern: FcPattern, path: FcFontPath) {
self.cache.insert_builder_font(pattern, path);
}
fn resolve_chains(&self, expanded_stacks: &[Vec<String>]) -> Vec<FontFallbackChain> {
expanded_stacks
.iter()
.map(|stack| {
self.resolve_font_chain(
stack,
FcWeight::Normal,
PatternMatch::DontCare,
PatternMatch::DontCare,
)
})
.collect()
}
}
impl Drop for FcFontRegistry {
fn drop(&mut self) {
self.shutdown();
}
}
#[cfg(all(feature = "std", feature = "parsing"))]
fn read_or_mmap_font(path: &std::path::Path) -> Option<std::sync::Arc<crate::FontBytes>> {
#[cfg(all(not(target_family = "wasm"), feature = "std"))]
{
crate::open_font_bytes_mmap(&path.to_string_lossy())
}
#[cfg(target_family = "wasm")]
{
let bytes = std::fs::read(path).ok()?;
Some(std::sync::Arc::new(crate::FontBytes::Owned(
std::sync::Arc::from(bytes.as_slice()),
)))
}
}
#[cfg(all(feature = "std", feature = "parsing"))]
fn collect_face_style_order(
bytes: &[u8],
num_faces: usize,
want_bold: bool,
want_italic: bool,
) -> Vec<usize> {
use allsorts::{
binary::read::ReadScope, font_data::FontData,
tables::{FontTableProvider, HeadTable}, tag,
};
let scope = ReadScope::new(bytes);
let Ok(font_file) = scope.read::<FontData<'_>>() else {
return (0..num_faces).collect();
};
let mut styles: Vec<(usize, bool, bool)> = Vec::with_capacity(num_faces);
for fi in 0..num_faces {
let Ok(provider) = font_file.table_provider(fi) else { continue };
let Ok(Some(head_data)) = provider.table_data(tag::HEAD) else { continue };
let Ok(head) = ReadScope::new(&head_data).read::<HeadTable>() else { continue };
styles.push((fi, head.is_bold(), head.is_italic()));
}
if styles.is_empty() {
return (0..num_faces).collect();
}
styles.sort_by_key(|(_, is_bold, is_italic)| {
let bold_mismatch = (*is_bold != want_bold) as u8;
let italic_mismatch = (*is_italic != want_italic) as u8;
(bold_mismatch, italic_mismatch)
});
styles.into_iter().map(|(fi, _, _)| fi).collect()
}