use heic::{DecoderConfig, ImageInfo, PixelLayout};
const SEARCH_DIRS: &[&str] = &[
"/mnt/v/heic",
"/home/lilith/work/heic/libheif/examples",
"/home/lilith/work/heic/test-images",
];
fn find_heic_files() -> Vec<std::path::PathBuf> {
let mut files = Vec::new();
for dir in SEARCH_DIRS {
let path = std::path::Path::new(dir);
if !path.is_dir() {
continue;
}
if let Ok(entries) = std::fs::read_dir(path) {
for entry in entries.flatten() {
let p = entry.path();
if p.extension()
.and_then(|e| e.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("heic"))
{
files.push(p);
}
}
}
}
files.sort();
files
}
fn check_parity(path: &std::path::Path) -> (String, Vec<String>) {
let name = path.file_name().unwrap().to_string_lossy().to_string();
let mut mismatches = Vec::new();
let data = match std::fs::read(path) {
Ok(d) => d,
Err(e) => {
mismatches.push(format!("read error: {e}"));
return (name, mismatches);
}
};
let probe = match ImageInfo::from_bytes(&data) {
Ok(info) => info,
Err(e) => {
mismatches.push(format!("probe error: {e}"));
return (name, mismatches);
}
};
let decoder = DecoderConfig::new();
let frame = match decoder.decode_to_frame(&data) {
Ok(f) => f,
Err(e) => {
mismatches.push(format!("decode_to_frame error: {e}"));
return (name, mismatches);
}
};
let rgb_output = DecoderConfig::new().decode(&data, PixelLayout::Rgb8).ok();
let frame_w = frame.cropped_width();
let frame_h = frame.cropped_height();
if probe.width != frame_w {
mismatches.push(format!(
"width: probe={} frame_cropped={}",
probe.width, frame_w
));
}
if probe.height != frame_h {
mismatches.push(format!(
"height: probe={} frame_cropped={}",
probe.height, frame_h
));
}
if let Some(ref rgb) = rgb_output {
if probe.width != rgb.width {
mismatches.push(format!(
"width: probe={} rgb_decode={}",
probe.width, rgb.width
));
}
if probe.height != rgb.height {
mismatches.push(format!(
"height: probe={} rgb_decode={}",
probe.height, rgb.height
));
}
}
if probe.bit_depth != frame.bit_depth {
mismatches.push(format!(
"bit_depth: probe={} frame={}",
probe.bit_depth, frame.bit_depth
));
}
if probe.chroma_format != frame.chroma_format {
mismatches.push(format!(
"chroma_format: probe={} frame={}",
probe.chroma_format, frame.chroma_format
));
}
let frame_has_alpha = frame.alpha_plane.is_some();
if probe.has_alpha != frame_has_alpha {
mismatches.push(format!(
"has_alpha: probe={} frame={}",
probe.has_alpha, frame_has_alpha
));
}
let cicp_fields = [
(
"color_primaries",
probe.color_primaries,
frame.color_primaries as u16,
),
(
"transfer_characteristics",
probe.transfer_characteristics,
frame.transfer_characteristics as u16,
),
(
"matrix_coefficients",
probe.matrix_coefficients,
frame.matrix_coeffs as u16,
),
(
"video_full_range",
probe.video_full_range as u16,
frame.full_range as u16,
),
];
for (field, probe_val, frame_val) in cicp_fields {
if probe_val == 2 && field != "video_full_range" {
continue;
}
if probe_val != frame_val {
mismatches.push(format!(
"cicp.{field}: probe={probe_val} frame={frame_val} (container vs SPS)"
));
}
}
(name, mismatches)
}
#[test]
#[ignore] fn probe_vs_decode_parity() {
let files = find_heic_files();
assert!(
!files.is_empty(),
"No HEIC test files found in {SEARCH_DIRS:?}"
);
let mut pass = 0u32;
let mut fail = 0u32;
let mut decode_errors = 0u32;
let mut failures = Vec::new();
for path in &files {
let (name, mismatches) = check_parity(path);
let has_decode_error = mismatches
.iter()
.any(|m| m.contains("decode_to_frame error"));
if mismatches.is_empty() {
pass += 1;
println!(" PASS: {name}");
} else if has_decode_error {
decode_errors += 1;
println!(" SKIP (decode error): {name}");
for m in &mismatches {
println!(" {m}");
}
} else {
fail += 1;
println!(" FAIL: {name}");
for m in &mismatches {
println!(" {m}");
}
failures.push((name, mismatches));
}
}
println!();
println!(
"Probe-vs-decode parity: {pass} pass, {fail} fail, {decode_errors} decode errors, {} total",
files.len()
);
let hard_failures: Vec<_> = failures
.iter()
.filter(|(_, mismatches)| {
mismatches
.iter()
.any(|m| !m.contains("cicp.") && !m.contains("container vs SPS"))
})
.collect();
assert!(
hard_failures.is_empty(),
"Hard parity failures (dimensions/bit_depth/chroma/alpha):\n{}",
hard_failures
.iter()
.map(|(name, ms)| format!(" {name}: {}", ms.join("; ")))
.collect::<Vec<_>>()
.join("\n")
);
}