use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
const DEFAULT_CACHE_SIZE: usize = 50;
pub const MAX_IMAGE_FILE_SIZE: u64 = 10 * 1024 * 1024;
pub const MAX_IMAGE_DIMENSION: u32 = 8192;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct ImageMetadata {
pub source_path: Option<String>,
pub width: u32,
pub height: u32,
pub channels: u8,
pub mime: String,
pub sha256: Option<String>,
pub is_mask: bool,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct CachedImage {
pub png_data: Vec<u8>,
pub metadata: ImageMetadata,
access_order: u64,
}
pub struct ImageCache {
entries: HashMap<String, CachedImage>,
max_size: usize,
counter: AtomicU64,
}
impl Default for ImageCache {
fn default() -> Self {
Self::new(DEFAULT_CACHE_SIZE)
}
}
#[allow(dead_code)]
impl ImageCache {
pub fn new(max_size: usize) -> Self {
Self {
entries: HashMap::with_capacity(max_size),
max_size,
counter: AtomicU64::new(0),
}
}
pub fn store(
&mut self,
png_data: Vec<u8>,
metadata: ImageMetadata,
id_prefix: Option<&str>,
) -> String {
let id = self.next_id(id_prefix);
if self.entries.len() >= self.max_size {
self.evict_lru();
}
let access_order = self.counter.fetch_add(1, Ordering::Relaxed);
self.entries.insert(
id.clone(),
CachedImage {
png_data,
metadata,
access_order,
},
);
id
}
pub fn get(&mut self, id: &str) -> Option<&CachedImage> {
if let Some(entry) = self.entries.get_mut(id) {
entry.access_order = self.counter.fetch_add(1, Ordering::Relaxed);
}
self.entries.get(id)
}
pub fn peek(&self, id: &str) -> Option<&CachedImage> {
self.entries.get(id)
}
pub fn contains(&self, id: &str) -> bool {
self.entries.contains_key(id)
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
fn next_id(&self, prefix: Option<&str>) -> String {
let n = self.counter.fetch_add(1, Ordering::Relaxed);
match prefix {
Some(p) => format!("{}-{}", p, n),
None => format!("image-{}", n),
}
}
fn evict_lru(&mut self) {
if let Some((lru_id, _)) = self
.entries
.iter()
.min_by_key(|(_, entry)| entry.access_order)
{
let lru_id = lru_id.clone();
self.entries.remove(&lru_id);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_metadata() -> ImageMetadata {
ImageMetadata {
source_path: None,
width: 100,
height: 100,
channels: 4,
mime: "image/png".to_string(),
sha256: None,
is_mask: false,
}
}
#[test]
fn test_store_and_get() {
let mut cache = ImageCache::new(5);
let data = vec![1, 2, 3, 4];
let id = cache.store(data.clone(), make_metadata(), None);
let retrieved = cache.get(&id).unwrap();
assert_eq!(retrieved.png_data, data);
}
#[test]
fn test_store_with_prefix() {
let mut cache = ImageCache::new(5);
let id = cache.store(vec![1, 2, 3], make_metadata(), Some("template"));
assert!(id.starts_with("template-"));
}
#[test]
fn test_lru_eviction() {
let mut cache = ImageCache::new(3);
let id1 = cache.store(vec![1], make_metadata(), None);
let id2 = cache.store(vec![2], make_metadata(), None);
let id3 = cache.store(vec![3], make_metadata(), None);
assert_eq!(cache.len(), 3);
cache.get(&id1);
let _id4 = cache.store(vec![4], make_metadata(), None);
assert_eq!(cache.len(), 3);
assert!(cache.contains(&id1));
assert!(!cache.contains(&id2)); assert!(cache.contains(&id3));
}
#[test]
fn test_clear() {
let mut cache = ImageCache::new(5);
cache.store(vec![1], make_metadata(), None);
cache.store(vec![2], make_metadata(), None);
assert_eq!(cache.len(), 2);
cache.clear();
assert!(cache.is_empty());
}
#[test]
fn test_peek_does_not_update_lru() {
let mut cache = ImageCache::new(3);
let id1 = cache.store(vec![1], make_metadata(), None);
let id2 = cache.store(vec![2], make_metadata(), None);
let id3 = cache.store(vec![3], make_metadata(), None);
let _ = cache.peek(&id1);
let _id4 = cache.store(vec![4], make_metadata(), None);
assert!(!cache.contains(&id1)); assert!(cache.contains(&id2));
assert!(cache.contains(&id3));
}
}