filmcraft/crates/media/src/still/hdr/tests.rs

135 lines
5.3 KiB
Rust

use super::*;
fn file(res: &str, body: &[u8]) -> Vec<u8> {
let mut v = format!("#?RADIANCE\n# test\nFORMAT=32-bit_rle_rgbe\nEXPOSURE=1.0\n\n{res}\n").into_bytes();
v.extend_from_slice(body);
v
}
fn px(f: &VideoFrame) -> Vec<f32> {
match &f.data {
filmcraft_frame::PixelData::RgbaF32(p) => p.to_vec(),
_ => panic!("not float"),
}
}
/// New-style RLE of one scan line, as Radiance's own writer does: per channel, runs of 3+ equal bytes.
fn rle_line(line: &[[u8; 4]]) -> Vec<u8> {
let n = line.len();
let mut out = vec![2, 2, (n >> 8) as u8, n as u8];
for c in 0..4 {
let ch: Vec<u8> = line.iter().map(|p| p[c]).collect();
let mut i = 0;
while i < n {
let run = ch[i..].iter().take(127).take_while(|&&b| b == ch[i]).count();
if run >= 3 {
out.extend([128 + run as u8, ch[i]]);
i += run;
} else {
let mut j = i + 1;
while j < n && j - i < 128 && !(j + 2 < n && ch[j] == ch[j + 1] && ch[j] == ch[j + 2]) {
j += 1;
}
out.push((j - i) as u8);
out.extend_from_slice(&ch[i..j]);
i = j;
}
}
}
out
}
#[test]
fn rgbe_values() {
assert_eq!(rgbe([0, 0, 0, 0]), [0.0; 3]);
assert_eq!(rgbe([128, 128, 128, 129]), [1.0; 3]);
assert_eq!(rgbe([128, 64, 0, 136]), [128.0, 64.0, 0.0]);
assert_eq!(rgbe([128, 128, 128, 128])[0], 0.5);
assert!(rgbe([255, 255, 255, 255])[0].is_finite());
}
#[test]
fn flat_top_to_bottom() {
let body = [[10u8, 20, 30, 136], [1, 2, 3, 136], [4, 5, 6, 137], [7, 8, 9, 135]].concat();
let f = decode("a.hdr", &file("-Y 2 +X 2", &body)).unwrap();
assert_eq!((f.width, f.height), (2, 2));
let p = px(&f);
assert_eq!(&p[0..4], &[10.0, 20.0, 30.0, 1.0]);
assert_eq!(&p[4..8], &[1.0, 2.0, 3.0, 1.0]);
assert_eq!(&p[8..12], &[8.0, 10.0, 12.0, 1.0]); // exponent 137: scale 2
assert_eq!(&p[12..16], &[3.5, 4.0, 4.5, 1.0]); // exponent 135: scale 1/2
}
#[test]
fn orientations() {
// 3 wide, 2 high, pixel value = its index in the file
let body: Vec<u8> = (0..6u8).flat_map(|i| [i, 0, 0, 136]).collect();
let red = |res: &str| -> Vec<f32> {
let f = decode("a.hdr", &file(res, &body)).unwrap();
px(&f).chunks(4).map(|p| p[0]).collect()
};
assert_eq!(red("-Y 2 +X 3"), [0., 1., 2., 3., 4., 5.]);
assert_eq!(red("+Y 2 +X 3"), [3., 4., 5., 0., 1., 2.]);
assert_eq!(red("-Y 2 -X 3"), [2., 1., 0., 5., 4., 3.]);
// scan lines are columns: file index = x * 2 + (row, top first for -Y)
assert_eq!(red("+X 3 -Y 2"), [0., 2., 4., 1., 3., 5.]);
assert_eq!(red("-X 3 +Y 2"), [5., 3., 1., 4., 2., 0.]);
}
#[test]
fn run_length_lines() {
let w = 40usize;
let rows: Vec<Vec<[u8; 4]>> = (0..3).map(|y| (0..w).map(|x| if x < 20 { [100, 50, 25, 130] } else { [(x * 5 + y) as u8, 7, 9, 131] }).collect()).collect();
let body: Vec<u8> = rows.iter().flat_map(|r| rle_line(r)).collect();
let f = decode("a.hdr", &file(&format!("-Y 3 +X {w}"), &body)).unwrap();
let p = px(&f);
for (y, r) in rows.iter().enumerate() {
for (x, q) in r.iter().enumerate() {
let want = rgbe(*q);
assert_eq!(&p[(y * w + x) * 4..(y * w + x) * 4 + 3], &want, "pixel {x},{y}");
}
}
}
#[test]
fn old_style_runs() {
// one pixel, then (1,1,1,3) repeats it 3 times, then a (1,1,1,1) pair repeats 1<<0 then 1<<8 -> clipped to the line
let body = [[5u8, 6, 7, 136], [1, 1, 1, 3], [9, 9, 9, 136]].concat();
let f = decode("a.hdr", &file("-Y 1 +X 5", &body)).unwrap();
let p = px(&f);
assert_eq!(p[0], 5.0);
assert_eq!(p[12], 5.0); // fourth pixel is still a repeat
assert_eq!(p[16], 9.0);
}
#[test]
fn hostile_input_is_an_error_not_a_panic() {
let ok = file("-Y 2 +X 2", &[0; 16]);
assert!(decode("a.hdr", &ok).is_ok());
for cut in 0..ok.len() - 1 {
// a cut anywhere is an error (the header or the pixel data is incomplete), never a panic
assert!(decode("a.hdr", &ok[..cut]).is_err(), "{cut}");
}
assert!(decode("a.hdr", &file("-Y 2 +X 2", &[0; 8])).is_err());
assert!(decode("a.hdr", &file("-Y 99999999 +X 99999999", &[0; 16])).is_err());
assert!(decode("a.hdr", &file("-Y 0 +X 4", &[])).is_err());
assert!(decode("a.hdr", &file("-Y 2 -Y 2", &[0; 16])).is_err());
assert!(decode("a.hdr", &file("junk", &[0; 16])).is_err());
assert!(decode("a.hdr", b"#?RADIANCE\nFORMAT=32-bit_rle_xyze\n\n-Y 1 +X 1\n\0\0\0\0").is_err());
// an RLE line announcing the wrong width, a run past the end, a run at the start of a flat line
assert!(decode("a.hdr", &file("-Y 1 +X 10", &[2, 2, 0, 11, 0])).is_err());
assert!(decode("a.hdr", &file("-Y 1 +X 10", &[2, 2, 0, 10, 200, 1])).is_err());
assert!(decode("a.hdr", &file("-Y 1 +X 3", &[1, 1, 1, 9, 0, 0, 0, 0])).is_err());
assert!(decode("a.hdr", b"#?RADIANCE").is_err());
}
#[test]
fn opens_as_a_float_still() {
let body = [[128u8, 128, 128, 136]; 4].concat();
let s = still("a.hdr", &file("-Y 2 +X 2", &body)).unwrap();
let info = crate::MediaSource::info(&s);
assert_eq!(info.video.as_ref().map(|v| (v.width, v.height, v.codec.as_str())), Some((2, 2, "HDR")));
let src = crate::open_bytes("a.hdr", file("-Y 2 +X 2", &body).into(), &[]).unwrap();
assert_eq!(src.info().kind, MediaKind::Still);
}