use heic::DecoderConfig;
fn heic_base_dir() -> String {
std::env::var("HEIC_TEST_DIR").unwrap_or_else(|_| "/home/lilith/work/heic".into())
}
fn example_heic() -> String {
format!("{}/libheif/examples/example.heic", heic_base_dir())
}
fn iphone_heic() -> String {
format!(
"{}/test-images/classic-car-iphone12pro.heic",
heic_base_dir()
)
}
#[test]
fn test_get_info() {
let data = std::fs::read(example_heic()).expect("Failed to read test file");
let info = heic::ImageInfo::from_bytes(&data).expect("Failed to get info");
println!("Decoded info: {}x{}", info.width, info.height);
assert_eq!(info.width, 1280, "Expected width 1280");
assert_eq!(info.height, 854, "Expected height 854 (cropped)");
}
#[test]
#[ignore] fn test_decode() {
let data = std::fs::read(example_heic()).expect("Failed to read test file");
let decoder = DecoderConfig::new();
let image = decoder
.decode(&data, heic::PixelLayout::Rgb8)
.expect("Failed to decode");
assert_eq!(image.width, 1280, "Expected width 1280");
assert_eq!(image.height, 854, "Expected height 854 (cropped)");
let expected_size = (image.width * image.height * 3) as usize;
assert_eq!(image.data.len(), expected_size, "Unexpected data size");
let non_zero = image.data.iter().any(|&b| b != 0);
assert!(non_zero, "Image data is all zeros");
let min_val = *image.data.iter().min().unwrap();
let max_val = *image.data.iter().max().unwrap();
let sum: u64 = image.data.iter().map(|&b| b as u64).sum();
let avg = sum / image.data.len() as u64;
println!("Pixel stats: min={}, max={}, avg={}", min_val, max_val, avg);
println!("\n=== Our first 8x8 RGB block ===");
for y in 0..8 {
for x in 0..8 {
let idx = (y * image.width as usize + x) * 3;
let r = image.data[idx];
let g = image.data[idx + 1];
let b = image.data[idx + 2];
print!("({:3},{:3},{:3}) ", r, g, b);
}
println!();
}
let ppm_path = "/tmp/decoded_heic.ppm";
let mut ppm = String::new();
ppm.push_str(&format!("P6\n{} {}\n255\n", image.width, image.height));
let mut file = std::fs::File::create(ppm_path).expect("Failed to create PPM");
use std::io::Write;
file.write_all(ppm.as_bytes())
.expect("Failed to write PPM header");
file.write_all(&image.data)
.expect("Failed to write PPM data");
println!("Wrote decoded image to: {}", ppm_path);
}
#[test]
#[ignore]
fn test_raw_yuv_values() {
let data = std::fs::read(example_heic()).expect("Failed to read test file");
let decoder = DecoderConfig::new();
let frame = decoder.decode_to_frame(&data).expect("Failed to decode");
let mid_x = frame.width / 2;
let mid_y = frame.height / 2;
let mut quadrant_sums = [0u64; 4];
let mut quadrant_counts = [0u64; 4];
for y in 0..frame.height {
for x in 0..frame.width {
let idx = (y * frame.width + x) as usize;
let val = frame.y_plane[idx] as u64;
let q = if x < mid_x {
if y < mid_y { 0 } else { 2 }
} else if y < mid_y {
1
} else {
3
};
quadrant_sums[q] += val;
quadrant_counts[q] += 1;
}
}
println!("\nY quadrant averages:");
println!(" Top-Left: {}", quadrant_sums[0] / quadrant_counts[0]);
println!(" Top-Right: {}", quadrant_sums[1] / quadrant_counts[1]);
println!(" Bottom-Left: {}", quadrant_sums[2] / quadrant_counts[2]);
println!(" Bottom-Right: {}", quadrant_sums[3] / quadrant_counts[3]);
println!("\nY values at x=64 for different rows:");
for &y in &[0, 32, 64, 128, 256, 400] {
if y < frame.height {
let idx = (y * frame.width + 64) as usize;
let vals: Vec<u16> = (0..8).map(|dx| frame.y_plane[idx + dx]).collect();
println!(" y={:3}: {:?}", y, vals);
}
}
println!("\nY values at y=64 for different columns:");
for &x in &[0, 64, 96, 120, 127, 128, 192, 256, 400, 640] {
if x < frame.width {
let idx = (64 * frame.width + x) as usize;
let vals: Vec<u16> = (0..8).map(|dx| frame.y_plane[idx + dx]).collect();
println!(" x={:3}: {:?}", x, vals);
}
}
println!("\nY values at y=63 (top border row for CTU row 1):");
for &x in &[96, 104, 112, 120, 127] {
if x < frame.width {
let idx = (63 * frame.width + x) as usize;
let vals: Vec<u16> = (0..8).map(|dx| frame.y_plane[idx + dx]).collect();
println!(" x={:3}: {:?}", x, vals);
}
}
println!(
"Frame: {}x{}, bit_depth={}",
frame.width, frame.height, frame.bit_depth
);
println!("Y plane: {} samples", frame.y_plane.len());
println!("Cb plane: {} samples", frame.cb_plane.len());
println!("Cr plane: {} samples", frame.cr_plane.len());
let y_min = frame.y_plane.iter().min().unwrap_or(&0);
let y_max = frame.y_plane.iter().max().unwrap_or(&0);
let y_sum: u64 = frame.y_plane.iter().map(|&v| v as u64).sum();
let y_avg = y_sum / frame.y_plane.len().max(1) as u64;
let mut hist = [0usize; 8];
for &v in &frame.y_plane {
hist[(v as usize / 32).min(7)] += 1;
}
println!("\nY plane: min={}, max={}, avg={}", y_min, y_max, y_avg);
println!(" Histogram (32-bin):");
for (i, count) in hist.iter().enumerate() {
let pct = *count as f64 / frame.y_plane.len() as f64 * 100.0;
println!(
" {:3}-{:3}: {:7} ({:5.1}%)",
i * 32,
(i + 1) * 32 - 1,
count,
pct
);
}
let cb_min = frame.cb_plane.iter().min().unwrap_or(&0);
let cb_max = frame.cb_plane.iter().max().unwrap_or(&0);
let cb_sum: u64 = frame.cb_plane.iter().map(|&v| v as u64).sum();
let cb_avg = cb_sum / frame.cb_plane.len().max(1) as u64;
println!("Cb plane: min={}, max={}, avg={}", cb_min, cb_max, cb_avg);
let cr_min = frame.cr_plane.iter().min().unwrap_or(&0);
let cr_max = frame.cr_plane.iter().max().unwrap_or(&0);
let cr_sum: u64 = frame.cr_plane.iter().map(|&v| v as u64).sum();
let cr_avg = cr_sum / frame.cr_plane.len().max(1) as u64;
println!("Cr plane: min={}, max={}, avg={}", cr_min, cr_max, cr_avg);
println!("\n=== Raw YCbCr Values (first 8x8 Y block) ===");
for y in 0..8 {
let mut row = Vec::new();
for x in 0..8 {
let idx = (y * frame.width + x) as usize;
row.push(format!("{:3}", frame.y_plane[idx]));
}
println!(" Y: {}", row.join(" "));
}
println!("\n=== Raw Cb/Cr (first 4x4 chroma block) ===");
let c_stride = frame.width.div_ceil(2) as usize;
for cy in 0..4 {
let mut cb_row = Vec::new();
let mut cr_row = Vec::new();
for cx in 0..4 {
let idx = cy * c_stride + cx;
cb_row.push(format!("{:3}", frame.cb_plane[idx]));
cr_row.push(format!("{:3}", frame.cr_plane[idx]));
}
println!(" Cb: {} | Cr: {}", cb_row.join(" "), cr_row.join(" "));
}
println!("\n=== Chroma averages by CTU row ===");
let c_height = frame.height.div_ceil(2) as usize;
let ctu_chroma_size = 32usize;
let num_ctu_rows = c_height.div_ceil(ctu_chroma_size);
for ctu_row in 0..num_ctu_rows {
let start_y = ctu_row * ctu_chroma_size;
let end_y = ((ctu_row + 1) * ctu_chroma_size).min(c_height);
let mut cb_sum = 0u64;
let mut cr_sum = 0u64;
let mut count = 0u64;
for cy in start_y..end_y {
for cx in 0..c_stride {
let idx = cy * c_stride + cx;
cb_sum += frame.cb_plane[idx] as u64;
cr_sum += frame.cr_plane[idx] as u64;
count += 1;
}
}
if let (Some(cb_avg), Some(cr_avg)) = (cb_sum.checked_div(count), cr_sum.checked_div(count))
{
println!(
" CTU row {:2}: Cb avg={:3}, Cr avg={:3}",
ctu_row, cb_avg, cr_avg
);
}
}
println!("\n=== Chroma averages by CTU column (first row) ===");
let c_width = c_stride;
let num_ctu_cols = c_width.div_ceil(ctu_chroma_size);
for ctu_col in 0..num_ctu_cols {
let start_x = ctu_col * ctu_chroma_size;
let end_x = ((ctu_col + 1) * ctu_chroma_size).min(c_width);
let mut cb_sum = 0u64;
let mut cr_sum = 0u64;
let mut count = 0u64;
for cy in 0..ctu_chroma_size.min(c_height) {
for cx in start_x..end_x {
let idx = cy * c_stride + cx;
cb_sum += frame.cb_plane[idx] as u64;
cr_sum += frame.cr_plane[idx] as u64;
count += 1;
}
}
if let (Some(cb_avg), Some(cr_avg)) = (cb_sum.checked_div(count), cr_sum.checked_div(count))
{
println!(
" CTU col {:2}: Cb avg={:3}, Cr avg={:3}",
ctu_col, cb_avg, cr_avg
);
}
}
println!("\n=== Chroma at CTU boundary (col 0 -> 1) ===");
println!("Chroma values at x=28..35 (boundary at x=32), y=0..3:");
for cy in 0..4 {
let mut cb_row = Vec::new();
let mut cr_row = Vec::new();
for cx in 28..36 {
let idx = cy * c_stride + cx;
cb_row.push(format!("{:3}", frame.cb_plane[idx]));
cr_row.push(format!("{:3}", frame.cr_plane[idx]));
}
println!(
" y={}: Cb=[{}] Cr=[{}]",
cy,
cb_row.join(", "),
cr_row.join(", ")
);
}
println!(" (x=32 is start of CTU col 1)");
println!("\nChroma at right edge of CTU col 0 (x=31), all y:");
let mut cb_at_31 = vec![];
let mut cr_at_31 = vec![];
for cy in 0..32.min(c_height) {
let idx = cy * c_stride + 31;
cb_at_31.push(frame.cb_plane[idx]);
cr_at_31.push(frame.cr_plane[idx]);
}
let cb_avg: u64 = cb_at_31.iter().map(|&v| v as u64).sum::<u64>() / cb_at_31.len() as u64;
let cr_avg: u64 = cr_at_31.iter().map(|&v| v as u64).sum::<u64>() / cr_at_31.len() as u64;
println!(" x=31: Cb avg={}, Cr avg={}", cb_avg, cr_avg);
println!(" first 8 Cb: {:?}", &cb_at_31[..8.min(cb_at_31.len())]);
println!(" first 8 Cr: {:?}", &cr_at_31[..8.min(cr_at_31.len())]);
}
#[test]
fn test_extract_exif() {
let data = std::fs::read(iphone_heic()).expect("read");
let decoder = DecoderConfig::new();
let exif = decoder.extract_exif(&data).expect("extract_exif");
let exif = exif.expect("should have EXIF data");
assert!(exif.len() > 8, "EXIF data too short: {} bytes", exif.len());
assert!(
&exif[..2] == b"II" || &exif[..2] == b"MM",
"EXIF data should start with TIFF byte order mark, got {:02x?}",
&exif[..2]
);
println!(
"EXIF: {} bytes, byte order: {}",
exif.len(),
if exif[0] == b'I' {
"little-endian"
} else {
"big-endian"
}
);
}
#[test]
fn test_extract_exif_none() {
let data = std::fs::read(example_heic()).expect("read");
let decoder = DecoderConfig::new();
let exif = decoder.extract_exif(&data).expect("extract_exif");
assert!(exif.is_none(), "example.heic should not have EXIF");
}
#[test]
fn test_image_info_no_exif() {
let data = std::fs::read(example_heic()).expect("read");
let info = heic::ImageInfo::from_bytes(&data).expect("probe");
assert!(!info.has_exif, "example.heic should not have EXIF");
assert!(!info.has_xmp, "example.heic should not have XMP");
println!(
"ImageInfo: {}x{}, has_exif={}, has_xmp={}",
info.width, info.height, info.has_exif, info.has_xmp
);
}
#[test]
fn test_image_info_grid_with_exif() {
let data = std::fs::read(iphone_heic()).expect("read");
let info = heic::ImageInfo::from_bytes(&data).expect("probe grid image");
assert!(info.has_exif, "iPhone HEIC should have EXIF");
assert!(info.has_xmp, "iPhone HEIC should have XMP");
assert_eq!(info.width, 3024);
assert_eq!(info.height, 4032);
println!(
"Grid ImageInfo: {}x{}, bit_depth={}, has_exif={}, has_xmp={}",
info.width, info.height, info.bit_depth, info.has_exif, info.has_xmp
);
}
#[test]
fn test_extract_xmp() {
let data = std::fs::read(iphone_heic()).expect("read");
let decoder = DecoderConfig::new();
let xmp = decoder.extract_xmp(&data).expect("extract_xmp");
if let Some(xmp_data) = xmp {
let start =
std::str::from_utf8(&xmp_data[..xmp_data.len().min(100)]).unwrap_or("(non-utf8)");
println!("XMP: {} bytes, starts with: {:?}", xmp_data.len(), start);
} else {
println!("No XMP found (expected for some files)");
}
}
#[test]
fn test_decode_thumbnail() {
let data = std::fs::read(example_heic()).expect("read");
let decoder = DecoderConfig::new();
let thumb = decoder
.decode_thumbnail(&data, heic::PixelLayout::Rgb8)
.expect("decode_thumbnail");
let thumb = thumb.expect("example.heic should have a thumbnail");
assert_eq!(thumb.width, 320);
assert_eq!(thumb.height, 212);
assert_eq!(thumb.data.len(), 320 * 212 * 3);
println!(
"Thumbnail: {}x{}, {} bytes",
thumb.width,
thumb.height,
thumb.data.len()
);
}
#[test]
fn test_image_info_has_thumbnail() {
let data = std::fs::read(example_heic()).expect("read");
let info = heic::ImageInfo::from_bytes(&data).expect("probe");
assert!(
info.has_thumbnail,
"example.heic should report has_thumbnail=true"
);
}
#[test]
fn test_decode_thumbnail_none() {
let nokia_path = format!("{}/test-images/nokia/C001.heic", heic_base_dir());
if let Ok(data) = std::fs::read(nokia_path) {
let decoder = DecoderConfig::new();
let thumb = decoder
.decode_thumbnail(&data, heic::PixelLayout::Rgb8)
.expect("decode_thumbnail");
if thumb.is_none() {
println!("C001.heic has no thumbnail (expected)");
} else {
println!("C001.heic has a thumbnail (unexpected but OK)");
}
}
}
#[test]
fn test_image_info_matches_decoded_dimensions() {
let data = std::fs::read(iphone_heic()).expect("read");
let info = heic::ImageInfo::from_bytes(&data).expect("probe");
let decoder = DecoderConfig::new();
let decoded = decoder
.decode(&data, heic::PixelLayout::Rgb8)
.expect("decode");
assert_eq!(
info.width, decoded.width,
"ImageInfo width {} != decoded width {}",
info.width, decoded.width
);
assert_eq!(
info.height, decoded.height,
"ImageInfo height {} != decoded height {}",
info.height, decoded.height
);
}