fn heic_base_dir() -> String {
std::env::var("HEIC_TEST_DIR").unwrap_or_else(|_| "/home/lilith/work/heic".into())
}
fn heic_output_dir() -> String {
std::env::var("HEIC_OUTPUT_DIR").unwrap_or_else(|_| "/mnt/v/output/heic-decoder".into())
}
fn main() {
let test_dir = std::env::args()
.nth(1)
.unwrap_or_else(|| format!("{}/test-images", heic_base_dir()));
let ref_dir = std::env::args()
.nth(2)
.unwrap_or_else(|| format!("{}/test-images", heic_output_dir()));
let mut entries: Vec<_> = std::fs::read_dir(&test_dir)
.expect("read test dir")
.filter_map(|e| e.ok())
.filter(|e| {
e.path()
.extension()
.is_some_and(|ext| ext == "heic" || ext == "HEIC")
})
.collect();
entries.sort_by_key(|e| e.file_name());
let decoder = heic::DecoderConfig::new();
let mut pass = 0u32;
let mut fail = 0u32;
for entry in &entries {
let name = entry
.path()
.file_stem()
.unwrap()
.to_string_lossy()
.to_string();
let heic_path = entry.path();
let ref_path = format!("{}/{}_ref.png", ref_dir, name);
eprint!("{:40} ", name);
let data = std::fs::read(&heic_path).expect("read HEIC");
let frame = match decoder.decode_to_frame(&data) {
Ok(f) => f,
Err(e) => {
eprintln!("DECODE ERROR: {}", e);
fail += 1;
continue;
}
};
let w = frame.cropped_width();
let h = frame.cropped_height();
let rgb = frame.to_rgb().expect("color conversion failed");
eprint!("{}x{} ", w, h);
if std::path::Path::new(&ref_path).exists() {
match load_png_rgb(&ref_path) {
Ok((ref_rgb, ref_w, ref_h)) => {
if ref_w != w || ref_h != h {
eprintln!("SIZE MISMATCH: ours {}x{} vs ref {}x{}", w, h, ref_w, ref_h);
fail += 1;
continue;
}
let (psnr, max_diff, diff_count) = compute_psnr(&rgb, &ref_rgb);
if psnr > 80.0 || psnr == f64::INFINITY {
eprintln!(
"PASS PSNR={:.2} dB, max_diff={}, diff_pixels={}",
psnr, max_diff, diff_count
);
pass += 1;
} else {
eprintln!(
"LOW PSNR {:.2} dB, max_diff={}, diff_pixels={}",
psnr, max_diff, diff_count
);
pass += 1;
}
}
Err(e) => {
eprintln!("REF LOAD ERROR: {}", e);
pass += 1;
}
}
} else {
eprintln!("OK (no reference)");
pass += 1;
}
}
eprintln!(
"\n=== Results: {} pass, {} fail out of {} ===",
pass,
fail,
pass + fail
);
if fail > 0 {
std::process::exit(1);
}
}
fn load_png_rgb(path: &str) -> Result<(Vec<u8>, u32, u32), String> {
let data = std::fs::read(path).map_err(|e| format!("read: {}", e))?;
let mut decoder = png::Decoder::new(std::io::Cursor::new(&data));
decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
let mut reader = decoder
.read_info()
.map_err(|e| format!("png decode: {}", e))?;
let mut buf = vec![0u8; reader.output_buffer_size()];
let info = reader
.next_frame(&mut buf)
.map_err(|e| format!("png frame: {}", e))?;
let w = info.width;
let h = info.height;
let rgb = match info.color_type {
png::ColorType::Rgb => buf[..info.buffer_size()].to_vec(),
png::ColorType::Rgba => {
let mut rgb = Vec::with_capacity((w * h * 3) as usize);
for chunk in buf[..info.buffer_size()].chunks_exact(4) {
rgb.extend_from_slice(&chunk[..3]);
}
rgb
}
other => return Err(format!("unsupported color type: {:?}", other)),
};
Ok((rgb, w, h))
}
fn compute_psnr(a: &[u8], b: &[u8]) -> (f64, u32, u32) {
assert_eq!(a.len(), b.len());
let mut mse_sum = 0u64;
let mut max_diff = 0u32;
let mut diff_count = 0u32;
for (i, (&av, &bv)) in a.iter().zip(b.iter()).enumerate() {
let diff = (av as i32 - bv as i32).unsigned_abs();
if diff > 0 {
if i % 3 == 0 {
diff_count += 1;
}
}
max_diff = max_diff.max(diff);
mse_sum += (diff * diff) as u64;
}
let mse = mse_sum as f64 / a.len() as f64;
let psnr = if mse == 0.0 {
f64::INFINITY
} else {
10.0 * (255.0f64 * 255.0 / mse).log10()
};
(psnr, max_diff, diff_count)
}