use super::error::PdfError;
use ahash::AHashMap;
use once_cell::sync::Lazy;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::RwLock;
#[cfg(feature = "pdf")]
use pdfium_render::prelude::FontDescriptor;
static FONT_CACHE: Lazy<RwLock<FontCacheState>> = Lazy::new(|| {
RwLock::new(FontCacheState {
fonts: AHashMap::new(),
initialized: false,
})
});
struct FontCacheState {
fonts: AHashMap<String, Arc<[u8]>>,
initialized: bool,
}
#[cfg(target_os = "macos")]
fn system_font_directories() -> Vec<PathBuf> {
vec![PathBuf::from("/Library/Fonts"), PathBuf::from("/System/Library/Fonts")]
}
#[cfg(target_os = "linux")]
fn system_font_directories() -> Vec<PathBuf> {
vec![
PathBuf::from("/usr/share/fonts"),
PathBuf::from("/usr/local/share/fonts"),
]
}
#[cfg(target_os = "windows")]
fn system_font_directories() -> Vec<PathBuf> {
vec![PathBuf::from("C:\\Windows\\Fonts")]
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
fn system_font_directories() -> Vec<PathBuf> {
vec![]
}
fn load_font_file(path: &Path) -> Result<Arc<[u8]>, PdfError> {
std::fs::read(path)
.map(|bytes| Arc::from(bytes.into_boxed_slice()))
.map_err(|e| PdfError::FontLoadingFailed(format!("Failed to read font file '{}': {}", path.display(), e)))
}
fn discover_system_fonts() -> Result<AHashMap<String, Arc<[u8]>>, PdfError> {
let mut fonts = AHashMap::new();
const MAX_FONT_SIZE: u64 = 50 * 1024 * 1024;
for dir in system_font_directories() {
if !dir.exists() {
continue;
}
match std::fs::read_dir(&dir) {
Ok(entries) => {
for entry in entries.flatten() {
let path = entry.path();
if let Some(ext) = path.extension() {
let ext_str = ext.to_string_lossy().to_lowercase();
if ext_str != "ttf" && ext_str != "otf" {
continue;
}
if let Ok(metadata) = std::fs::metadata(&path) {
if metadata.len() > MAX_FONT_SIZE {
tracing::warn!(
"Font file too large (skipped): {} ({}MB)",
path.display(),
metadata.len() / (1024 * 1024)
);
continue;
}
} else {
continue;
}
match load_font_file(&path) {
Ok(font_data) => {
if let Some(filename) = path.file_name() {
let key = filename.to_string_lossy().to_string();
fonts.insert(key, font_data);
}
}
Err(_e) => {
tracing::debug!("Failed to load font file: {}", path.display());
}
}
}
}
}
Err(_e) => {
tracing::debug!("Failed to read font directory: {}", dir.display());
}
}
}
Ok(fonts)
}
pub fn initialize_font_cache() -> Result<(), PdfError> {
{
let cache = FONT_CACHE
.read()
.map_err(|e| PdfError::FontLoadingFailed(format!("Font cache lock poisoned: {}", e)))?;
if cache.initialized {
return Ok(());
}
}
let mut cache = FONT_CACHE
.write()
.map_err(|e| PdfError::FontLoadingFailed(format!("Font cache lock poisoned: {}", e)))?;
if cache.initialized {
return Ok(());
}
tracing::debug!("Initializing font cache...");
let fonts = discover_system_fonts()?;
let font_count = fonts.len();
cache.fonts = fonts;
cache.initialized = true;
tracing::debug!("Font cache initialized with {} fonts", font_count);
Ok(())
}
pub fn get_font_descriptors() -> Result<Vec<FontDescriptor>, PdfError> {
initialize_font_cache()?;
let cache = FONT_CACHE
.read()
.map_err(|e| PdfError::FontLoadingFailed(format!("Font cache lock poisoned: {}", e)))?;
let descriptors = cache
.fonts
.iter()
.map(|(filename, data)| {
let is_italic = filename.to_lowercase().contains("italic");
let is_bold = filename.to_lowercase().contains("bold");
let weight = if is_bold { 700 } else { 400 };
let family = filename.split('.').next().unwrap_or("Unknown").to_string();
FontDescriptor {
family,
weight,
is_italic,
charset: 0,
data: data.clone(),
}
})
.collect();
Ok(descriptors)
}
pub fn cached_font_count() -> usize {
FONT_CACHE.read().map(|cache| cache.fonts.len()).unwrap_or(0)
}
#[cfg(test)]
pub fn clear_font_cache() {
let mut cache = FONT_CACHE.write().expect("Failed to acquire write lock");
cache.fonts.clear();
cache.initialized = false;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_initialize_font_cache() {
clear_font_cache();
let result = initialize_font_cache();
assert!(result.is_ok(), "Font cache initialization should succeed");
}
#[test]
fn test_initialize_font_cache_idempotent() {
clear_font_cache();
let result1 = initialize_font_cache();
assert!(result1.is_ok());
let result2 = initialize_font_cache();
assert!(result2.is_ok());
}
#[test]
fn test_get_font_descriptors() {
clear_font_cache();
let result = get_font_descriptors();
assert!(result.is_ok());
}
#[test]
fn test_cached_font_count() {
clear_font_cache();
assert_eq!(cached_font_count(), 0, "Cache should be empty before initialization");
let result = initialize_font_cache();
assert!(result.is_ok(), "Font cache initialization should succeed");
let _count = cached_font_count();
}
#[test]
fn test_system_font_directories() {
let dirs = system_font_directories();
assert!(!dirs.is_empty(), "Should have at least one font directory");
for dir in dirs {
assert!(
dir.is_absolute(),
"Font directory should be absolute: {}",
dir.display()
);
}
}
#[test]
fn test_load_font_file_nonexistent() {
let result = load_font_file(Path::new("/nonexistent/path/font.ttf"));
assert!(result.is_err(), "Loading nonexistent file should fail with error");
}
#[test]
fn test_font_descriptors_attributes() {
clear_font_cache();
let data: Arc<[u8]> = Arc::from(vec![0u8; 100].into_boxed_slice());
let descriptor = FontDescriptor {
family: "TestFont".to_string(),
weight: 700,
is_italic: false,
charset: 0,
data,
};
assert_eq!(descriptor.family, "TestFont");
assert_eq!(descriptor.weight, 700);
assert!(!descriptor.is_italic);
assert_eq!(descriptor.charset, 0);
}
}