use butteraugli::{ButteraugliParams, Img, RGB8, butteraugli as butteraugli_compare};
use super::icc::ColorProfile;
use crate::error::{Error, Result};
fn rgb_bytes_to_pixels(rgb: &[u8]) -> Vec<RGB8> {
rgb.chunks_exact(3)
.map(|c| RGB8::new(c[0], c[1], c[2]))
.collect()
}
pub fn calculate_butteraugli(
reference: &[u8],
test: &[u8],
width: usize,
height: usize,
) -> Result<f64> {
if reference.len() != test.len() {
return Err(Error::DimensionMismatch {
expected: (width, height),
actual: (test.len() / 3 / height, height),
});
}
let expected_len = width * height * 3;
if reference.len() != expected_len {
return Err(Error::MetricCalculation {
metric: "Butteraugli".to_string(),
reason: format!(
"Invalid image size: expected {} bytes, got {}",
expected_len,
reference.len()
),
});
}
let img1 = Img::new(rgb_bytes_to_pixels(reference), width, height);
let img2 = Img::new(rgb_bytes_to_pixels(test), width, height);
let params = ButteraugliParams::default();
let result = butteraugli_compare(img1.as_ref(), img2.as_ref(), ¶ms).map_err(|e| {
Error::MetricCalculation {
metric: "Butteraugli".to_string(),
reason: e.to_string(),
}
})?;
Ok(result.score)
}
pub fn calculate_butteraugli_with_intensity(
reference: &[u8],
test: &[u8],
width: usize,
height: usize,
intensity_target: f32,
) -> Result<f64> {
if reference.len() != test.len() {
return Err(Error::DimensionMismatch {
expected: (width, height),
actual: (test.len() / 3 / height, height),
});
}
let expected_len = width * height * 3;
if reference.len() != expected_len {
return Err(Error::MetricCalculation {
metric: "Butteraugli".to_string(),
reason: format!(
"Invalid image size: expected {} bytes, got {}",
expected_len,
reference.len()
),
});
}
let img1 = Img::new(rgb_bytes_to_pixels(reference), width, height);
let img2 = Img::new(rgb_bytes_to_pixels(test), width, height);
let params = ButteraugliParams::default().with_intensity_target(intensity_target);
let result = butteraugli_compare(img1.as_ref(), img2.as_ref(), ¶ms).map_err(|e| {
Error::MetricCalculation {
metric: "Butteraugli".to_string(),
reason: e.to_string(),
}
})?;
Ok(result.score)
}
pub fn calculate_butteraugli_icc(
reference: &[u8],
reference_profile: &ColorProfile,
test: &[u8],
test_profile: &ColorProfile,
width: usize,
height: usize,
) -> Result<f64> {
let (ref_srgb, test_srgb) =
super::icc::prepare_for_comparison(reference, reference_profile, test, test_profile)?;
calculate_butteraugli(&ref_srgb, &test_srgb, width, height)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_identical_images() {
let data: Vec<u8> = (0..100 * 100 * 3).map(|i| (i % 256) as u8).collect();
let score = calculate_butteraugli(&data, &data, 100, 100).unwrap();
assert!(
score < 0.01,
"Identical images should have score ~0, got {score}"
);
}
#[test]
fn test_different_images() {
let ref_data: Vec<u8> = vec![100u8; 100 * 100 * 3];
let test_data: Vec<u8> = vec![200u8; 100 * 100 * 3];
let score = calculate_butteraugli(&ref_data, &test_data, 100, 100).unwrap();
assert!(
score > 1.0,
"Very different images should have high score, got {score}"
);
}
#[test]
fn test_size_mismatch() {
let small: Vec<u8> = vec![128u8; 50 * 50 * 3];
let large: Vec<u8> = vec![128u8; 100 * 100 * 3];
let result = calculate_butteraugli(&small, &large, 100, 100);
assert!(result.is_err());
}
#[test]
fn test_custom_intensity() {
let data: Vec<u8> = (0..100 * 100 * 3).map(|i| (i % 256) as u8).collect();
let score = calculate_butteraugli_with_intensity(&data, &data, 100, 100, 250.0).unwrap();
assert!(
score < 0.01,
"Identical images should have score ~0 at any intensity"
);
}
}