use super::FontInfo;
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, OnceLock, RwLock};
const DEFAULT_MAX_ENTRIES: usize = 1024;
struct FifoFontCache {
entries: HashMap<u64, Arc<FontInfo>>,
order: VecDeque<u64>,
max_entries: usize,
}
impl FifoFontCache {
fn new(max_entries: usize) -> Self {
Self {
entries: HashMap::with_capacity(max_entries / 2),
order: VecDeque::new(),
max_entries,
}
}
fn get(&self, key: u64) -> Option<Arc<FontInfo>> {
self.entries.get(&key).map(Arc::clone)
}
fn insert(&mut self, key: u64, font: Arc<FontInfo>) {
if self.entries.insert(key, font).is_some() {
return;
}
self.order.push_back(key);
while self.entries.len() > self.max_entries {
match self.order.pop_front() {
Some(old) => {
self.entries.remove(&old);
},
None => break,
}
}
}
fn len(&self) -> usize {
self.entries.len()
}
fn capacity(&self) -> usize {
self.max_entries
}
fn clear(&mut self) {
self.entries.clear();
self.order.clear();
}
fn set_capacity(&mut self, new_max: usize) {
self.max_entries = new_max;
while self.entries.len() > self.max_entries {
match self.order.pop_front() {
Some(old) => {
self.entries.remove(&old);
},
None => break,
}
}
}
}
static GLOBAL_FONT_CACHE: OnceLock<RwLock<FifoFontCache>> = OnceLock::new();
fn cache() -> &'static RwLock<FifoFontCache> {
GLOBAL_FONT_CACHE.get_or_init(|| RwLock::new(FifoFontCache::new(DEFAULT_MAX_ENTRIES)))
}
pub fn global_font_cache_get(identity_hash: u64) -> Option<Arc<FontInfo>> {
cache().read().ok()?.get(identity_hash)
}
pub fn global_font_cache_insert(identity_hash: u64, font: Arc<FontInfo>) {
if let Ok(mut guard) = cache().write() {
guard.insert(identity_hash, font);
}
}
pub fn clear_global_font_cache() {
if let Ok(mut guard) = cache().write() {
guard.clear();
}
}
pub fn global_font_cache_stats() -> (usize, usize) {
cache()
.read()
.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().write() {
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,
ascent: 0.95,
descent: -0.35,
embedded_font_data: None,
truetype_cmap: std::sync::OnceLock::new(),
embedded_glyph_names: 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,
font_matrix_a: 0.001,
default_width: 600.0,
cid_widths: None,
cid_default_width: 1000.0,
has_explicit_dw: false,
cff_gid_map: None,
multi_char_map: HashMap::new(),
byte_to_char_table: std::sync::OnceLock::new(),
type0_unicode_memo: std::sync::Arc::new(std::sync::Mutex::new(
std::collections::HashMap::new(),
)),
byte_to_width_table: std::sync::OnceLock::new(),
diff_glyph_names: std::collections::HashMap::new(),
}
}
#[test]
fn test_fifo_cache_insert_and_get() {
let mut cache = FifoFontCache::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_fifo_cache_eviction() {
let mut cache = FifoFontCache::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_none(), "F1 (oldest) should have been evicted");
assert!(cache.get(20).is_some(), "F2 should still be cached");
assert!(cache.get(30).is_some(), "F3 should still be cached");
assert!(cache.get(40).is_some(), "F4 should be cached");
}
#[test]
fn test_fifo_cache_clear() {
let mut cache = FifoFontCache::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_fifo_cache_set_capacity() {
let mut cache = FifoFontCache::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);
assert!(cache.get(3).is_some());
assert!(cache.get(4).is_some());
}
#[test]
fn test_fifo_cache_duplicate_key_update() {
let mut cache = FifoFontCache::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_fifo_cache_eviction_order_is_insertion_order() {
let mut cache = FifoFontCache::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(1).is_none(), "Key 1 (first in) should be evicted");
assert!(cache.get(2).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);
}
}