use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use crate::{MunsellColor, ColorMetadata, MunsellError};
const CACHE_SIZE: usize = 500;
#[derive(Clone, Debug)]
pub struct CachedColorResult {
pub rgb: [u8; 3],
pub munsell: MunsellColor,
pub iscc_nbs: Option<ColorMetadata>,
}
#[derive(Clone)]
pub struct UnifiedColorCache {
cache: Arc<Mutex<VecDeque<([u8; 3], CachedColorResult)>>>,
max_size: usize,
}
impl UnifiedColorCache {
pub fn new() -> Self {
Self::with_capacity(CACHE_SIZE)
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
cache: Arc::new(Mutex::new(VecDeque::with_capacity(capacity))),
max_size: capacity,
}
}
pub fn get(&self, rgb: &[u8; 3]) -> Option<CachedColorResult> {
let cache = self.cache.lock().unwrap();
for (cached_rgb, result) in cache.iter().rev() {
if cached_rgb == rgb {
return Some(result.clone());
}
}
None
}
pub fn insert(&self, rgb: [u8; 3], result: CachedColorResult) {
let mut cache = self.cache.lock().unwrap();
cache.retain(|(cached_rgb, _)| cached_rgb != &rgb);
cache.push_back((rgb, result));
if cache.len() > self.max_size {
cache.pop_front();
}
}
pub fn clear(&self) {
let mut cache = self.cache.lock().unwrap();
cache.clear();
}
pub fn len(&self) -> usize {
let cache = self.cache.lock().unwrap();
cache.len()
}
pub fn is_empty(&self) -> bool {
let cache = self.cache.lock().unwrap();
cache.is_empty()
}
pub fn stats(&self) -> CacheStats {
let cache = self.cache.lock().unwrap();
CacheStats {
current_size: cache.len(),
max_size: self.max_size,
capacity: cache.capacity(),
}
}
}
impl Default for UnifiedColorCache {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub current_size: usize,
pub max_size: usize,
pub capacity: usize,
}
pub fn hex_to_rgb(hex: &str) -> Result<[u8; 3], MunsellError> {
let hex = hex.trim().trim_start_matches('#').to_uppercase();
let rgb = if hex.len() == 3 {
let r = u8::from_str_radix(&hex[0..1].repeat(2), 16)
.map_err(|_| MunsellError::ConversionError {
message: format!("Invalid hex color: {}", hex)
})?;
let g = u8::from_str_radix(&hex[1..2].repeat(2), 16)
.map_err(|_| MunsellError::ConversionError {
message: format!("Invalid hex color: {}", hex)
})?;
let b = u8::from_str_radix(&hex[2..3].repeat(2), 16)
.map_err(|_| MunsellError::ConversionError {
message: format!("Invalid hex color: {}", hex)
})?;
[r, g, b]
} else if hex.len() == 6 {
let r = u8::from_str_radix(&hex[0..2], 16)
.map_err(|_| MunsellError::ConversionError {
message: format!("Invalid hex color: {}", hex)
})?;
let g = u8::from_str_radix(&hex[2..4], 16)
.map_err(|_| MunsellError::ConversionError {
message: format!("Invalid hex color: {}", hex)
})?;
let b = u8::from_str_radix(&hex[4..6], 16)
.map_err(|_| MunsellError::ConversionError {
message: format!("Invalid hex color: {}", hex)
})?;
[r, g, b]
} else {
return Err(MunsellError::ConversionError {
message: format!("Invalid hex color length: expected 3 or 6 characters, got {}", hex.len())
});
};
Ok(rgb)
}
pub fn lab_to_rgb(lab: [f64; 3]) -> Result<[u8; 3], MunsellError> {
use palette::{Lab, Srgb, white_point::D65, convert::IntoColor};
let lab_color = Lab::<D65, f64>::new(lab[0], lab[1], lab[2]);
let srgb: Srgb<f64> = lab_color.into_color();
Ok([
(srgb.red * 255.0).round().clamp(0.0, 255.0) as u8,
(srgb.green * 255.0).round().clamp(0.0, 255.0) as u8,
(srgb.blue * 255.0).round().clamp(0.0, 255.0) as u8,
])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hex_parsing() {
assert_eq!(hex_to_rgb("#FF0000").unwrap(), [255, 0, 0]);
assert_eq!(hex_to_rgb("#ff0000").unwrap(), [255, 0, 0]);
assert_eq!(hex_to_rgb("FF0000").unwrap(), [255, 0, 0]);
assert_eq!(hex_to_rgb("ff0000").unwrap(), [255, 0, 0]);
assert_eq!(hex_to_rgb("#F00").unwrap(), [255, 0, 0]);
assert_eq!(hex_to_rgb("#f00").unwrap(), [255, 0, 0]);
assert_eq!(hex_to_rgb("F00").unwrap(), [255, 0, 0]);
assert_eq!(hex_to_rgb("f00").unwrap(), [255, 0, 0]);
}
#[test]
fn test_cache_fifo_eviction() {
let cache = UnifiedColorCache::with_capacity(3);
let result1 = CachedColorResult {
rgb: [255, 0, 0],
munsell: MunsellColor {
hue: Some("5R".to_string()),
value: 5.0,
chroma: Some(10.0),
notation: "5R 5.0/10.0".to_string(),
},
iscc_nbs: None,
};
let result2 = result1.clone();
let result3 = result1.clone();
let result4 = result1.clone();
cache.insert([1, 0, 0], result1.clone());
cache.insert([2, 0, 0], result2.clone());
cache.insert([3, 0, 0], result3.clone());
assert_eq!(cache.len(), 3);
cache.insert([4, 0, 0], result4.clone());
assert_eq!(cache.len(), 3);
assert!(cache.get(&[1, 0, 0]).is_none());
assert!(cache.get(&[2, 0, 0]).is_some());
assert!(cache.get(&[3, 0, 0]).is_some());
assert!(cache.get(&[4, 0, 0]).is_some());
}
#[test]
fn test_cache_thread_safety() {
use std::thread;
let cache = Arc::new(UnifiedColorCache::with_capacity(100));
let mut handles = vec![];
for i in 0..10 {
let cache_clone = Arc::clone(&cache);
let handle = thread::spawn(move || {
let result = CachedColorResult {
rgb: [i as u8, 0, 0],
munsell: MunsellColor {
hue: Some(format!("{}R", i)),
value: i as f64,
chroma: Some(i as f64),
notation: format!("{}R {}.0/{}.0", i, i, i),
},
iscc_nbs: None,
};
for j in 0..10 {
let rgb = [(i * 10 + j) as u8, 0, 0];
cache_clone.insert(rgb, result.clone());
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
assert!(cache.len() > 0);
assert!(cache.len() <= 100);
}
}