wsi_streamer/slide/
reader.rs

1//! SlideReader trait for format-agnostic slide access.
2//!
3//! This module defines the `SlideReader` trait, which provides a unified interface
4//! for reading tiles from Whole Slide Images regardless of their underlying format.
5//!
6//! # Usage
7//!
8//! The trait is implemented by format-specific readers:
9//! - [`crate::format::SvsReader`] for Aperio SVS files
10//! - [`crate::format::GenericTiffReader`] for standard pyramidal TIFF files
11//!
12//! This allows the tile service layer to work with any supported format without
13//! format-specific logic.
14
15use async_trait::async_trait;
16use bytes::Bytes;
17
18use crate::error::TiffError;
19use crate::io::RangeReader;
20
21// =============================================================================
22// Level Information
23// =============================================================================
24
25/// Information about a single pyramid level.
26///
27/// This struct provides a snapshot of level metadata that can be queried
28/// without async operations.
29#[derive(Debug, Clone, Copy, PartialEq)]
30pub struct LevelInfo {
31    /// Width of this level in pixels
32    pub width: u32,
33
34    /// Height of this level in pixels
35    pub height: u32,
36
37    /// Width of each tile in pixels
38    pub tile_width: u32,
39
40    /// Height of each tile in pixels
41    pub tile_height: u32,
42
43    /// Number of tiles in X direction
44    pub tiles_x: u32,
45
46    /// Number of tiles in Y direction
47    pub tiles_y: u32,
48
49    /// Downsample factor relative to level 0
50    ///
51    /// Level 0 has downsample 1.0, level 1 might have 2.0, etc.
52    pub downsample: f64,
53}
54
55// =============================================================================
56// SlideReader Trait
57// =============================================================================
58
59/// Format-agnostic interface for reading tiles from Whole Slide Images.
60///
61/// This trait provides a unified API for accessing slide metadata and reading
62/// tiles, abstracting over the underlying file format. Implementations handle
63/// format-specific details like JPEGTables merging for SVS files.
64///
65/// # Type Parameters
66///
67/// The trait is generic over the `RangeReader` used for I/O, allowing the same
68/// slide reader to work with different storage backends (S3, local files, etc.).
69///
70/// # Example
71///
72/// ```ignore
73/// use wsi_streamer::slide::SlideReader;
74///
75/// async fn read_tile<R: RangeReader, S: SlideReader>(
76///     reader: &R,
77///     slide: &S,
78///     level: usize,
79///     x: u32,
80///     y: u32,
81/// ) -> Result<Bytes, TiffError> {
82///     // Check bounds
83///     let info = slide.level_info(level).ok_or_else(|| /* error */)?;
84///     if x >= info.tiles_x || y >= info.tiles_y {
85///         return Err(/* error */);
86///     }
87///
88///     // Read tile
89///     slide.read_tile(reader, level, x, y).await
90/// }
91/// ```
92#[async_trait]
93pub trait SlideReader: Send + Sync {
94    /// Get the number of pyramid levels.
95    ///
96    /// Level 0 is always the highest resolution (full size).
97    /// Higher levels have progressively lower resolution.
98    fn level_count(&self) -> usize;
99
100    /// Get dimensions of the full-resolution (level 0) image.
101    ///
102    /// Returns `(width, height)` in pixels, or `None` if no levels exist.
103    fn dimensions(&self) -> Option<(u32, u32)>;
104
105    /// Get dimensions of a specific level.
106    ///
107    /// Returns `(width, height)` in pixels, or `None` if level is out of range.
108    fn level_dimensions(&self, level: usize) -> Option<(u32, u32)>;
109
110    /// Get the downsample factor for a level.
111    ///
112    /// Level 0 always has downsample 1.0. Higher levels have larger values
113    /// (e.g., 2.0 means half the resolution in each dimension).
114    ///
115    /// Returns `None` if level is out of range.
116    fn level_downsample(&self, level: usize) -> Option<f64>;
117
118    /// Get tile size for a level.
119    ///
120    /// Returns `(tile_width, tile_height)` in pixels, or `None` if level is out of range.
121    ///
122    /// Note: Edge tiles may be smaller than this size.
123    fn tile_size(&self, level: usize) -> Option<(u32, u32)>;
124
125    /// Get the number of tiles in X and Y directions for a level.
126    ///
127    /// Returns `(tiles_x, tiles_y)`, or `None` if level is out of range.
128    fn tile_count(&self, level: usize) -> Option<(u32, u32)>;
129
130    /// Get complete information about a level.
131    ///
132    /// Returns `None` if level is out of range.
133    fn level_info(&self, level: usize) -> Option<LevelInfo> {
134        let (width, height) = self.level_dimensions(level)?;
135        let (tile_width, tile_height) = self.tile_size(level)?;
136        let (tiles_x, tiles_y) = self.tile_count(level)?;
137        let downsample = self.level_downsample(level)?;
138
139        Some(LevelInfo {
140            width,
141            height,
142            tile_width,
143            tile_height,
144            tiles_x,
145            tiles_y,
146            downsample,
147        })
148    }
149
150    /// Find the best level for a given downsample factor.
151    ///
152    /// Returns the index of the level with the smallest downsample that is
153    /// greater than or equal to the requested factor.
154    ///
155    /// This is useful for selecting an appropriate resolution level when
156    /// the viewer requests a specific zoom level.
157    ///
158    /// Returns `None` if no suitable level exists.
159    fn best_level_for_downsample(&self, downsample: f64) -> Option<usize>;
160
161    /// Read a tile and prepare it for JPEG decoding.
162    ///
163    /// This reads the tile data from storage and performs any necessary
164    /// processing (e.g., JPEGTables merging for SVS files) to produce
165    /// a complete JPEG stream that can be decoded by standard libraries.
166    ///
167    /// # Arguments
168    ///
169    /// * `reader` - The range reader for accessing the file data
170    /// * `level` - Pyramid level index (0 = highest resolution)
171    /// * `tile_x` - Tile X coordinate (0-indexed from left)
172    /// * `tile_y` - Tile Y coordinate (0-indexed from top)
173    ///
174    /// # Returns
175    ///
176    /// Complete JPEG data ready for decoding.
177    ///
178    /// # Errors
179    ///
180    /// Returns an error if:
181    /// - Level is out of range
182    /// - Tile coordinates are out of range
183    /// - I/O error occurs during read
184    async fn read_tile<R: RangeReader>(
185        &self,
186        reader: &R,
187        level: usize,
188        tile_x: u32,
189        tile_y: u32,
190    ) -> Result<Bytes, TiffError>;
191}
192
193// =============================================================================
194// Tests
195// =============================================================================
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_level_info_equality() {
203        let info1 = LevelInfo {
204            width: 1000,
205            height: 800,
206            tile_width: 256,
207            tile_height: 256,
208            tiles_x: 4,
209            tiles_y: 4,
210            downsample: 1.0,
211        };
212
213        let info2 = LevelInfo {
214            width: 1000,
215            height: 800,
216            tile_width: 256,
217            tile_height: 256,
218            tiles_x: 4,
219            tiles_y: 4,
220            downsample: 1.0,
221        };
222
223        assert_eq!(info1, info2);
224    }
225
226    #[test]
227    fn test_level_info_clone() {
228        let info = LevelInfo {
229            width: 1000,
230            height: 800,
231            tile_width: 256,
232            tile_height: 256,
233            tiles_x: 4,
234            tiles_y: 4,
235            downsample: 2.0,
236        };
237
238        let cloned = info;
239        assert_eq!(info.width, cloned.width);
240        assert_eq!(info.downsample, cloned.downsample);
241    }
242}