wsi_streamer/format/
generic_tiff.rs

1//! Generic pyramidal TIFF reader.
2//!
3//! This module provides support for reading standard pyramidal TIFF files
4//! that use tiled organization with JPEG compression.
5//!
6//! # Supported Files
7//!
8//! This reader supports TIFF files that:
9//! - Use tiled organization (not strips)
10//! - Use JPEG or JPEG 2000 compression (compression tag = 7 or 33003)
11//! - Have multiple resolution levels (pyramid structure)
12//!
13//! # Unsupported Files
14//!
15//! Files that don't meet these requirements return an error that can be
16//! mapped to HTTP 415 Unsupported Media Type:
17//! - Strip-based TIFFs
18//! - Non-JPEG/JPEG 2000 compression (LZW, Deflate, etc.)
19//! - Single-level TIFFs without pyramid structure
20
21use async_trait::async_trait;
22use bytes::Bytes;
23
24use crate::error::TiffError;
25use crate::io::RangeReader;
26use crate::slide::SlideReader;
27
28use super::jpeg::prepare_tile_jpeg;
29use super::tiff::{
30    validate_pyramid, PyramidLevel, TiffHeader, TiffPyramid, TileData, ValidationResult,
31};
32
33// =============================================================================
34// Generic TIFF Level Data
35// =============================================================================
36
37/// Data for a single pyramid level in a generic TIFF file.
38#[derive(Debug, Clone)]
39pub struct GenericTiffLevelData {
40    /// The pyramid level metadata
41    pub level: PyramidLevel,
42
43    /// Tile offsets and byte counts
44    pub tile_data: TileData,
45}
46
47impl GenericTiffLevelData {
48    /// Get the offset and size for a specific tile.
49    pub fn get_tile_location(&self, tile_x: u32, tile_y: u32) -> Option<(u64, u64)> {
50        let tile_index = self.level.tile_index(tile_x, tile_y)?;
51        self.tile_data.get_tile_location(tile_index)
52    }
53
54    /// Get the JPEGTables for this level (if present).
55    pub fn jpeg_tables(&self) -> Option<&Bytes> {
56        self.tile_data.jpeg_tables.as_ref()
57    }
58}
59
60// =============================================================================
61// Generic TIFF Reader
62// =============================================================================
63
64/// Reader for generic pyramidal TIFF files.
65///
66/// This reader handles standard tiled TIFF files with JPEG compression.
67/// It validates the file structure on open and rejects unsupported configurations.
68#[derive(Debug)]
69pub struct GenericTiffReader {
70    /// Parsed TIFF pyramid structure
71    pyramid: TiffPyramid,
72
73    /// Level data including tile offsets and optional JPEGTables
74    levels: Vec<GenericTiffLevelData>,
75
76    /// Validation warnings (non-fatal issues)
77    warnings: Vec<String>,
78}
79
80impl GenericTiffReader {
81    /// Open a generic pyramidal TIFF file.
82    ///
83    /// This reads the TIFF structure, validates it meets requirements,
84    /// and loads tile offset arrays.
85    ///
86    /// # Errors
87    ///
88    /// Returns an error if:
89    /// - The file is not a valid TIFF
90    /// - The file uses strip organization (not tiles)
91    /// - The file uses unsupported compression (not JPEG)
92    /// - No pyramid levels are found
93    pub async fn open<R: RangeReader>(reader: &R) -> Result<Self, TiffError> {
94        // Parse the TIFF pyramid structure
95        let pyramid = TiffPyramid::parse(reader).await?;
96
97        // Validate the pyramid meets our requirements
98        let validation = validate_pyramid(&pyramid);
99        if !validation.is_valid {
100            return Err(validation.into_result().unwrap_err());
101        }
102
103        // Store warnings for later inspection
104        let warnings = validation.warnings;
105
106        // Load tile data for each pyramid level
107        let mut levels = Vec::with_capacity(pyramid.levels.len());
108        for level in &pyramid.levels {
109            let tile_data = TileData::load(reader, level, &pyramid.header).await?;
110            levels.push(GenericTiffLevelData {
111                level: level.clone(),
112                tile_data,
113            });
114        }
115
116        Ok(GenericTiffReader {
117            pyramid,
118            levels,
119            warnings,
120        })
121    }
122
123    /// Open a generic pyramidal TIFF with detailed validation result.
124    ///
125    /// This is like `open()` but returns the validation result separately,
126    /// allowing access to warnings even on success.
127    pub async fn open_with_validation<R: RangeReader>(
128        reader: &R,
129    ) -> Result<(Self, ValidationResult), TiffError> {
130        // Parse the TIFF pyramid structure
131        let pyramid = TiffPyramid::parse(reader).await?;
132
133        // Validate the pyramid
134        let validation = validate_pyramid(&pyramid);
135        if !validation.is_valid {
136            return Err(validation.clone().into_result().unwrap_err());
137        }
138
139        // Load tile data for each pyramid level
140        let mut levels = Vec::with_capacity(pyramid.levels.len());
141        for level in &pyramid.levels {
142            let tile_data = TileData::load(reader, level, &pyramid.header).await?;
143            levels.push(GenericTiffLevelData {
144                level: level.clone(),
145                tile_data,
146            });
147        }
148
149        let reader = GenericTiffReader {
150            pyramid,
151            levels,
152            warnings: validation.warnings.clone(),
153        };
154
155        Ok((reader, validation))
156    }
157
158    /// Get the TIFF header.
159    pub fn header(&self) -> &TiffHeader {
160        &self.pyramid.header
161    }
162
163    /// Get validation warnings from file open.
164    ///
165    /// Warnings indicate non-fatal issues like unusual tile dimensions.
166    pub fn warnings(&self) -> &[String] {
167        &self.warnings
168    }
169
170    /// Get the number of pyramid levels.
171    pub fn level_count(&self) -> usize {
172        self.levels.len()
173    }
174
175    /// Get data for a specific pyramid level.
176    pub fn get_level(&self, level: usize) -> Option<&GenericTiffLevelData> {
177        self.levels.get(level)
178    }
179
180    /// Get dimensions of the full-resolution (level 0) image.
181    pub fn dimensions(&self) -> Option<(u32, u32)> {
182        self.levels.first().map(|l| (l.level.width, l.level.height))
183    }
184
185    /// Get dimensions of a specific level.
186    pub fn level_dimensions(&self, level: usize) -> Option<(u32, u32)> {
187        self.levels
188            .get(level)
189            .map(|l| (l.level.width, l.level.height))
190    }
191
192    /// Get the downsample factor for a level.
193    pub fn level_downsample(&self, level: usize) -> Option<f64> {
194        self.levels.get(level).map(|l| l.level.downsample)
195    }
196
197    /// Get tile size for a level.
198    pub fn tile_size(&self, level: usize) -> Option<(u32, u32)> {
199        self.levels
200            .get(level)
201            .map(|l| (l.level.tile_width, l.level.tile_height))
202    }
203
204    /// Get the number of tiles in X and Y directions for a level.
205    pub fn tile_count(&self, level: usize) -> Option<(u32, u32)> {
206        self.levels
207            .get(level)
208            .map(|l| (l.level.tiles_x, l.level.tiles_y))
209    }
210
211    /// Read raw tile data from the file.
212    ///
213    /// This reads the raw bytes from the file without any processing.
214    pub async fn read_raw_tile<R: RangeReader>(
215        &self,
216        reader: &R,
217        level: usize,
218        tile_x: u32,
219        tile_y: u32,
220    ) -> Result<Bytes, TiffError> {
221        let level_data = self.levels.get(level).ok_or(TiffError::InvalidTagValue {
222            tag: "level",
223            message: format!("level {} out of range (max {})", level, self.levels.len()),
224        })?;
225
226        let (offset, size) =
227            level_data
228                .get_tile_location(tile_x, tile_y)
229                .ok_or(TiffError::InvalidTagValue {
230                    tag: "tile",
231                    message: format!(
232                        "tile ({}, {}) out of range for level {}",
233                        tile_x, tile_y, level
234                    ),
235                })?;
236
237        let data = reader.read_exact_at(offset, size as usize).await?;
238        Ok(data)
239    }
240
241    /// Read a tile and prepare it for JPEG decoding.
242    ///
243    /// This reads the tile data and merges it with JPEGTables if the tile
244    /// contains an abbreviated JPEG stream (rare for generic TIFF but handled).
245    ///
246    /// # Arguments
247    /// * `reader` - Range reader for the file
248    /// * `level` - Pyramid level index
249    /// * `tile_x` - Tile X coordinate
250    /// * `tile_y` - Tile Y coordinate
251    ///
252    /// # Returns
253    /// Complete JPEG data ready for decoding.
254    pub async fn read_tile<R: RangeReader>(
255        &self,
256        reader: &R,
257        level: usize,
258        tile_x: u32,
259        tile_y: u32,
260    ) -> Result<Bytes, TiffError> {
261        // Read raw tile data
262        let raw_data = self.read_raw_tile(reader, level, tile_x, tile_y).await?;
263
264        // Get JPEGTables for this level (may not be present in generic TIFF)
265        let level_data = self.levels.get(level).ok_or(TiffError::InvalidTagValue {
266            tag: "level",
267            message: format!("level {} out of range", level),
268        })?;
269
270        let tables = level_data.jpeg_tables();
271
272        // Prepare the JPEG data (merge tables if needed)
273        let jpeg_data = prepare_tile_jpeg(tables.map(|t| t.as_ref()), &raw_data);
274
275        Ok(jpeg_data)
276    }
277
278    /// Find the best level for a given downsample factor.
279    ///
280    /// Returns the level with the smallest downsample that is >= the requested factor.
281    pub fn best_level_for_downsample(&self, downsample: f64) -> Option<usize> {
282        self.pyramid
283            .best_level_for_downsample(downsample)
284            .map(|l| l.level_index)
285    }
286}
287
288// =============================================================================
289// SlideReader Implementation
290// =============================================================================
291
292#[async_trait]
293impl SlideReader for GenericTiffReader {
294    fn level_count(&self) -> usize {
295        self.levels.len()
296    }
297
298    fn dimensions(&self) -> Option<(u32, u32)> {
299        self.levels.first().map(|l| (l.level.width, l.level.height))
300    }
301
302    fn level_dimensions(&self, level: usize) -> Option<(u32, u32)> {
303        self.levels
304            .get(level)
305            .map(|l| (l.level.width, l.level.height))
306    }
307
308    fn level_downsample(&self, level: usize) -> Option<f64> {
309        self.levels.get(level).map(|l| l.level.downsample)
310    }
311
312    fn tile_size(&self, level: usize) -> Option<(u32, u32)> {
313        self.levels
314            .get(level)
315            .map(|l| (l.level.tile_width, l.level.tile_height))
316    }
317
318    fn tile_count(&self, level: usize) -> Option<(u32, u32)> {
319        self.levels
320            .get(level)
321            .map(|l| (l.level.tiles_x, l.level.tiles_y))
322    }
323
324    fn best_level_for_downsample(&self, downsample: f64) -> Option<usize> {
325        GenericTiffReader::best_level_for_downsample(self, downsample)
326    }
327
328    async fn read_tile<R: RangeReader>(
329        &self,
330        reader: &R,
331        level: usize,
332        tile_x: u32,
333        tile_y: u32,
334    ) -> Result<Bytes, TiffError> {
335        GenericTiffReader::read_tile(self, reader, level, tile_x, tile_y).await
336    }
337}
338
339// =============================================================================
340// Tests
341// =============================================================================
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346    use crate::error::IoError;
347    use crate::format::tiff::{FieldType, Ifd, IfdEntry, TiffTag};
348    use crate::io::RangeReader;
349    use async_trait::async_trait;
350    use std::collections::HashMap;
351
352    // Mock reader for testing
353    #[allow(dead_code)]
354    struct MockTiffReader {
355        // Simulates a minimal tiled TIFF with JPEG compression
356        data: Vec<u8>,
357    }
358
359    #[allow(dead_code)]
360    impl MockTiffReader {
361        fn new_valid_tiff() -> Self {
362            // Create a minimal valid TIFF header and structure
363            // This is a simplified mock - real tests would use actual TIFF files
364            let mut data = vec![0u8; 1024];
365
366            // Little-endian TIFF header
367            data[0] = 0x49; // 'I'
368            data[1] = 0x49; // 'I'
369            data[2] = 0x2A; // Version 42
370            data[3] = 0x00;
371            data[4] = 0x08; // First IFD at offset 8
372            data[5] = 0x00;
373            data[6] = 0x00;
374            data[7] = 0x00;
375
376            // IFD at offset 8
377            // Entry count = 7
378            data[8] = 0x07;
379            data[9] = 0x00;
380
381            // This is a simplified structure - actual IFD parsing is complex
382            // The real tests would use integration tests with actual files
383
384            MockTiffReader { data }
385        }
386    }
387
388    #[async_trait]
389    impl RangeReader for MockTiffReader {
390        async fn read_exact_at(&self, offset: u64, len: usize) -> Result<Bytes, IoError> {
391            let start = offset as usize;
392            let end = start + len;
393            if end > self.data.len() {
394                return Err(IoError::RangeOutOfBounds {
395                    offset,
396                    requested: len as u64,
397                    size: self.data.len() as u64,
398                });
399            }
400            Ok(Bytes::copy_from_slice(&self.data[start..end]))
401        }
402
403        fn size(&self) -> u64 {
404            self.data.len() as u64
405        }
406
407        fn identifier(&self) -> &str {
408            "mock://test.tif"
409        }
410    }
411
412    // -------------------------------------------------------------------------
413    // GenericTiffLevelData tests
414    // -------------------------------------------------------------------------
415
416    fn make_mock_level() -> GenericTiffLevelData {
417        let ifd = Ifd {
418            entries: vec![],
419            entries_by_tag: HashMap::new(),
420            next_ifd_offset: 0,
421        };
422
423        let level = PyramidLevel {
424            level_index: 0,
425            ifd_index: 0,
426            width: 1000,
427            height: 800,
428            tile_width: 256,
429            tile_height: 256,
430            tiles_x: 4,
431            tiles_y: 4,
432            tile_count: 16,
433            downsample: 1.0,
434            compression: 7,
435            ifd,
436            tile_offsets_entry: Some(IfdEntry {
437                tag_id: TiffTag::TileOffsets.as_u16(),
438                field_type: Some(FieldType::Long),
439                field_type_raw: 4,
440                count: 16,
441                value_offset_bytes: vec![0, 0, 0, 0],
442                is_inline: false,
443            }),
444            tile_byte_counts_entry: Some(IfdEntry {
445                tag_id: TiffTag::TileByteCounts.as_u16(),
446                field_type: Some(FieldType::Long),
447                field_type_raw: 4,
448                count: 16,
449                value_offset_bytes: vec![0, 0, 0, 0],
450                is_inline: false,
451            }),
452            jpeg_tables_entry: None,
453        };
454
455        let tile_data = TileData {
456            offsets: vec![
457                1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000, 11000, 12000, 13000,
458                14000, 15000, 16000,
459            ],
460            byte_counts: vec![500; 16],
461            jpeg_tables: None,
462        };
463
464        GenericTiffLevelData { level, tile_data }
465    }
466
467    #[test]
468    fn test_get_tile_location() {
469        let level_data = make_mock_level();
470
471        // First tile
472        assert_eq!(level_data.get_tile_location(0, 0), Some((1000, 500)));
473
474        // Second tile in first row
475        assert_eq!(level_data.get_tile_location(1, 0), Some((2000, 500)));
476
477        // First tile in second row
478        assert_eq!(level_data.get_tile_location(0, 1), Some((5000, 500)));
479
480        // Out of bounds
481        assert_eq!(level_data.get_tile_location(10, 0), None);
482        assert_eq!(level_data.get_tile_location(0, 10), None);
483    }
484
485    #[test]
486    fn test_jpeg_tables_none() {
487        let level_data = make_mock_level();
488        assert!(level_data.jpeg_tables().is_none());
489    }
490
491    #[test]
492    fn test_jpeg_tables_present() {
493        let mut level_data = make_mock_level();
494        level_data.tile_data.jpeg_tables = Some(Bytes::from_static(&[0xFF, 0xD8, 0xFF, 0xD9]));
495
496        let tables = level_data.jpeg_tables();
497        assert!(tables.is_some());
498        assert_eq!(tables.unwrap().len(), 4);
499    }
500}