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}