use fast_ssim2::compute_ssimulacra2;
use heic::{DecoderConfig, ImageInfo, PixelLayout};
use imgref::ImgVec;
use std::path::{Path, PathBuf};
use std::process::Command;
const DOCKER_IMAGE: &str = "ghcr.io/imazen/heif-ref:latest";
fn testdata() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("testdata")
}
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf()
}
fn docker_available() -> bool {
Command::new("docker")
.args(["image", "inspect", DOCKER_IMAGE])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
fn extract_reference(input_path: &Path) -> PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let stem = input_path
.file_stem()
.unwrap()
.to_string_lossy()
.to_string();
let id = COUNTER.fetch_add(1, Ordering::Relaxed);
let outdir = repo_root().join(format!("target/heif-ref/{stem}_{id}"));
let _ = std::fs::remove_dir_all(&outdir);
std::fs::create_dir_all(&outdir).expect("create outdir");
let root = repo_root();
let rel_input = input_path.strip_prefix(&root).unwrap_or(input_path);
let rel_out = outdir.strip_prefix(&root).unwrap_or(&outdir);
let status = Command::new("docker")
.args([
"run",
"--rm",
"-v",
&format!("{}:/repo", root.display()),
DOCKER_IMAGE,
"extract",
&format!("/repo/{}", rel_input.display()),
&format!("/repo/{}", rel_out.display()),
])
.status()
.expect("docker run failed");
assert!(
status.success(),
"heif-ref extract failed for {}",
input_path.display()
);
outdir
}
#[derive(Debug)]
#[allow(dead_code)]
struct RefInfo {
width: u32,
height: u32,
has_alpha: bool,
bit_depth: u8,
pixel_crc32: Option<u64>,
exif_present: bool,
exif_size: Option<usize>,
exif_crc32: Option<u64>,
xmp_present: bool,
xmp_size: Option<usize>,
xmp_crc32: Option<u64>,
icc_size: Option<usize>,
icc_crc32: Option<u64>,
thumbnail_present: bool,
thumbnail_width: Option<u32>,
thumbnail_height: Option<u32>,
aux_types: Vec<String>,
aux_images: Vec<AuxInfo>,
color_profile_type: String,
color_primaries: Option<u16>,
transfer_characteristics: Option<u16>,
matrix_coefficients: Option<u16>,
full_range: Option<bool>,
}
#[derive(Debug)]
#[allow(dead_code)]
struct AuxInfo {
aux_type: String,
width: u32,
height: u32,
pixel_crc32: Option<u64>,
}
fn json_str<'a>(json: &'a str, key: &str) -> Option<&'a str> {
let pattern = format!("\"{key}\": \"");
let start = json.find(&pattern)? + pattern.len();
let end = start + json[start..].find('"')?;
Some(&json[start..end])
}
fn json_int(json: &str, key: &str) -> Option<i64> {
let pattern = format!("\"{key}\": ");
let start = json.find(&pattern)? + pattern.len();
let end = start
+ json[start..]
.find(|c: char| !c.is_ascii_digit() && c != '-')
.unwrap_or(json[start..].len());
json[start..end].parse().ok()
}
fn json_bool(json: &str, key: &str) -> Option<bool> {
let pattern = format!("\"{key}\": ");
let start = json.find(&pattern)? + pattern.len();
if json[start..].starts_with("true") {
Some(true)
} else if json[start..].starts_with("false") {
Some(false)
} else {
None
}
}
#[allow(dead_code)]
fn json_float(json: &str, key: &str) -> Option<f64> {
let pattern = format!("\"{key}\": ");
let start = json.find(&pattern)? + pattern.len();
let end = start
+ json[start..]
.find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-')
.unwrap_or(json[start..].len());
json[start..end].parse().ok()
}
fn parse_ref_info(outdir: &Path) -> RefInfo {
let json = std::fs::read_to_string(outdir.join("info.json")).expect("read info.json");
let primary_start = json.find("\"primary\":").expect("no primary in JSON");
let primary = &json[primary_start..];
let cp_start = primary
.find("\"color_profile\":")
.map(|i| &primary[i..])
.unwrap_or("");
let color_profile_type = json_str(cp_start, "type").unwrap_or("none").to_string();
let mut aux_types = Vec::new();
let mut aux_images = Vec::new();
if let Some(aux_start) = primary.find("\"auxiliary_images\":") {
let aux_section = &primary[aux_start..];
let mut search_from = 0;
while let Some(obj_start) = aux_section[search_from..].find("\"type\": \"") {
let obj_region_start = search_from + obj_start;
let obj_section = &aux_section[obj_region_start..];
if let Some(t) = json_str(obj_section, "type") {
aux_types.push(t.to_string());
let w = json_int(obj_section, "width").unwrap_or(0) as u32;
let h = json_int(obj_section, "height").unwrap_or(0) as u32;
let dw = json_int(obj_section, "decoded_width")
.map(|v| v as u32)
.unwrap_or(w);
let dh = json_int(obj_section, "decoded_height")
.map(|v| v as u32)
.unwrap_or(h);
let crc = json_int(obj_section, "pixel_crc32").map(|v| v as u64);
aux_images.push(AuxInfo {
aux_type: t.to_string(),
width: dw,
height: dh,
pixel_crc32: crc,
});
}
search_from = obj_region_start + 10;
}
}
let exif_section = primary
.find("\"exif\":")
.map(|i| &primary[i..])
.unwrap_or("");
let exif_present = json_bool(exif_section, "present").unwrap_or(false);
let xmp_section = primary
.find("\"xmp\":")
.map(|i| &primary[i..])
.unwrap_or("");
let xmp_present = json_bool(xmp_section, "present").unwrap_or(false);
let thumb_section = primary
.find("\"thumbnail\":")
.map(|i| &primary[i..])
.unwrap_or("");
let thumbnail_present = json_bool(thumb_section, "present").unwrap_or(false);
RefInfo {
width: json_int(primary, "width").unwrap_or(0) as u32,
height: json_int(primary, "height").unwrap_or(0) as u32,
has_alpha: json_bool(primary, "has_alpha").unwrap_or(false),
bit_depth: json_int(primary, "bit_depth").unwrap_or(0) as u8,
pixel_crc32: json_int(primary, "pixel_crc32").map(|v| v as u64),
exif_present,
exif_size: json_int(exif_section, "size").map(|v| v as usize),
exif_crc32: json_int(exif_section, "crc32").map(|v| v as u64),
xmp_present,
xmp_size: json_int(xmp_section, "size").map(|v| v as usize),
xmp_crc32: json_int(xmp_section, "crc32").map(|v| v as u64),
icc_size: json_int(cp_start, "icc_size").map(|v| v as usize),
icc_crc32: json_int(cp_start, "icc_crc32").map(|v| v as u64),
thumbnail_present,
thumbnail_width: if thumbnail_present {
json_int(thumb_section, "width").map(|v| v as u32)
} else {
None
},
thumbnail_height: if thumbnail_present {
json_int(thumb_section, "height").map(|v| v as u32)
} else {
None
},
aux_types,
aux_images,
color_profile_type,
color_primaries: json_int(cp_start, "color_primaries").map(|v| v as u16),
transfer_characteristics: json_int(cp_start, "transfer_characteristics").map(|v| v as u16),
matrix_coefficients: json_int(cp_start, "matrix_coefficients").map(|v| v as u16),
full_range: json_bool(cp_start, "full_range"),
}
}
fn rgb_to_imgvec(rgb: &[u8], width: u32, height: u32) -> ImgVec<[u8; 3]> {
let pixels: Vec<[u8; 3]> = rgb.chunks_exact(3).map(|c| [c[0], c[1], c[2]]).collect();
ImgVec::new(pixels, width as usize, height as usize)
}
fn pixel_diff_stats(ours: &[u8], reference: &[u8]) -> (f64, u32, f64) {
let mut total_diff: u64 = 0;
let mut max_diff: u32 = 0;
let mut exact = 0u64;
let n = ours.len().min(reference.len());
for i in 0..n {
let diff = (ours[i] as i32 - reference[i] as i32).unsigned_abs();
total_diff += diff as u64;
if diff > max_diff {
max_diff = diff;
}
if diff == 0 {
exact += 1;
}
}
let avg = total_diff as f64 / n as f64;
let exact_pct = exact as f64 / n as f64 * 100.0;
(avg, max_diff, exact_pct)
}
fn skip_if_no_docker() -> bool {
if !docker_available() {
eprintln!("SKIP: Docker not available or heif-ref image not built");
return true;
}
false
}
#[test]
fn compare_example_heic_metadata() {
if skip_if_no_docker() {
return;
}
let input = testdata().join("libheif-examples/example.heic");
let ref_dir = extract_reference(&input);
let ref_info = parse_ref_info(&ref_dir);
let data = std::fs::read(&input).unwrap();
let our_info = ImageInfo::from_bytes(&data).unwrap();
assert_eq!(our_info.width, ref_info.width, "width mismatch");
assert_eq!(our_info.height, ref_info.height, "height mismatch");
assert_eq!(our_info.has_alpha, ref_info.has_alpha, "alpha mismatch");
assert_eq!(our_info.bit_depth, ref_info.bit_depth, "bit_depth mismatch");
assert_eq!(
our_info.has_exif, ref_info.exif_present,
"EXIF presence mismatch"
);
assert_eq!(
our_info.has_xmp, ref_info.xmp_present,
"XMP presence mismatch"
);
assert_eq!(
our_info.has_thumbnail, ref_info.thumbnail_present,
"thumbnail presence mismatch"
);
println!("example.heic metadata: MATCH");
println!(
" {}x{}, alpha={}, depth={}, exif={}, xmp={}, thumb={}",
ref_info.width,
ref_info.height,
ref_info.has_alpha,
ref_info.bit_depth,
ref_info.exif_present,
ref_info.xmp_present,
ref_info.thumbnail_present
);
}
#[test]
fn compare_example_heic_pixels() {
if skip_if_no_docker() {
return;
}
let input = testdata().join("libheif-examples/example.heic");
let ref_dir = extract_reference(&input);
let ref_info = parse_ref_info(&ref_dir);
let data = std::fs::read(&input).unwrap();
let ours = DecoderConfig::new()
.decode(&data, PixelLayout::Rgb8)
.unwrap();
assert_eq!(ours.width, ref_info.width);
assert_eq!(ours.height, ref_info.height);
let ref_pixels = std::fs::read(ref_dir.join("primary.bin")).unwrap();
assert_eq!(
ref_pixels.len(),
ours.data.len(),
"pixel buffer size mismatch"
);
let ref_img = rgb_to_imgvec(&ref_pixels, ref_info.width, ref_info.height);
let our_img = rgb_to_imgvec(&ours.data, ours.width, ours.height);
let ssim2 = compute_ssimulacra2(ref_img.as_ref(), our_img.as_ref()).unwrap();
let (avg_diff, max_diff, exact_pct) = pixel_diff_stats(&ours.data, &ref_pixels);
println!("example.heic pixels vs libheif {}:", ref_info.bit_depth);
println!(" SSIM2: {ssim2:.2}");
println!(" Avg diff: {avg_diff:.2}, Max diff: {max_diff}, Exact: {exact_pct:.1}%");
assert!(ssim2 > 50.0, "SSIM2 {ssim2:.2} too low (expected > 50)");
}
#[test]
fn compare_hdr_sample_metadata() {
if skip_if_no_docker() {
return;
}
let input = testdata().join("apple-hdr/hdr-sample.heic");
let ref_dir = extract_reference(&input);
let ref_info = parse_ref_info(&ref_dir);
let data = std::fs::read(&input).unwrap();
let our_info = ImageInfo::from_bytes(&data).unwrap();
assert_eq!(our_info.width, ref_info.width, "width mismatch");
assert_eq!(our_info.height, ref_info.height, "height mismatch");
assert_eq!(our_info.bit_depth, ref_info.bit_depth, "bit_depth mismatch");
assert_eq!(our_info.has_exif, ref_info.exif_present, "EXIF mismatch");
assert_eq!(our_info.has_xmp, ref_info.xmp_present, "XMP mismatch");
assert_eq!(
our_info.has_gain_map,
!ref_info.aux_types.is_empty()
&& ref_info.aux_types.iter().any(|t| t.contains("hdrgainmap")),
"gain map presence mismatch"
);
assert_eq!(
our_info.has_icc_profile,
ref_info.color_profile_type == "prof" || ref_info.color_profile_type == "rICC",
"ICC presence mismatch"
);
println!("hdr-sample.heic metadata: MATCH");
println!(
" {}x{}, exif={}, xmp={}, icc={}, gain_map={}",
ref_info.width,
ref_info.height,
ref_info.exif_present,
ref_info.xmp_present,
ref_info.color_profile_type,
!ref_info.aux_types.is_empty()
);
for aux in &ref_info.aux_images {
println!(" aux: {} {}x{}", aux.aux_type, aux.width, aux.height);
}
}
#[test]
fn compare_hdr_sample_pixels() {
if skip_if_no_docker() {
return;
}
let input = testdata().join("apple-hdr/hdr-sample.heic");
let ref_dir = extract_reference(&input);
let ref_info = parse_ref_info(&ref_dir);
let data = std::fs::read(&input).unwrap();
let ours = DecoderConfig::new()
.decode(&data, PixelLayout::Rgb8)
.unwrap();
assert_eq!(ours.width, ref_info.width);
assert_eq!(ours.height, ref_info.height);
let ref_pixels = std::fs::read(ref_dir.join("primary.bin")).unwrap();
assert_eq!(ref_pixels.len(), ours.data.len());
let ref_img = rgb_to_imgvec(&ref_pixels, ref_info.width, ref_info.height);
let our_img = rgb_to_imgvec(&ours.data, ours.width, ours.height);
let ssim2 = compute_ssimulacra2(ref_img.as_ref(), our_img.as_ref()).unwrap();
let (avg_diff, max_diff, exact_pct) = pixel_diff_stats(&ours.data, &ref_pixels);
println!("hdr-sample.heic pixels vs libheif 1.19:");
println!(" SSIM2: {ssim2:.2}");
println!(" Avg diff: {avg_diff:.2}, Max diff: {max_diff}, Exact: {exact_pct:.1}%");
assert!(ssim2 > 50.0, "SSIM2 {ssim2:.2} too low");
}
#[test]
fn compare_hdr_sample_gain_map() {
if skip_if_no_docker() {
return;
}
let input = testdata().join("apple-hdr/hdr-sample.heic");
let ref_dir = extract_reference(&input);
let ref_info = parse_ref_info(&ref_dir);
let gm_aux = ref_info
.aux_images
.iter()
.find(|a| a.aux_type.contains("hdrgainmap"));
let gm_aux = match gm_aux {
Some(a) => a,
None => {
eprintln!("SKIP: libheif did not extract gain map auxiliary");
return;
}
};
let data = std::fs::read(&input).unwrap();
let our_gm = DecoderConfig::new()
.decode_gain_map(&data)
.expect("our gain map decode failed");
assert_eq!(
our_gm.width, gm_aux.width,
"gain map width: ours={} ref={}",
our_gm.width, gm_aux.width
);
assert_eq!(
our_gm.height, gm_aux.height,
"gain map height: ours={} ref={}",
our_gm.height, gm_aux.height
);
let ref_gm_path = std::fs::read_dir(&ref_dir)
.unwrap()
.filter_map(|e| e.ok())
.find(|e| {
let name = e.file_name().to_string_lossy().to_string();
name.starts_with("aux_") && name.ends_with(".bin")
})
.map(|e| e.path());
if let Some(ref_gm_path) = ref_gm_path {
let ref_gm_pixels = std::fs::read(&ref_gm_path).unwrap();
let expected_size = (gm_aux.width * gm_aux.height) as usize;
assert_eq!(
ref_gm_pixels.len(),
expected_size,
"ref gain map size mismatch"
);
assert_eq!(
our_gm.data.len(),
expected_size,
"our gain map size mismatch"
);
let mut total_diff: u64 = 0;
let mut max_diff: u32 = 0;
let mut exact = 0u64;
for (i, &ref_px) in ref_gm_pixels.iter().enumerate().take(expected_size) {
let diff = (our_gm.data[i] as i32 - ref_px as i32).unsigned_abs();
total_diff += diff as u64;
if diff > max_diff {
max_diff = diff;
}
if diff == 0 {
exact += 1;
}
}
let avg = total_diff as f64 / expected_size as f64;
let exact_pct = exact as f64 / expected_size as f64 * 100.0;
println!(
"hdr-sample.heic gain map ({}x{}):",
gm_aux.width, gm_aux.height
);
println!(" Avg diff: {avg:.2}, Max diff: {max_diff}, Exact: {exact_pct:.1}%");
}
}
#[test]
fn compare_hdr_sample_exif() {
if skip_if_no_docker() {
return;
}
let input = testdata().join("apple-hdr/hdr-sample.heic");
let ref_dir = extract_reference(&input);
let ref_info = parse_ref_info(&ref_dir);
if !ref_info.exif_present {
return;
}
let data = std::fs::read(&input).unwrap();
let our_exif = DecoderConfig::new()
.extract_exif(&data)
.expect("extract_exif failed");
let our_exif = our_exif.expect("should have EXIF");
let ref_exif = std::fs::read(ref_dir.join("exif.bin")).unwrap();
let ref_exif_body = if ref_exif.len() > 10 && &ref_exif[4..8] == b"Exif" {
&ref_exif[10..]
} else if ref_exif.len() > 4
&& (ref_exif[4..].starts_with(b"II") || ref_exif[4..].starts_with(b"MM"))
{
&ref_exif[4..]
} else {
&ref_exif
};
assert_eq!(
our_exif.len(),
ref_exif_body.len(),
"EXIF size: ours={} ref={}",
our_exif.len(),
ref_exif_body.len()
);
assert_eq!(our_exif.as_ref(), ref_exif_body, "EXIF content mismatch");
println!("hdr-sample.heic EXIF: MATCH ({} bytes)", our_exif.len());
}
#[test]
fn compare_hdr_sample_xmp() {
if skip_if_no_docker() {
return;
}
let input = testdata().join("apple-hdr/hdr-sample.heic");
let ref_dir = extract_reference(&input);
let ref_info = parse_ref_info(&ref_dir);
if !ref_info.xmp_present {
return;
}
let data = std::fs::read(&input).unwrap();
let our_xmp = DecoderConfig::new()
.extract_xmp(&data)
.expect("extract_xmp failed");
let our_xmp = our_xmp.expect("should have XMP");
let ref_xmp = std::fs::read(ref_dir.join("xmp.xml")).unwrap();
fn strip_trailing_nulls(data: &[u8]) -> &[u8] {
let mut end = data.len();
while end > 0 && data[end - 1] == 0 {
end -= 1;
}
&data[..end]
}
let our_xmp_trimmed = strip_trailing_nulls(&our_xmp);
let ref_xmp_trimmed = strip_trailing_nulls(&ref_xmp);
assert_eq!(
our_xmp_trimmed.len(),
ref_xmp_trimmed.len(),
"XMP size: ours={} ref={}",
our_xmp_trimmed.len(),
ref_xmp_trimmed.len()
);
assert_eq!(our_xmp_trimmed, ref_xmp_trimmed, "XMP content mismatch");
println!(
"hdr-sample.heic XMP: MATCH ({} bytes)",
our_xmp_trimmed.len()
);
}
#[test]
fn compare_synthetic_files() {
if skip_if_no_docker() {
return;
}
for name in &[
"synthetic/synth_8bit_q10.heic",
"synthetic/synth_8bit_q50.heic",
"synthetic/synth_8bit_q95.heic",
] {
let input = testdata().join(name);
let ref_dir = extract_reference(&input);
let ref_info = parse_ref_info(&ref_dir);
let data = std::fs::read(&input).unwrap();
let ours = DecoderConfig::new()
.decode(&data, PixelLayout::Rgb8)
.unwrap();
assert_eq!(ours.width, ref_info.width, "{name}: width");
assert_eq!(ours.height, ref_info.height, "{name}: height");
let ref_pixels = std::fs::read(ref_dir.join("primary.bin")).unwrap();
if ref_pixels.len() != ours.data.len() {
eprintln!(
"{name}: pixel buffer size mismatch (ref={}, ours={})",
ref_pixels.len(),
ours.data.len()
);
continue;
}
let ref_img = rgb_to_imgvec(&ref_pixels, ref_info.width, ref_info.height);
let our_img = rgb_to_imgvec(&ours.data, ours.width, ours.height);
let ssim2 = compute_ssimulacra2(ref_img.as_ref(), our_img.as_ref()).unwrap();
let (avg_diff, max_diff, exact_pct) = pixel_diff_stats(&ours.data, &ref_pixels);
println!(
"{name}: SSIM2={ssim2:.2}, avg_diff={avg_diff:.2}, max_diff={max_diff}, exact={exact_pct:.1}%"
);
assert!(ssim2 > 40.0, "{name}: SSIM2 {ssim2:.2} too low");
}
}