use super::FontInfo;
use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
const DEFAULT_MAX_ENTRIES: usize = 1024;
struct LruFontCache {
entries: HashMap<u64, (Arc<FontInfo>, u64)>,
generation: u64,
max_entries: usize,
}
impl LruFontCache {
fn new(max_entries: usize) -> Self {
Self {
entries: HashMap::with_capacity(max_entries / 2),
generation: 0,
max_entries,
}
}
fn get(&mut self, key: u64) -> Option<Arc<FontInfo>> {
if let Some((font, gen)) = self.entries.get_mut(&key) {
self.generation += 1;
*gen = self.generation;
Some(Arc::clone(font))
} else {
None
}
}
fn insert(&mut self, key: u64, font: Arc<FontInfo>) {
if self.entries.contains_key(&key) {
self.generation += 1;
self.entries.insert(key, (font, self.generation));
return;
}
if self.entries.len() >= self.max_entries {
self.evict_lru();
}
self.generation += 1;
self.entries.insert(key, (font, self.generation));
}
fn evict_lru(&mut self) {
if self.entries.is_empty() {
return;
}
let lru_key = self
.entries
.iter()
.min_by_key(|(_, (_, gen))| *gen)
.map(|(k, _)| *k);
if let Some(key) = lru_key {
self.entries.remove(&key);
}
}
fn len(&self) -> usize {
self.entries.len()
}
fn capacity(&self) -> usize {
self.max_entries
}
fn clear(&mut self) {
self.entries.clear();
self.generation = 0;
}
fn set_capacity(&mut self, new_max: usize) {
self.max_entries = new_max;
while self.entries.len() > self.max_entries {
self.evict_lru();
}
}
}
static GLOBAL_FONT_CACHE: OnceLock<Mutex<LruFontCache>> = OnceLock::new();
fn cache() -> &'static Mutex<LruFontCache> {
GLOBAL_FONT_CACHE.get_or_init(|| Mutex::new(LruFontCache::new(DEFAULT_MAX_ENTRIES)))
}
pub fn global_font_cache_get(identity_hash: u64) -> Option<Arc<FontInfo>> {
cache().lock().ok()?.get(identity_hash)
}
pub fn global_font_cache_insert(identity_hash: u64, font: Arc<FontInfo>) {
if let Ok(mut guard) = cache().lock() {
guard.insert(identity_hash, font);
}
}
pub fn clear_global_font_cache() {
if let Ok(mut guard) = cache().lock() {
guard.clear();
}
}
pub fn global_font_cache_stats() -> (usize, usize) {
cache()
.lock()
.map(|guard| (guard.len(), guard.capacity()))
.unwrap_or((0, 0))
}
pub fn set_global_font_cache_capacity(max_entries: usize) {
if let Ok(mut guard) = cache().lock() {
guard.set_capacity(max_entries);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fonts::font_dict::Encoding;
use std::collections::HashMap;
fn make_test_font(name: &str) -> FontInfo {
FontInfo {
base_font: name.to_string(),
subtype: "Type1".to_string(),
encoding: Encoding::Standard("WinAnsiEncoding".to_string()),
to_unicode: None,
font_weight: None,
flags: None,
stem_v: None,
embedded_font_data: None,
truetype_cmap: std::sync::OnceLock::new(),
is_truetype_font: false,
cid_to_gid_map: None,
cid_system_info: None,
cid_font_type: None,
widths: None,
first_char: None,
last_char: None,
default_width: 600.0,
cid_widths: None,
cid_default_width: 1000.0,
cff_gid_map: None,
multi_char_map: HashMap::new(),
byte_to_char_table: std::sync::OnceLock::new(),
byte_to_width_table: std::sync::OnceLock::new(),
}
}
#[test]
fn test_lru_cache_insert_and_get() {
let mut cache = LruFontCache::new(16);
let font = Arc::new(make_test_font("Helvetica"));
assert!(cache.get(100).is_none());
cache.insert(100, Arc::clone(&font));
let cached = cache.get(100);
assert!(cached.is_some());
assert_eq!(cached.unwrap().base_font, "Helvetica");
}
#[test]
fn test_lru_cache_eviction() {
let mut cache = LruFontCache::new(3);
cache.insert(10, Arc::new(make_test_font("F1")));
cache.insert(20, Arc::new(make_test_font("F2")));
cache.insert(30, Arc::new(make_test_font("F3")));
cache.get(10);
cache.insert(40, Arc::new(make_test_font("F4")));
assert!(cache.get(10).is_some(), "F1 should still be cached");
assert!(cache.get(20).is_none(), "F2 should have been evicted");
assert!(cache.get(30).is_some(), "F3 should still be cached");
assert!(cache.get(40).is_some(), "F4 should be cached");
}
#[test]
fn test_lru_cache_clear() {
let mut cache = LruFontCache::new(16);
cache.insert(1, Arc::new(make_test_font("A")));
cache.insert(2, Arc::new(make_test_font("B")));
assert_eq!(cache.len(), 2);
cache.clear();
assert_eq!(cache.len(), 0);
assert!(cache.get(1).is_none());
}
#[test]
fn test_lru_cache_set_capacity() {
let mut cache = LruFontCache::new(16);
for i in 0..5 {
cache.insert(i, Arc::new(make_test_font(&format!("Font{}", i))));
}
assert_eq!(cache.len(), 5);
cache.set_capacity(2);
assert_eq!(cache.len(), 2);
assert_eq!(cache.capacity(), 2);
}
#[test]
fn test_lru_cache_duplicate_key_update() {
let mut cache = LruFontCache::new(16);
cache.insert(50, Arc::new(make_test_font("OldFont")));
assert_eq!(cache.get(50).unwrap().base_font, "OldFont");
cache.insert(50, Arc::new(make_test_font("NewFont")));
assert_eq!(cache.get(50).unwrap().base_font, "NewFont");
assert_eq!(cache.len(), 1, "Duplicate key should not increase size");
}
#[test]
fn test_lru_cache_generation_ordering() {
let mut cache = LruFontCache::new(3);
cache.insert(1, Arc::new(make_test_font("A"))); cache.insert(2, Arc::new(make_test_font("B"))); cache.insert(3, Arc::new(make_test_font("C")));
cache.get(1); cache.get(3);
cache.insert(4, Arc::new(make_test_font("D")));
assert!(cache.get(2).is_none(), "Key 2 should be evicted");
assert!(cache.get(1).is_some());
assert!(cache.get(3).is_some());
assert!(cache.get(4).is_some());
}
#[test]
fn test_global_api_insert_get_clear_stats() {
let key_base = 9_000_000u64;
let font = Arc::new(make_test_font("GlobalTestFont"));
global_font_cache_insert(key_base, Arc::clone(&font));
let cached = global_font_cache_get(key_base);
assert!(cached.is_some());
assert_eq!(cached.unwrap().base_font, "GlobalTestFont");
assert!(global_font_cache_get(key_base + 999).is_none());
let (size, cap) = global_font_cache_stats();
assert!(size >= 1);
assert!(cap > 0);
clear_global_font_cache();
assert!(global_font_cache_get(key_base).is_none());
let (size_after, _) = global_font_cache_stats();
assert_eq!(size_after, 0);
set_global_font_cache_capacity(DEFAULT_MAX_ENTRIES);
}
}