use oxideav_tiff::decode_tiff;
fn entry_short(tag: u16, value: u16) -> [u8; 12] {
let mut e = [0u8; 12];
e[0..2].copy_from_slice(&tag.to_le_bytes());
e[2..4].copy_from_slice(&3u16.to_le_bytes()); e[4..8].copy_from_slice(&1u32.to_le_bytes()); e[8..10].copy_from_slice(&value.to_le_bytes());
e
}
fn entry_short_array(
tag: u16,
count: u32,
inline_or_offset: &[u16],
offset: Option<u32>,
) -> [u8; 12] {
let mut e = [0u8; 12];
e[0..2].copy_from_slice(&tag.to_le_bytes());
e[2..4].copy_from_slice(&3u16.to_le_bytes()); e[4..8].copy_from_slice(&count.to_le_bytes());
match offset {
Some(off) => e[8..12].copy_from_slice(&off.to_le_bytes()),
None => {
for (i, v) in inline_or_offset.iter().enumerate().take(2) {
e[8 + 2 * i..10 + 2 * i].copy_from_slice(&v.to_le_bytes());
}
}
}
e
}
fn build_1x1_rgb(spp: u16, pixel: &[u8], extra_samples: Option<&[u16]>) -> Vec<u8> {
assert_eq!(pixel.len(), spp as usize);
let es_len = extra_samples.map(|v| v.len()).unwrap_or(0);
assert!(es_len <= 2, "inline SHORT slot holds at most 2 values");
let mut out = Vec::new();
out.extend_from_slice(b"II");
out.extend_from_slice(&42u16.to_le_bytes());
let ifd_off_pos = out.len();
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(pixel);
if out.len() % 2 != 0 {
out.push(0);
}
let bps_off = out.len() as u32;
for _ in 0..spp {
out.extend_from_slice(&8u16.to_le_bytes());
}
let ifd_off = out.len() as u32;
out[ifd_off_pos..ifd_off_pos + 4].copy_from_slice(&ifd_off.to_le_bytes());
let n: u16 = 9 + if extra_samples.is_some() { 1 } else { 0 };
out.extend_from_slice(&n.to_le_bytes());
out.extend_from_slice(&entry_short(256, 1)); out.extend_from_slice(&entry_short(257, 1)); out.extend_from_slice(&entry_short_array(258, spp as u32, &[], Some(bps_off))); out.extend_from_slice(&entry_short(259, 1)); out.extend_from_slice(&entry_short(262, 2)); out.extend_from_slice(&entry_short(273, 8)); out.extend_from_slice(&entry_short(277, spp)); out.extend_from_slice(&entry_short(278, 1)); out.extend_from_slice(&entry_short(279, spp)); if let Some(es) = extra_samples {
out.extend_from_slice(&entry_short_array(338, es.len() as u32, es, None));
}
out.extend_from_slice(&0u32.to_le_bytes());
out
}
fn build_1x1_gray8_with_extras(extra_samples: &[u16]) -> Vec<u8> {
assert!(extra_samples.len() <= 2);
let mut out = Vec::new();
out.extend_from_slice(b"II");
out.extend_from_slice(&42u16.to_le_bytes());
out.extend_from_slice(&16u32.to_le_bytes());
out.push(0xAB); out.extend_from_slice(&[0u8; 7]);
let n: u16 = 10;
out.extend_from_slice(&n.to_le_bytes());
out.extend_from_slice(&entry_short(256, 1)); out.extend_from_slice(&entry_short(257, 1)); out.extend_from_slice(&entry_short(258, 8)); out.extend_from_slice(&entry_short(259, 1)); out.extend_from_slice(&entry_short(262, 1)); out.extend_from_slice(&entry_short(273, 8)); out.extend_from_slice(&entry_short(277, 1)); out.extend_from_slice(&entry_short(278, 1)); out.extend_from_slice(&entry_short(279, 1)); out.extend_from_slice(&entry_short_array(
338,
extra_samples.len() as u32,
extra_samples,
None,
));
out.extend_from_slice(&0u32.to_le_bytes());
out
}
fn expect_err_containing(bytes: &[u8], needle: &str) {
match decode_tiff(bytes) {
Ok(_) => panic!("expected an error containing {needle:?}, got Ok(..)"),
Err(e) => {
let msg = format!("{e}");
assert!(
msg.contains(needle),
"expected error to contain {needle:?}, got: {msg}"
);
}
}
}
#[test]
fn rgb_spp4_without_extra_samples_decodes_by_skipping() {
let bytes = build_1x1_rgb(4, &[10, 20, 30, 99], None);
let d = decode_tiff(&bytes).expect("RGB SamplesPerPixel=4 without tag 338 must decode");
assert_eq!((d.width, d.height), (1, 1));
assert_eq!(d.frame.planes[0].data, vec![10, 20, 30]);
}
#[test]
fn extra_samples_zero_unspecified_decodes_by_skipping() {
let bytes = build_1x1_rgb(4, &[10, 20, 30, 99], Some(&[0]));
let d = decode_tiff(&bytes).expect("ExtraSamples=[0] must decode");
assert_eq!((d.width, d.height), (1, 1));
assert_eq!(d.frame.planes[0].data, vec![10, 20, 30]);
}
#[test]
fn extra_samples_two_unassociated_alpha_decodes_by_skipping() {
let bytes = build_1x1_rgb(4, &[10, 20, 30, 99], Some(&[2]));
let d = decode_tiff(&bytes).expect("ExtraSamples=[2] must decode");
assert_eq!((d.width, d.height), (1, 1));
assert_eq!(d.frame.planes[0].data, vec![10, 20, 30]);
}
#[test]
fn extra_samples_one_associated_alpha_is_rejected_unsupported() {
let bytes = build_1x1_rgb(4, &[10, 20, 30, 99], Some(&[1]));
expect_err_containing(&bytes, "ExtraSamples=1");
expect_err_containing(&bytes, "unsupported");
}
#[test]
fn extra_samples_two_extras_spp5_decode_by_skipping() {
let bytes = build_1x1_rgb(5, &[10, 20, 30, 99, 77], Some(&[0, 2]));
let d = decode_tiff(&bytes).expect("ExtraSamples=[0,2] on SamplesPerPixel=5 must decode");
assert_eq!((d.width, d.height), (1, 1));
assert_eq!(d.frame.planes[0].data, vec![10, 20, 30]);
}
#[test]
fn extra_samples_mixed_with_associated_alpha_is_rejected() {
let bytes = build_1x1_rgb(5, &[10, 20, 30, 99, 77], Some(&[0, 1]));
expect_err_containing(&bytes, "ExtraSamples=1");
}
#[test]
fn extra_samples_unknown_value_is_rejected_as_invalid() {
let bytes = build_1x1_rgb(4, &[10, 20, 30, 99], Some(&[3]));
expect_err_containing(&bytes, "ExtraSamples=3");
}
#[test]
fn extra_samples_large_value_is_rejected_as_invalid() {
let bytes = build_1x1_rgb(4, &[10, 20, 30, 99], Some(&[65535]));
expect_err_containing(&bytes, "ExtraSamples=65535");
}
#[test]
fn extra_samples_count_mismatch_on_rgb_is_rejected() {
let bytes = build_1x1_rgb(4, &[10, 20, 30, 99], Some(&[2, 2]));
expect_err_containing(&bytes, "ExtraSamples count 2");
}
#[test]
fn extra_samples_on_spp3_rgb_is_rejected() {
let bytes = build_1x1_rgb(3, &[10, 20, 30], Some(&[2]));
expect_err_containing(&bytes, "ExtraSamples count 1");
}
#[test]
fn extra_samples_on_spp1_gray_is_rejected() {
let bytes = build_1x1_gray8_with_extras(&[2]);
expect_err_containing(&bytes, "ExtraSamples count 1");
}