use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::metrics::{MetricResult, PerceptionLevel};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodecResult {
pub codec_id: String,
pub codec_version: String,
pub quality: f64,
pub file_size: usize,
pub bits_per_pixel: f64,
#[serde(with = "duration_millis")]
pub encode_time: Duration,
#[serde(with = "duration_millis_option")]
pub decode_time: Option<Duration>,
pub metrics: MetricResult,
pub perception: Option<PerceptionLevel>,
pub cached_path: Option<PathBuf>,
#[serde(default)]
pub codec_params: HashMap<String, String>,
}
impl CodecResult {
#[must_use]
pub fn compression_ratio(&self, original_size: usize) -> f64 {
if self.file_size == 0 {
0.0
} else {
original_size as f64 / self.file_size as f64
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageReport {
pub name: String,
pub source_path: Option<PathBuf>,
pub width: u32,
pub height: u32,
pub uncompressed_size: usize,
pub results: Vec<CodecResult>,
#[serde(with = "chrono_serde")]
pub timestamp: chrono::DateTime<chrono::Utc>,
}
impl ImageReport {
#[must_use]
pub fn new(name: String, width: u32, height: u32) -> Self {
Self {
name,
source_path: None,
width,
height,
uncompressed_size: (width as usize) * (height as usize) * 3,
results: Vec::new(),
timestamp: chrono::Utc::now(),
}
}
pub fn results_for_codec(&self, codec_id: &str) -> impl Iterator<Item = &CodecResult> {
self.results.iter().filter(move |r| r.codec_id == codec_id)
}
#[must_use]
pub fn best_at_size(&self, max_bytes: usize) -> Option<&CodecResult> {
self.results
.iter()
.filter(|r| r.file_size <= max_bytes)
.max_by(|a, b| {
let a_quality = a.metrics.dssim.map(|d| -d).unwrap_or(f64::NEG_INFINITY);
let b_quality = b.metrics.dssim.map(|d| -d).unwrap_or(f64::NEG_INFINITY);
a_quality
.partial_cmp(&b_quality)
.unwrap_or(std::cmp::Ordering::Equal)
})
}
#[must_use]
pub fn smallest_at_quality(&self, max_dssim: f64) -> Option<&CodecResult> {
self.results
.iter()
.filter(|r| r.metrics.dssim.map_or(false, |d| d <= max_dssim))
.min_by_key(|r| r.file_size)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CorpusReport {
pub name: String,
pub images: Vec<ImageReport>,
#[serde(with = "chrono_serde")]
pub timestamp: chrono::DateTime<chrono::Utc>,
pub config_summary: String,
}
impl CorpusReport {
#[must_use]
pub fn new(name: String) -> Self {
Self {
name,
images: Vec::new(),
timestamp: chrono::Utc::now(),
config_summary: String::new(),
}
}
#[must_use]
pub fn total_results(&self) -> usize {
self.images.iter().map(|img| img.results.len()).sum()
}
#[must_use]
pub fn codec_ids(&self) -> Vec<String> {
let mut ids: Vec<String> = self
.images
.iter()
.flat_map(|img| img.results.iter().map(|r| r.codec_id.clone()))
.collect();
ids.sort();
ids.dedup();
ids
}
}
mod duration_millis {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::time::Duration;
pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
duration.as_millis().serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
let millis = u64::deserialize(deserializer)?;
Ok(Duration::from_millis(millis))
}
}
mod duration_millis_option {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::time::Duration;
pub fn serialize<S>(duration: &Option<Duration>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
duration.map(|d| d.as_millis()).serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
where
D: Deserializer<'de>,
{
let millis: Option<u64> = Option::deserialize(deserializer)?;
Ok(millis.map(Duration::from_millis))
}
}
mod chrono_serde {
use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S>(dt: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
dt.to_rfc3339().serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
DateTime::parse_from_rfc3339(&s)
.map(|dt| dt.with_timezone(&Utc))
.map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_image_report_new() {
let report = ImageReport::new("test.png".to_string(), 1920, 1080);
assert_eq!(report.name, "test.png");
assert_eq!(report.width, 1920);
assert_eq!(report.height, 1080);
assert_eq!(report.uncompressed_size, 1920 * 1080 * 3);
}
#[test]
fn test_codec_result_compression_ratio() {
let result = CodecResult {
codec_id: "test".to_string(),
codec_version: "1.0".to_string(),
quality: 80.0,
file_size: 1000,
bits_per_pixel: 0.5,
encode_time: Duration::from_millis(100),
decode_time: None,
metrics: MetricResult::default(),
perception: None,
cached_path: None,
codec_params: HashMap::new(),
};
assert!((result.compression_ratio(10000) - 10.0).abs() < 0.001);
}
#[test]
fn test_corpus_report_codec_ids() {
let mut report = CorpusReport::new("test".to_string());
let mut img = ImageReport::new("img1.png".to_string(), 100, 100);
img.results.push(CodecResult {
codec_id: "mozjpeg".to_string(),
codec_version: "4.0".to_string(),
quality: 80.0,
file_size: 1000,
bits_per_pixel: 0.8,
encode_time: Duration::from_millis(50),
decode_time: None,
metrics: MetricResult::default(),
perception: None,
cached_path: None,
codec_params: HashMap::new(),
});
img.results.push(CodecResult {
codec_id: "webp".to_string(),
codec_version: "1.0".to_string(),
quality: 80.0,
file_size: 900,
bits_per_pixel: 0.72,
encode_time: Duration::from_millis(60),
decode_time: None,
metrics: MetricResult::default(),
perception: None,
cached_path: None,
codec_params: HashMap::new(),
});
report.images.push(img);
let ids = report.codec_ids();
assert_eq!(ids.len(), 2);
assert!(ids.contains(&"mozjpeg".to_string()));
assert!(ids.contains(&"webp".to_string()));
}
}