Skip to main content

wsi_rs/core/registry/
traits.rs

1use super::*;
2
3// ── Probe traits ───────────────────────────────────────────────────
4
5/// Detects whether a file is a given format. Fast, no full parse.
6pub trait FormatProbe: Send + Sync {
7    fn probe(&self, path: &Path) -> Result<ProbeResult, WsiError>;
8}
9
10/// Result from a cheap file-format probe.
11#[derive(Debug)]
12#[non_exhaustive]
13pub struct ProbeResult {
14    pub detected: bool,
15    pub vendor: String,
16    pub confidence: ProbeConfidence,
17}
18
19impl ProbeResult {
20    /// Creates a positive probe result for a detected vendor.
21    pub fn detected(vendor: impl Into<String>, confidence: ProbeConfidence) -> Self {
22        Self {
23            detected: true,
24            vendor: vendor.into(),
25            confidence,
26        }
27    }
28
29    /// Creates a negative probe result for a vendor that did not match.
30    ///
31    /// The registry ignores `confidence` when `detected` is false.
32    pub fn not_detected(vendor: impl Into<String>) -> Self {
33        Self {
34            detected: false,
35            vendor: vendor.into(),
36            confidence: ProbeConfidence::Likely,
37        }
38    }
39}
40
41#[derive(Debug, Clone, Copy, Eq, PartialEq)]
42#[non_exhaustive]
43pub enum ProbeConfidence {
44    Definite,
45    Likely,
46}
47
48/// Opens a file and returns a SlideReader.
49pub trait DatasetReader: Send + Sync {
50    fn open(&self, path: &Path) -> Result<Box<dyn SlideReader>, WsiError>;
51}
52
53// ── Read interface ─────────────────────────────────────────────────
54
55pub struct SlideReadContext<'a> {
56    tile_cache: Option<&'a TileCache>,
57    output: TileOutputPreference,
58    max_region_pixels: u64,
59}
60
61impl<'a> SlideReadContext<'a> {
62    pub(crate) fn new(
63        tile_cache: Option<&'a TileCache>,
64        output: TileOutputPreference,
65        max_region_pixels: u64,
66    ) -> Self {
67        Self {
68            tile_cache,
69            output,
70            max_region_pixels,
71        }
72    }
73
74    pub(crate) fn tile_cache(&self) -> Option<&'a TileCache> {
75        self.tile_cache
76    }
77
78    pub fn output(&self) -> &TileOutputPreference {
79        &self.output
80    }
81
82    pub fn max_region_pixels(&self) -> u64 {
83        self.max_region_pixels
84    }
85}
86
87/// Phase-2 read interface.
88///
89/// `read_tile` is a default impl over a 1-element slice into `read_tiles`. A
90/// backend that overrides `read_tiles` automatically gets the right
91/// `read_tile` for free:
92///
93/// ```
94/// use wsi_rs::{
95///     ColorSpace, CpuTile, Dataset, SlideReader, TileOutputPreference, TilePixels, TileRequest,
96///     WsiError,
97/// };
98/// # fn _example() {
99/// struct Mock;
100/// impl SlideReader for Mock {
101///     fn dataset(&self) -> &Dataset { unimplemented!() }
102///     fn read_tiles(
103///         &self,
104///         reqs: &[TileRequest],
105///         _: TileOutputPreference,
106///     ) -> Result<Vec<TilePixels>, WsiError> {
107///         Ok(reqs
108///             .iter()
109///             .map(|_| {
110///                 TilePixels::Cpu(
111///                     CpuTile::from_u8_interleaved(1, 1, 3, ColorSpace::Rgb, vec![255, 0, 0])
112///                         .unwrap(),
113///                 )
114///             })
115///             .collect())
116///     }
117///     fn read_tile_cpu(&self, _: &TileRequest) -> Result<CpuTile, WsiError> {
118///         Ok(CpuTile::from_u8_interleaved(1, 1, 3, ColorSpace::Rgb, vec![255, 0, 0]).unwrap())
119///     }
120///     fn read_associated(&self, name: &str) -> Result<CpuTile, WsiError> {
121///         Err(WsiError::AssociatedImageNotFound(name.into()))
122///     }
123/// }
124/// let m = Mock;
125/// let _ = m.read_tile(
126///     &TileRequest::new(0usize, 0usize, 0, 0, 0),
127///     TileOutputPreference::cpu(),
128/// );
129/// # }
130/// ```
131pub trait SlideReader: Send + Sync {
132    fn dataset(&self) -> &Dataset;
133    fn tile_codec_kind(&self, _req: &TileRequest) -> TileCodecKind {
134        TileCodecKind::Other
135    }
136    fn level_source_kind(
137        &self,
138        scene: SceneId,
139        series: SeriesId,
140        level: LevelIdx,
141    ) -> Result<LevelSourceKind, WsiError> {
142        let dataset = self.dataset();
143        let scene_ref = dataset
144            .scenes
145            .get(scene.get())
146            .ok_or(WsiError::SceneOutOfRange {
147                index: scene.get(),
148                count: dataset.scenes.len(),
149            })?;
150        let series_ref = scene_ref
151            .series
152            .get(series.get())
153            .ok_or(WsiError::SeriesOutOfRange {
154                index: series.get(),
155                count: scene_ref.series.len(),
156            })?;
157        if level.get() as usize >= series_ref.levels.len() {
158            return Err(WsiError::LevelOutOfRange {
159                level: level.get(),
160                count: series_ref.levels.len() as u32,
161            });
162        }
163        Ok(LevelSourceKind::Physical)
164    }
165    fn read_tiles(
166        &self,
167        reqs: &[TileRequest],
168        output: TileOutputPreference,
169    ) -> Result<Vec<TilePixels>, WsiError> {
170        if matches!(output, TileOutputPreference::RequireDevice { .. }) {
171            return Err(WsiError::Unsupported {
172                reason: "RequireDevice not supported by this reader in Phase 2".into(),
173            });
174        }
175        reqs.iter()
176            .map(|req| self.read_tile_cpu(req).map(TilePixels::Cpu))
177            .collect()
178    }
179    fn read_tile(
180        &self,
181        req: &TileRequest,
182        output: TileOutputPreference,
183    ) -> Result<TilePixels, WsiError> {
184        let mut tiles = self.read_tiles(std::slice::from_ref(req), output)?;
185        match tiles.len() {
186            1 => Ok(tiles.remove(0)),
187            0 => Err(WsiError::TileRead {
188                col: req.col,
189                row: req.row,
190                level: req.level.get(),
191                reason: "empty tile batch result".into(),
192            }),
193            count => Err(WsiError::TileRead {
194                col: req.col,
195                row: req.row,
196                level: req.level.get(),
197                reason: format!("single tile read returned {count} tiles"),
198            }),
199        }
200    }
201    fn read_tile_cpu(&self, req: &TileRequest) -> Result<CpuTile, WsiError>;
202    fn read_raw_compressed_tile(&self, req: &TileRequest) -> Result<RawCompressedTile, WsiError> {
203        Err(WsiError::Unsupported {
204            reason: format!(
205                "raw compressed tile access is not available for tile ({}, {}) at level {}",
206                req.col,
207                req.row,
208                req.level.get()
209            ),
210        })
211    }
212    fn read_raw_compressed_display_tile(
213        &self,
214        req: &TileViewRequest,
215    ) -> Result<RawCompressedTile, WsiError> {
216        Err(WsiError::Unsupported {
217            reason: format!(
218                "raw compressed display tile access is not available for tile ({}, {}) at level {}",
219                req.col,
220                req.row,
221                req.level.get()
222            ),
223        })
224    }
225    fn read_tiles_cpu(&self, reqs: &[TileRequest]) -> Result<Vec<CpuTile>, WsiError> {
226        self.read_tiles(reqs, TileOutputPreference::cpu())?
227            .into_iter()
228            .map(|tile| match tile {
229                TilePixels::Cpu(cpu) => Ok(cpu),
230                TilePixels::Device(_) => Err(WsiError::Unsupported {
231                    reason: "CPU tile request returned device payload".into(),
232                }),
233            })
234            .collect()
235    }
236    fn use_display_tile_cache(&self, _req: &TileViewRequest) -> bool {
237        true
238    }
239    fn read_region_fastpath(
240        &self,
241        _ctx: &mut SlideReadContext<'_>,
242        _req: &RegionRequest,
243    ) -> Option<Result<CpuTile, WsiError>> {
244        None
245    }
246    fn read_region(
247        &self,
248        req: &RegionRequest,
249        output: TileOutputPreference,
250    ) -> Result<TilePixels, WsiError> {
251        if matches!(output, TileOutputPreference::RequireDevice { .. }) {
252            return Err(WsiError::Unsupported {
253                reason: "region requires CPU composition; RequireDevice not supported in Phase 2"
254                    .into(),
255            });
256        }
257        composite_region_from_source(self, None, req, DEFAULT_MAX_REGION_PIXELS)
258            .map(TilePixels::Cpu)
259    }
260    fn read_display_tile(&self, req: &TileViewRequest) -> Result<CpuTile, WsiError> {
261        read_display_tile_from_source(self, None, req, TileOutputPreference::cpu())
262    }
263    fn associated_image(&self, name: &str) -> Result<Option<CpuTile>, WsiError> {
264        match self.read_associated(name) {
265            Ok(tile) => Ok(Some(tile)),
266            Err(WsiError::AssociatedImageNotFound(_)) => Ok(None),
267            Err(err) => Err(err),
268        }
269    }
270    fn read_associated(&self, name: &str) -> Result<CpuTile, WsiError>;
271    fn recommended_shared_cache_bytes(&self) -> Option<u64> {
272        None
273    }
274}