use std::collections::HashMap;
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExternalResult {
pub image_name: String,
pub codec: String,
pub codec_version: Option<String>,
pub quality_setting: Option<f64>,
pub file_size: Option<usize>,
pub bits_per_pixel: Option<f64>,
pub ssimulacra2: Option<f64>,
pub dssim: Option<f64>,
pub psnr: Option<f64>,
pub butteraugli: Option<f64>,
pub encode_time_ms: Option<f64>,
#[serde(default)]
pub extra: HashMap<String, String>,
}
#[derive(Debug, Clone, Default)]
pub struct CsvSchema {
pub image_column: Option<String>,
pub codec_column: Option<String>,
pub codec_version_column: Option<String>,
pub quality_column: Option<String>,
pub size_column: Option<String>,
pub bpp_column: Option<String>,
pub ssimulacra2_column: Option<String>,
pub dssim_column: Option<String>,
pub psnr_column: Option<String>,
pub butteraugli_column: Option<String>,
pub encode_time_column: Option<String>,
}
impl CsvSchema {
#[must_use]
pub fn builder() -> CsvSchemaBuilder {
CsvSchemaBuilder::default()
}
#[must_use]
pub fn auto_detect() -> Self {
Self::default()
}
fn find_column(
&self,
headers: &[&str],
primary: Option<&str>,
aliases: &[&str],
) -> Option<usize> {
if let Some(name) = primary {
if let Some(idx) = find_header_index(headers, name) {
return Some(idx);
}
}
for alias in aliases {
if let Some(idx) = find_header_index(headers, alias) {
return Some(idx);
}
}
None
}
}
#[derive(Debug, Default)]
pub struct CsvSchemaBuilder {
schema: CsvSchema,
}
impl CsvSchemaBuilder {
#[must_use]
pub fn image_column(mut self, name: impl Into<String>) -> Self {
self.schema.image_column = Some(name.into());
self
}
#[must_use]
pub fn codec_column(mut self, name: impl Into<String>) -> Self {
self.schema.codec_column = Some(name.into());
self
}
#[must_use]
pub fn codec_version_column(mut self, name: impl Into<String>) -> Self {
self.schema.codec_version_column = Some(name.into());
self
}
#[must_use]
pub fn quality_column(mut self, name: impl Into<String>) -> Self {
self.schema.quality_column = Some(name.into());
self
}
#[must_use]
pub fn size_column(mut self, name: impl Into<String>) -> Self {
self.schema.size_column = Some(name.into());
self
}
#[must_use]
pub fn bpp_column(mut self, name: impl Into<String>) -> Self {
self.schema.bpp_column = Some(name.into());
self
}
#[must_use]
pub fn ssimulacra2_column(mut self, name: impl Into<String>) -> Self {
self.schema.ssimulacra2_column = Some(name.into());
self
}
#[must_use]
pub fn dssim_column(mut self, name: impl Into<String>) -> Self {
self.schema.dssim_column = Some(name.into());
self
}
#[must_use]
pub fn psnr_column(mut self, name: impl Into<String>) -> Self {
self.schema.psnr_column = Some(name.into());
self
}
#[must_use]
pub fn butteraugli_column(mut self, name: impl Into<String>) -> Self {
self.schema.butteraugli_column = Some(name.into());
self
}
#[must_use]
pub fn encode_time_column(mut self, name: impl Into<String>) -> Self {
self.schema.encode_time_column = Some(name.into());
self
}
#[must_use]
pub fn build(self) -> CsvSchema {
self.schema
}
}
pub struct CsvImporter {
schema: CsvSchema,
}
impl CsvImporter {
#[must_use]
pub fn new(schema: CsvSchema) -> Self {
Self { schema }
}
#[must_use]
pub fn auto_detect() -> Self {
Self::new(CsvSchema::auto_detect())
}
pub fn import(&self, path: impl AsRef<Path>) -> Result<Vec<ExternalResult>> {
let path = path.as_ref();
let mut reader = csv::Reader::from_path(path)?;
let headers: Vec<String> = reader.headers()?.iter().map(String::from).collect();
let header_refs: Vec<&str> = headers.iter().map(String::as_str).collect();
let image_idx = self.schema.find_column(
&header_refs,
self.schema.image_column.as_deref(),
&["image", "filename", "file", "name", "source", "input"],
);
let codec_idx = self.schema.find_column(
&header_refs,
self.schema.codec_column.as_deref(),
&["codec", "encoder", "format", "method"],
);
let version_idx = self.schema.find_column(
&header_refs,
self.schema.codec_version_column.as_deref(),
&["version", "codec_version", "encoder_version"],
);
let quality_idx = self.schema.find_column(
&header_refs,
self.schema.quality_column.as_deref(),
&["quality", "q", "qp", "crf", "effort"],
);
let size_idx = self.schema.find_column(
&header_refs,
self.schema.size_column.as_deref(),
&["size", "file_size", "bytes", "filesize"],
);
let bpp_idx = self.schema.find_column(
&header_refs,
self.schema.bpp_column.as_deref(),
&["bpp", "bits_per_pixel", "bitrate"],
);
let ssimulacra2_idx = self.schema.find_column(
&header_refs,
self.schema.ssimulacra2_column.as_deref(),
&["ssimulacra2", "ssim2", "ssimulacra_2"],
);
let dssim_idx = self.schema.find_column(
&header_refs,
self.schema.dssim_column.as_deref(),
&["dssim", "ssim", "ms_ssim", "ms-ssim"],
);
let psnr_idx = self.schema.find_column(
&header_refs,
self.schema.psnr_column.as_deref(),
&["psnr", "psnr_db", "psnr-hvs"],
);
let butteraugli_idx = self.schema.find_column(
&header_refs,
self.schema.butteraugli_column.as_deref(),
&["butteraugli", "butter", "ba"],
);
let encode_time_idx = self.schema.find_column(
&header_refs,
self.schema.encode_time_column.as_deref(),
&["encode_time", "encode_ms", "time_ms", "encoding_time"],
);
let image_idx = image_idx.ok_or_else(|| Error::CsvImport {
line: 0,
reason: "Could not find image/filename column".to_string(),
})?;
let codec_idx = codec_idx.ok_or_else(|| Error::CsvImport {
line: 0,
reason: "Could not find codec/encoder column".to_string(),
})?;
let mut results = Vec::new();
for (line_num, record) in reader.records().enumerate() {
let record = record.map_err(|e| Error::CsvImport {
line: line_num + 2, reason: e.to_string(),
})?;
let image_name = record.get(image_idx).unwrap_or("").to_string();
let codec = record.get(codec_idx).unwrap_or("").to_string();
if image_name.is_empty() || codec.is_empty() {
continue;
}
let result = ExternalResult {
image_name,
codec,
codec_version: version_idx.and_then(|i| record.get(i)).map(String::from),
quality_setting: quality_idx
.and_then(|i| record.get(i))
.and_then(|s| s.parse().ok()),
file_size: size_idx
.and_then(|i| record.get(i))
.and_then(|s| s.parse().ok()),
bits_per_pixel: bpp_idx
.and_then(|i| record.get(i))
.and_then(|s| s.parse().ok()),
ssimulacra2: ssimulacra2_idx
.and_then(|i| record.get(i))
.and_then(|s| s.parse().ok()),
dssim: dssim_idx
.and_then(|i| record.get(i))
.and_then(|s| s.parse().ok()),
psnr: psnr_idx
.and_then(|i| record.get(i))
.and_then(|s| s.parse().ok()),
butteraugli: butteraugli_idx
.and_then(|i| record.get(i))
.and_then(|s| s.parse().ok()),
encode_time_ms: encode_time_idx
.and_then(|i| record.get(i))
.and_then(|s| s.parse().ok()),
extra: HashMap::new(),
};
results.push(result);
}
Ok(results)
}
}
fn find_header_index(headers: &[&str], name: &str) -> Option<usize> {
let name_lower = name.to_lowercase();
headers.iter().position(|h| h.to_lowercase() == name_lower)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_schema_builder() {
let schema = CsvSchema::builder()
.image_column("img")
.codec_column("enc")
.quality_column("q")
.build();
assert_eq!(schema.image_column, Some("img".to_string()));
assert_eq!(schema.codec_column, Some("enc".to_string()));
assert_eq!(schema.quality_column, Some("q".to_string()));
}
#[test]
fn test_find_header_index() {
let headers = ["Image", "Codec", "Quality", "DSSIM"];
assert_eq!(find_header_index(&headers, "image"), Some(0));
assert_eq!(find_header_index(&headers, "QUALITY"), Some(2));
assert_eq!(find_header_index(&headers, "unknown"), None);
}
}