#![cfg(feature = "ciede2000")]
mod common;
use std::path::{Path, PathBuf};
use common::{Fixture, Image, Srgb8, srgb8};
use iqa::{Ciede2000Options, ciede2000};
const FIXTURE_DIR: &str = "tests/fixtures/ciede2000";
const CASES: &[(&str, &str, &str)] = &[
("gradient_lo", "gradient_ref.ppm", "gradient_lo.ppm"),
("gradient_hi", "gradient_ref.ppm", "gradient_hi.ppm"),
("solid_dist", "solid_ref.ppm", "solid_dist.ppm"),
("chroma_dist", "chroma_ref.ppm", "chroma_dist.ppm"),
];
const GOLDENS: &[(&str, f64)] = &[
("gradient_lo", 4.603349),
("gradient_hi", 15.278680),
("solid_dist", 3.169049),
("chroma_dist", 2.970071),
];
#[test]
fn matches_colour_science_ciede2000() {
for &(label, ref_ppm, dist_ppm) in CASES {
let reference = load(&fixture_path(ref_ppm));
let distorted = load(&fixture_path(dist_ppm));
let ours = ciede2000(&reference, &distorted, Ciede2000Options::default())
.unwrap_or_else(|e| panic!("{label}: ciede2000 errored: {e}"));
let golden = golden(label);
let tol = 1e-3 + 0.01 * golden;
let delta = (ours - golden).abs();
eprintln!("{label}: ours={ours:.6} colour-science={golden:.6} (Δ={delta:.6})");
assert!(
delta <= tol,
"{label}: ours={ours:.6} colour-science={golden:.6} (|Δ|={delta:.6} > tol={tol:.6})",
);
}
}
fn load(path: &Path) -> Image<Srgb8> {
let decoded = image::open(path)
.unwrap_or_else(|e| {
panic!(
"open {}: {e}\n\
Run `cargo test --features ciede2000 --test ciede2000_reference \
write_ciede2000_fixtures -- --ignored` to (re)generate fixtures.",
path.display(),
)
})
.to_rgb8();
let (width, height) = decoded.dimensions();
Image::srgb8(width, height, decoded.into_raw()).expect("fixture dimensions match its buffer")
}
fn fixture_path(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join(FIXTURE_DIR)
.join(name)
}
fn golden(label: &str) -> f64 {
GOLDENS
.iter()
.find(|(l, _)| *l == label)
.map(|&(_, v)| v)
.unwrap_or_else(|| panic!("no golden recorded for case `{label}`"))
}
fn fixtures() -> Vec<(&'static str, Image<Srgb8>)> {
let gradient = Srgb8::base(64, 64);
let solid = Srgb8::solid(64, 64, 128.0);
let chroma = srgb8(64, 64, chroma_sample);
vec![
("gradient_ref.ppm", Srgb8::base(64, 64)),
("gradient_lo.ppm", Srgb8::distort(&gradient, 10.0)),
("gradient_hi.ppm", Srgb8::distort(&gradient, 40.0)),
("solid_ref.ppm", Srgb8::solid(64, 64, 128.0)),
("solid_dist.ppm", Srgb8::distort(&solid, 5.0)),
("chroma_ref.ppm", srgb8(64, 64, chroma_sample)),
("chroma_dist.ppm", Srgb8::distort(&chroma, 20.0)),
]
}
fn chroma_sample(x: u32, y: u32, c: usize) -> u8 {
const PALETTE: [[u8; 3]; 8] = [
[255, 0, 0],
[0, 255, 0],
[0, 0, 255],
[255, 255, 0],
[0, 255, 255],
[255, 0, 255],
[255, 128, 0],
[128, 0, 255],
];
let band = ((x / 8) % 8) as usize;
let base = PALETTE[band][c] as f64;
let scale = 0.7 + 0.3 * (y % 16) as f64 / 15.0;
(base * scale).round().clamp(0.0, 255.0) as u8
}
#[test]
#[ignore = "regenerates committed PPM fixtures; rerun scripts/gen-ciede2000-goldens.py after"]
fn write_ciede2000_fixtures() {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join(FIXTURE_DIR);
std::fs::create_dir_all(&dir).expect("create fixture dir");
for (name, img) in fixtures() {
let mut bytes = format!("P6\n{} {}\n255\n", img.width(), img.height()).into_bytes();
bytes.extend_from_slice(img.samples());
std::fs::write(dir.join(name), bytes).unwrap_or_else(|e| panic!("write {name}: {e}"));
}
eprintln!("wrote {} fixtures to {}", fixtures().len(), dir.display());
}