use enough::Unstoppable;
use zenraw::{FileFormat, OutputMode, RawDecodeConfig, classify};
const SAMPLES_DIR: &str = "/mnt/v/input/raw-samples";
const FIVEK_DIR: &str = "/mnt/v/input/fivek/dng";
fn load_sample(name: &str) -> Option<Vec<u8>> {
std::fs::read(format!("{SAMPLES_DIR}/{name}")).ok()
}
fn load_first_fivek_dng() -> Option<(String, Vec<u8>)> {
let entries = std::fs::read_dir(FIVEK_DIR).ok()?;
for entry in entries.filter_map(|e| e.ok()).take(3) {
let path = entry.path();
if path
.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("dng"))
{
let name = path.file_name()?.to_str()?.to_string();
let data = std::fs::read(&path).ok()?;
return Some((name, data));
}
}
None
}
#[test]
fn dng_classify_agrees_with_probe_and_decode() {
let Some((name, data)) = load_first_fivek_dng() else {
eprintln!("Skipping: no FiveK DNG files found at {FIVEK_DIR}");
return;
};
let fmt = classify(&data);
assert_eq!(
fmt,
FileFormat::Dng,
"{name}: classify() returned {fmt}, expected Dng"
);
assert!(fmt.is_raw(), "{name}: classify().is_raw() should be true");
assert!(
zenraw::is_raw_file(&data),
"{name}: is_raw_file() should be true for DNG"
);
let info = zenraw::probe(&data, &Unstoppable)
.unwrap_or_else(|e| panic!("{name}: probe() failed: {e}"));
assert!(info.is_dng, "{name}: probe().is_dng should be true");
assert!(info.width > 0, "{name}: probe width should be > 0");
assert!(info.height > 0, "{name}: probe height should be > 0");
let config = RawDecodeConfig::new().with_output(OutputMode::Linear);
let output = zenraw::decode(&data, &config, &Unstoppable)
.unwrap_or_else(|e| panic!("{name}: decode() failed: {e}"));
let config_raw = RawDecodeConfig::new()
.with_output(OutputMode::Linear)
.with_crop(false)
.with_orientation(false);
let output_raw = zenraw::decode(&data, &config_raw, &Unstoppable)
.unwrap_or_else(|e| panic!("{name}: decode(no_crop,no_orient) failed: {e}"));
assert_eq!(
info.width, output_raw.info.width,
"{name}: probe vs decode(no_crop,no_orient) width mismatch"
);
assert_eq!(
info.height, output_raw.info.height,
"{name}: probe vs decode(no_crop,no_orient) height mismatch"
);
let raw_pixels = info.width as u64 * info.height as u64;
let out_pixels = output.info.width as u64 * output.info.height as u64;
assert!(
out_pixels <= raw_pixels,
"{name}: cropped output has more pixels than probe"
);
eprintln!(
"{name}: classify=Dng, is_dng=true, probe={}x{}, decode={}x{} — all agree",
info.width, info.height, output.info.width, output.info.height
);
}
#[test]
fn non_dng_raw_classify_agrees_with_probe_and_decode() {
let samples: &[&str] = &[
"nikon_d40.nef",
"canon_350d.cr2",
"sony_nex3.arw",
"olympus_c5050z.orf",
"panasonic_gf1.rw2",
"fuji_xt1.raf",
];
let mut tested = 0;
for &name in samples {
let Some(data) = load_sample(name) else {
eprintln!("{name}: skipping (file not found)");
continue;
};
let fmt = classify(&data);
assert_ne!(
fmt,
FileFormat::Dng,
"{name}: classify() should not return Dng for non-DNG"
);
assert_ne!(
fmt,
FileFormat::AppleDng,
"{name}: classify() should not return AppleDng"
);
eprintln!("{name}: classify() = {fmt}");
assert!(
zenraw::is_raw_file(&data),
"{name}: is_raw_file() should be true"
);
let info = match zenraw::probe(&data, &Unstoppable) {
Ok(info) => info,
Err(e) => {
eprintln!("{name}: probe() failed (backend limitation): {e}");
continue;
}
};
assert!(!info.is_dng, "{name}: probe().is_dng should be false");
let config = RawDecodeConfig::new().with_output(OutputMode::Linear);
let output = match zenraw::decode(&data, &config, &Unstoppable) {
Ok(out) => out,
Err(e) => {
eprintln!("{name}: decode() failed (backend limitation): {e}");
continue;
}
};
let config_raw = RawDecodeConfig::new()
.with_output(OutputMode::Linear)
.with_crop(false)
.with_orientation(false);
let output_raw = match zenraw::decode(&data, &config_raw, &Unstoppable) {
Ok(out) => out,
Err(e) => {
eprintln!("{name}: decode(no_crop,no_orient) failed: {e}");
continue;
}
};
assert_eq!(
info.width, output_raw.info.width,
"{name}: probe vs decode(no_crop,no_orient) width mismatch"
);
assert_eq!(
info.height, output_raw.info.height,
"{name}: probe vs decode(no_crop,no_orient) height mismatch"
);
let raw_pixels = info.width as u64 * info.height as u64;
let out_pixels = output.info.width as u64 * output.info.height as u64;
assert!(
out_pixels <= raw_pixels,
"{name}: cropped output has more pixels than probe"
);
eprintln!(
"{name}: classify={fmt}, is_dng=false, probe={}x{}, decode={}x{} — all agree",
info.width, info.height, output.info.width, output.info.height
);
tested += 1;
}
assert!(tested > 0, "no non-DNG RAW samples were available");
eprintln!("Tested {tested}/{} non-DNG formats", samples.len());
}
#[test]
fn non_raw_correctly_rejected() {
let jpeg = [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, b'J', b'F', b'I', b'F'];
assert!(
!zenraw::is_raw_file(&jpeg),
"JPEG should not be detected as RAW"
);
assert_eq!(classify(&jpeg), FileFormat::Jpeg);
assert!(!classify(&jpeg).is_raw());
let png = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
assert!(
!zenraw::is_raw_file(&png),
"PNG should not be detected as RAW"
);
assert_eq!(classify(&png), FileFormat::Unknown);
assert!(!zenraw::is_raw_file(&[]), "empty should not be RAW");
assert!(!zenraw::is_raw_file(&[0xDE, 0xAD, 0xBE, 0xEF]));
assert!(zenraw::probe(&jpeg, &Unstoppable).is_err());
assert!(zenraw::probe(&[], &Unstoppable).is_err());
eprintln!("Non-RAW rejection: all formats correctly rejected");
}
#[cfg(feature = "xmp")]
#[test]
fn xmp_extraction_consistent_across_methods() {
let Some((name, data)) = load_first_fivek_dng() else {
eprintln!("Skipping: no FiveK DNG files found at {FIVEK_DIR}");
return;
};
let xmp_raw = zenraw::xmp::extract_xmp(&data);
let xmp_meta = zenraw::xmp::read_xmp_metadata(&data);
match (&xmp_raw, &xmp_meta) {
(Some(xml), Some(meta)) => {
assert_eq!(
meta.raw_xml.as_deref(),
Some(xml.as_str()),
"{name}: raw_xml from read_xmp_metadata differs from extract_xmp"
);
eprintln!(
"{name}: XMP consistent — {} bytes, tiff:Make={:?}",
xml.len(),
meta.tiff_make
);
}
(None, None) => {
eprintln!("{name}: no XMP in this DNG (both methods agree)");
}
(Some(_), None) => {
panic!("{name}: extract_xmp found XMP but read_xmp_metadata returned None");
}
(None, Some(_)) => {
panic!("{name}: read_xmp_metadata found XMP but extract_xmp returned None");
}
}
}
#[cfg(feature = "xmp")]
#[test]
fn xmp_cross_format_consistency() {
let samples = [
"nikon_d40.nef",
"canon_350d.cr2",
"sony_nex3.arw",
"iphone12pro.dng",
];
let mut tested = 0;
for name in &samples {
let Some(data) = load_sample(name) else {
continue;
};
let raw = zenraw::xmp::extract_xmp(&data);
let meta = zenraw::xmp::read_xmp_metadata(&data);
match (&raw, &meta) {
(Some(xml), Some(m)) => {
assert_eq!(
m.raw_xml.as_deref(),
Some(xml.as_str()),
"{name}: XMP methods disagree"
);
eprintln!("{name}: has XMP ({} bytes)", xml.len());
}
(None, None) => {
eprintln!("{name}: no XMP (consistent)");
}
_ => {
panic!("{name}: XMP extraction methods disagree on presence");
}
}
tested += 1;
}
if tested == 0 {
eprintln!("Skipping: no sample files available");
} else {
eprintln!("XMP consistency verified for {tested} formats");
}
}