1use crate::error::FormatError;
12use crate::io::RangeReader;
13
14use super::tiff::{ByteOrder, Ifd, TiffHeader, TiffTag, BIGTIFF_HEADER_SIZE, TIFF_HEADER_SIZE};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum SlideFormat {
26 AperioSvs,
28
29 GenericTiff,
31}
32
33impl SlideFormat {
34 pub const fn name(&self) -> &'static str {
36 match self {
37 SlideFormat::AperioSvs => "Aperio SVS",
38 SlideFormat::GenericTiff => "Generic Pyramidal TIFF",
39 }
40 }
41}
42
43const MIN_HEADER_BYTES: usize = BIGTIFF_HEADER_SIZE;
49
50const MAX_DESCRIPTION_BYTES: usize = 1024;
53
54const APERIO_MARKER: &[u8] = b"Aperio";
56
57pub async fn detect_format<R: RangeReader>(reader: &R) -> Result<SlideFormat, FormatError> {
77 if reader.size() < MIN_HEADER_BYTES as u64 {
79 return Err(FormatError::UnsupportedFormat {
80 reason: "File too small to be a valid TIFF".to_string(),
81 });
82 }
83
84 let header_bytes = reader.read_exact_at(0, MIN_HEADER_BYTES).await?;
86 let header = TiffHeader::parse(&header_bytes, reader.size())?;
87
88 let format = detect_format_from_first_ifd(reader, &header).await?;
90
91 Ok(format)
92}
93
94async fn detect_format_from_first_ifd<R: RangeReader>(
99 reader: &R,
100 header: &TiffHeader,
101) -> Result<SlideFormat, FormatError> {
102 let count_size = header.ifd_count_size();
104 let count_bytes = reader
105 .read_exact_at(header.first_ifd_offset, count_size)
106 .await?;
107
108 let entry_count = if header.is_bigtiff {
109 header.byte_order.read_u64(&count_bytes)
110 } else {
111 header.byte_order.read_u16(&count_bytes) as u64
112 };
113
114 let ifd_size = Ifd::calculate_size(entry_count, header);
116 let ifd_bytes = reader
117 .read_exact_at(header.first_ifd_offset, ifd_size)
118 .await?;
119 let ifd = Ifd::parse(&ifd_bytes, header)?;
120
121 if let Some(description) = read_image_description(reader, &ifd, header).await? {
123 if contains_aperio_marker(&description) {
125 return Ok(SlideFormat::AperioSvs);
126 }
127 }
128
129 Ok(SlideFormat::GenericTiff)
131}
132
133async fn read_image_description<R: RangeReader>(
137 reader: &R,
138 ifd: &Ifd,
139 header: &TiffHeader,
140) -> Result<Option<Vec<u8>>, FormatError> {
141 let entry = match ifd.get_entry_by_tag(TiffTag::ImageDescription) {
142 Some(e) => e,
143 None => return Ok(None),
144 };
145
146 let read_len = (entry.count as usize).min(MAX_DESCRIPTION_BYTES);
148 if read_len == 0 {
149 return Ok(None);
150 }
151
152 let bytes = if entry.is_inline {
154 entry.value_offset_bytes[..read_len.min(entry.value_offset_bytes.len())].to_vec()
156 } else {
157 let offset = entry.value_offset(header.byte_order);
159 reader.read_exact_at(offset, read_len).await?.to_vec()
160 };
161
162 Ok(Some(bytes))
163}
164
165fn contains_aperio_marker(data: &[u8]) -> bool {
167 data.windows(APERIO_MARKER.len())
169 .any(|window| window == APERIO_MARKER)
170}
171
172pub fn is_tiff_header(bytes: &[u8]) -> bool {
176 if bytes.len() < TIFF_HEADER_SIZE {
177 return false;
178 }
179
180 let magic = u16::from_le_bytes([bytes[0], bytes[1]]);
182 if magic != 0x4949 && magic != 0x4D4D {
183 return false;
184 }
185
186 let byte_order = if magic == 0x4949 {
188 ByteOrder::LittleEndian
189 } else {
190 ByteOrder::BigEndian
191 };
192
193 let version = byte_order.read_u16(&bytes[2..4]);
194 version == 42 || version == 43
195}
196
197#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
210 fn test_is_tiff_header_little_endian_classic() {
211 let header = [
212 0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00, ];
216 assert!(is_tiff_header(&header));
217 }
218
219 #[test]
220 fn test_is_tiff_header_big_endian_classic() {
221 let header = [
222 0x4D, 0x4D, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x08, ];
226 assert!(is_tiff_header(&header));
227 }
228
229 #[test]
230 fn test_is_tiff_header_little_endian_bigtiff() {
231 let header = [
232 0x49, 0x49, 0x2B, 0x00, 0x08, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
238 assert!(is_tiff_header(&header));
239 }
240
241 #[test]
242 fn test_is_tiff_header_big_endian_bigtiff() {
243 let header = [
244 0x4D, 0x4D, 0x00, 0x2B, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, ];
250 assert!(is_tiff_header(&header));
251 }
252
253 #[test]
254 fn test_is_tiff_header_invalid_magic() {
255 let header = [
256 0x00, 0x00, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00,
258 ];
259 assert!(!is_tiff_header(&header));
260 }
261
262 #[test]
263 fn test_is_tiff_header_invalid_version() {
264 let header = [
265 0x49, 0x49, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
268 ];
269 assert!(!is_tiff_header(&header));
270 }
271
272 #[test]
273 fn test_is_tiff_header_too_small() {
274 let header = [0x49, 0x49, 0x2A, 0x00]; assert!(!is_tiff_header(&header));
276 }
277
278 #[test]
279 fn test_is_tiff_header_jpeg() {
280 let header = [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46];
282 assert!(!is_tiff_header(&header));
283 }
284
285 #[test]
286 fn test_is_tiff_header_png() {
287 let header = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
289 assert!(!is_tiff_header(&header));
290 }
291
292 #[test]
297 fn test_contains_aperio_marker_present() {
298 let data = b"Aperio Image Library v12.0.0";
299 assert!(contains_aperio_marker(data));
300 }
301
302 #[test]
303 fn test_contains_aperio_marker_in_description() {
304 let data = b"Some prefix|Aperio Image Library|Some suffix";
305 assert!(contains_aperio_marker(data));
306 }
307
308 #[test]
309 fn test_contains_aperio_marker_not_present() {
310 let data = b"Generic TIFF image description";
311 assert!(!contains_aperio_marker(data));
312 }
313
314 #[test]
315 fn test_contains_aperio_marker_empty() {
316 let data = b"";
317 assert!(!contains_aperio_marker(data));
318 }
319
320 #[test]
321 fn test_contains_aperio_marker_partial() {
322 let data = b"Aperi"; assert!(!contains_aperio_marker(data));
324 }
325
326 #[test]
327 fn test_contains_aperio_marker_case_sensitive() {
328 let data = b"aperio"; assert!(!contains_aperio_marker(data));
330 }
331
332 #[test]
337 fn test_slide_format_name() {
338 assert_eq!(SlideFormat::AperioSvs.name(), "Aperio SVS");
339 assert_eq!(SlideFormat::GenericTiff.name(), "Generic Pyramidal TIFF");
340 }
341}