use super::*;
use crate::formats::tiff_family::container::TiffContainer;
use std::io::Write;
use tempfile::NamedTempFile;
#[allow(clippy::type_complexity)]
fn build_synthetic_tiff(ifds: &[Vec<(u16, u16, u32, [u8; 4])>]) -> NamedTempFile {
let mut buf = Vec::new();
buf.extend_from_slice(b"II");
buf.extend_from_slice(&42u16.to_le_bytes());
let first_ifd_pos = buf.len();
buf.extend_from_slice(&0u32.to_le_bytes());
let mut ifd_offsets = Vec::new();
let mut next_ifd_patch_positions = Vec::new();
for tags in ifds.iter() {
let ifd_offset = buf.len() as u32;
ifd_offsets.push(ifd_offset);
let mut all_tags = tags.clone();
all_tags.sort_by_key(|t| t.0);
let entry_count = all_tags.len() as u16;
buf.extend_from_slice(&entry_count.to_le_bytes());
for (tag_id, type_id, count, value) in &all_tags {
buf.extend_from_slice(&tag_id.to_le_bytes());
buf.extend_from_slice(&type_id.to_le_bytes());
buf.extend_from_slice(&count.to_le_bytes());
buf.extend_from_slice(value);
}
let next_pos = buf.len();
buf.extend_from_slice(&0u32.to_le_bytes());
next_ifd_patch_positions.push(next_pos);
}
let offset_bytes = ifd_offsets[0].to_le_bytes();
buf[first_ifd_pos..first_ifd_pos + 4].copy_from_slice(&offset_bytes);
for i in 0..ifd_offsets.len() - 1 {
let patch_pos = next_ifd_patch_positions[i];
let next_offset = ifd_offsets[i + 1];
let bytes = next_offset.to_le_bytes();
buf[patch_pos..patch_pos + 4].copy_from_slice(&bytes);
}
let mut file = NamedTempFile::new().unwrap();
file.write_all(&buf).unwrap();
file.flush().unwrap();
file
}
fn long_tag(tag: u16, value: u32) -> (u16, u16, u32, [u8; 4]) {
(tag, 4, 1, value.to_le_bytes())
}
fn short_tag(tag: u16, value: u16) -> (u16, u16, u32, [u8; 4]) {
let mut val = [0u8; 4];
val[0..2].copy_from_slice(&value.to_le_bytes());
(tag, 3, 1, val)
}
fn clone_tempfile(src: &NamedTempFile) -> NamedTempFile {
let bytes = std::fs::read(src.path()).unwrap();
let mut file = NamedTempFile::new().unwrap();
file.write_all(&bytes).unwrap();
file.flush().unwrap();
file
}
#[test]
fn detect_tiled_tiff() {
let file = build_synthetic_tiff(&[vec![
long_tag(tags::IMAGE_WIDTH, 1024),
long_tag(tags::IMAGE_LENGTH, 768),
long_tag(tags::TILE_WIDTH, 256),
long_tag(tags::TILE_LENGTH, 256),
]]);
let container = TiffContainer::open(file.path()).unwrap();
let interp = GenericTiffInterpreter;
assert!(interp.detect(&container));
}
#[test]
fn reject_non_tiled_tiff() {
let file = build_synthetic_tiff(&[vec![
long_tag(tags::IMAGE_WIDTH, 1024),
long_tag(tags::IMAGE_LENGTH, 768),
]]);
let container = TiffContainer::open(file.path()).unwrap();
let interp = GenericTiffInterpreter;
assert!(!interp.detect(&container));
}
#[test]
fn reject_ndpi() {
let file = build_synthetic_tiff(&[vec![
long_tag(tags::IMAGE_WIDTH, 1024),
long_tag(tags::IMAGE_LENGTH, 768),
long_tag(tags::TILE_WIDTH, 256),
long_tag(tags::TILE_LENGTH, 256),
long_tag(tags::NDPI_MARKER, 1),
]]);
let container = TiffContainer::open(file.path()).unwrap();
let interp = GenericTiffInterpreter;
assert!(!interp.detect(&container));
}
#[test]
fn interpret_single_level() {
let file = build_synthetic_tiff(&[vec![
long_tag(tags::IMAGE_WIDTH, 1024),
long_tag(tags::IMAGE_LENGTH, 768),
long_tag(tags::TILE_WIDTH, 256),
long_tag(tags::TILE_LENGTH, 256),
short_tag(tags::COMPRESSION, 7), ]]);
let container = TiffContainer::open(file.path()).unwrap();
let interp = GenericTiffInterpreter;
let layout = interp.interpret(&container).unwrap();
assert_eq!(layout.dataset.scenes.len(), 1);
let series = &layout.dataset.scenes[0].series[0];
assert_eq!(series.levels.len(), 1);
assert_eq!(series.levels[0].dimensions, (1024, 768));
assert!((series.levels[0].downsample - 1.0).abs() < 0.001);
match &series.levels[0].tile_layout {
TileLayout::Regular {
tile_width,
tile_height,
tiles_across,
tiles_down,
} => {
assert_eq!(*tile_width, 256);
assert_eq!(*tile_height, 256);
assert_eq!(*tiles_across, 4);
assert_eq!(*tiles_down, 3);
}
other => panic!("expected Regular tile layout, got: {:?}", other),
}
let key = TileSourceKey {
scene: 0usize,
series: 0usize,
level: 0u32,
z: 0,
c: 0,
t: 0,
};
assert!(layout.tile_sources.contains_key(&key));
match layout.tile_sources.get(&key).unwrap() {
TileSource::TiledIfd { compression, .. } => {
assert_eq!(*compression, Compression::Jpeg);
}
other => panic!("expected TiledIfd, got: {:?}", other),
}
assert_eq!(layout.dataset.properties.vendor(), Some("generic-tiff"),);
assert!(layout.dataset.properties.quickhash1().is_some());
}
#[test]
fn dataset_identity_is_path_independent_for_same_contents() {
let file_a = build_synthetic_tiff(&[vec![
long_tag(tags::IMAGE_WIDTH, 1024),
long_tag(tags::IMAGE_LENGTH, 768),
long_tag(tags::TILE_WIDTH, 256),
long_tag(tags::TILE_LENGTH, 256),
short_tag(tags::COMPRESSION, 7),
]]);
let file_b = clone_tempfile(&file_a);
let container_a = TiffContainer::open(file_a.path()).unwrap();
let container_b = TiffContainer::open(file_b.path()).unwrap();
let interp = GenericTiffInterpreter;
let layout_a = interp.interpret(&container_a).unwrap();
let layout_b = interp.interpret(&container_b).unwrap();
assert_eq!(layout_a.dataset.id, layout_b.dataset.id);
assert_eq!(
layout_a.dataset.properties.quickhash1(),
layout_b.dataset.properties.quickhash1()
);
}
#[test]
fn interpret_multi_level_sorted() {
let file = build_synthetic_tiff(&[
vec![
long_tag(tags::IMAGE_WIDTH, 512),
long_tag(tags::IMAGE_LENGTH, 384),
long_tag(tags::TILE_WIDTH, 256),
long_tag(tags::TILE_LENGTH, 256),
],
vec![
long_tag(tags::IMAGE_WIDTH, 2048),
long_tag(tags::IMAGE_LENGTH, 1536),
long_tag(tags::TILE_WIDTH, 256),
long_tag(tags::TILE_LENGTH, 256),
],
]);
let container = TiffContainer::open(file.path()).unwrap();
let interp = GenericTiffInterpreter;
let layout = interp.interpret(&container).unwrap();
let series = &layout.dataset.scenes[0].series[0];
assert_eq!(series.levels.len(), 2);
assert_eq!(series.levels[0].dimensions, (2048, 1536));
assert!((series.levels[0].downsample - 1.0).abs() < 0.001);
assert_eq!(series.levels[1].dimensions, (512, 384));
assert!(series.levels[1].downsample > 1.0);
assert!((series.levels[1].downsample - 4.0).abs() < 0.01);
}
#[test]
fn interpret_stripped_as_associated() {
let file = build_synthetic_tiff(&[
vec![
long_tag(tags::IMAGE_WIDTH, 1024),
long_tag(tags::IMAGE_LENGTH, 768),
long_tag(tags::TILE_WIDTH, 256),
long_tag(tags::TILE_LENGTH, 256),
],
vec![
long_tag(tags::IMAGE_WIDTH, 400),
long_tag(tags::IMAGE_LENGTH, 300),
long_tag(tags::STRIP_OFFSETS, 100),
long_tag(tags::STRIP_BYTE_COUNTS, 500),
],
]);
let container = TiffContainer::open(file.path()).unwrap();
let interp = GenericTiffInterpreter;
let layout = interp.interpret(&container).unwrap();
assert_eq!(layout.dataset.scenes[0].series[0].levels.len(), 1);
assert!(layout.dataset.associated_images.contains_key("image_0"));
let ai = &layout.dataset.associated_images["image_0"];
assert_eq!(ai.dimensions, (400, 300));
assert!(layout.associated_sources.contains_key("image_0"));
match layout.associated_sources.get("image_0").unwrap() {
TileSource::Stripped {
strip_offsets,
strip_byte_counts,
..
} => {
assert_eq!(strip_offsets.as_slice(), &[100]);
assert_eq!(strip_byte_counts.as_slice(), &[500]);
}
other => panic!("expected Stripped, got: {:?}", other),
}
}
#[test]
fn interpret_ignores_associated_only_icc_for_main_series() {
let file = build_synthetic_tiff(&[
vec![
long_tag(tags::IMAGE_WIDTH, 1024),
long_tag(tags::IMAGE_LENGTH, 768),
long_tag(tags::TILE_WIDTH, 256),
long_tag(tags::TILE_LENGTH, 256),
],
vec![
long_tag(tags::IMAGE_WIDTH, 400),
long_tag(tags::IMAGE_LENGTH, 300),
long_tag(tags::STRIP_OFFSETS, 100),
long_tag(tags::STRIP_BYTE_COUNTS, 500),
(tags::ICC_PROFILE, 7, 4, [1, 2, 3, 4]),
],
]);
let container = TiffContainer::open(file.path()).unwrap();
let interp = GenericTiffInterpreter;
let layout = interp.interpret(&container).unwrap();
assert!(layout.dataset.source_icc_profiles.is_empty());
assert!(layout.dataset.icc_profiles.is_empty());
assert!(layout.dataset.associated_images.contains_key("image_0"));
}
#[test]
fn interpret_no_tiled_ifds_returns_error() {
let file = build_synthetic_tiff(&[vec![
long_tag(tags::IMAGE_WIDTH, 400),
long_tag(tags::IMAGE_LENGTH, 300),
]]);
let container = TiffContainer::open(file.path()).unwrap();
let interp = GenericTiffInterpreter;
let result = interp.interpret(&container);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("No tiled IFDs"),
"expected 'No tiled IFDs', got: {}",
msg,
);
}
#[test]
fn interpret_axes_default() {
let file = build_synthetic_tiff(&[vec![
long_tag(tags::IMAGE_WIDTH, 512),
long_tag(tags::IMAGE_LENGTH, 512),
long_tag(tags::TILE_WIDTH, 256),
long_tag(tags::TILE_LENGTH, 256),
]]);
let container = TiffContainer::open(file.path()).unwrap();
let interp = GenericTiffInterpreter;
let layout = interp.interpret(&container).unwrap();
let axes = layout.dataset.scenes[0].series[0].axes;
assert_eq!(axes, AxesShape { z: 1, c: 1, t: 1 });
}
#[test]
fn interpret_tile_count_ceil() {
let file = build_synthetic_tiff(&[vec![
long_tag(tags::IMAGE_WIDTH, 1000),
long_tag(tags::IMAGE_LENGTH, 500),
long_tag(tags::TILE_WIDTH, 256),
long_tag(tags::TILE_LENGTH, 256),
]]);
let container = TiffContainer::open(file.path()).unwrap();
let interp = GenericTiffInterpreter;
let layout = interp.interpret(&container).unwrap();
match &layout.dataset.scenes[0].series[0].levels[0].tile_layout {
TileLayout::Regular {
tiles_across,
tiles_down,
..
} => {
assert_eq!(*tiles_across, 4);
assert_eq!(*tiles_down, 2);
}
other => panic!("expected Regular, got: {:?}", other),
}
}
#[test]
fn compression_mapping() {
assert_eq!(compression_from_tag(1), Compression::None);
assert_eq!(compression_from_tag(6), Compression::Jpeg);
assert_eq!(compression_from_tag(7), Compression::Jpeg);
assert_eq!(compression_from_tag(5), Compression::Lzw);
assert_eq!(compression_from_tag(8), Compression::Deflate);
assert_eq!(compression_from_tag(32946), Compression::Deflate);
assert_eq!(compression_from_tag(50000), Compression::Zstd);
assert_eq!(compression_from_tag(33003), Compression::Jp2kYcbcr);
assert_eq!(compression_from_tag(33005), Compression::Jp2kYcbcr);
assert_eq!(compression_from_tag(33004), Compression::Jp2kRgb);
assert_eq!(compression_from_tag(999), Compression::Other(999));
}