1use async_trait::async_trait;
29use bytes::Bytes;
30use std::collections::HashMap;
31
32use crate::error::TiffError;
33use crate::io::RangeReader;
34use crate::slide::SlideReader;
35
36use super::jpeg::prepare_tile_jpeg;
37use super::tiff::{
38 validate_pyramid, PyramidLevel, TiffHeader, TiffPyramid, TiffTag, TileData, ValueReader,
39};
40
41#[derive(Debug, Clone, Default)]
50pub struct SvsMetadata {
51 pub mpp: Option<f64>,
53
54 pub magnification: Option<f64>,
56
57 pub vendor: Option<String>,
59
60 pub image_description: Option<String>,
62
63 pub properties: HashMap<String, String>,
65}
66
67impl SvsMetadata {
68 pub fn parse(description: &str) -> Self {
79 let mut metadata = SvsMetadata {
80 image_description: Some(description.to_string()),
81 ..Default::default()
82 };
83
84 if description.contains("Aperio") {
86 metadata.vendor = Some("Aperio".to_string());
87 }
88
89 for part in description.split('|') {
91 let part = part.trim();
92
93 if let Some(eq_pos) = part.find('=') {
95 let key = part[..eq_pos].trim();
96 let value = part[eq_pos + 1..].trim();
97
98 metadata
100 .properties
101 .insert(key.to_string(), value.to_string());
102
103 match key {
105 "MPP" => {
106 if let Ok(mpp) = value.parse::<f64>() {
107 metadata.mpp = Some(mpp);
108 }
109 }
110 "AppMag" => {
111 if let Ok(mag) = value.parse::<f64>() {
112 metadata.magnification = Some(mag);
113 }
114 }
115 _ => {}
116 }
117 }
118 }
119
120 metadata
121 }
122}
123
124#[derive(Debug, Clone)]
133pub struct SvsLevelData {
134 pub level: PyramidLevel,
136
137 pub tile_data: TileData,
139}
140
141impl SvsLevelData {
142 pub fn get_tile_location(&self, tile_x: u32, tile_y: u32) -> Option<(u64, u64)> {
144 let tile_index = self.level.tile_index(tile_x, tile_y)?;
145 self.tile_data.get_tile_location(tile_index)
146 }
147
148 pub fn jpeg_tables(&self) -> Option<&Bytes> {
150 self.tile_data.jpeg_tables.as_ref()
151 }
152}
153
154#[derive(Debug)]
163pub struct SvsReader {
164 pyramid: TiffPyramid,
166
167 levels: Vec<SvsLevelData>,
169
170 metadata: SvsMetadata,
172}
173
174impl SvsReader {
175 pub async fn open<R: RangeReader>(reader: &R) -> Result<Self, TiffError> {
180 let pyramid = TiffPyramid::parse(reader).await?;
182
183 let validation = validate_pyramid(&pyramid);
185 if !validation.is_valid {
186 return Err(validation.into_result().unwrap_err());
187 }
188
189 let mut levels = Vec::with_capacity(pyramid.levels.len());
191 for level in &pyramid.levels {
192 let tile_data = TileData::load(reader, level, &pyramid.header).await?;
193 levels.push(SvsLevelData {
194 level: level.clone(),
195 tile_data,
196 });
197 }
198
199 let metadata = Self::parse_metadata(reader, &pyramid).await?;
201
202 Ok(SvsReader {
203 pyramid,
204 levels,
205 metadata,
206 })
207 }
208
209 async fn parse_metadata<R: RangeReader>(
211 reader: &R,
212 pyramid: &TiffPyramid,
213 ) -> Result<SvsMetadata, TiffError> {
214 let first_level = match pyramid.levels.first() {
216 Some(level) => level,
217 None => return Ok(SvsMetadata::default()),
218 };
219
220 let entry = match first_level.ifd.get_entry_by_tag(TiffTag::ImageDescription) {
222 Some(e) => e,
223 None => return Ok(SvsMetadata::default()),
224 };
225
226 let value_reader = ValueReader::new(reader, &pyramid.header);
228 let description = value_reader.read_string(entry).await?;
229
230 Ok(SvsMetadata::parse(&description))
231 }
232
233 pub fn header(&self) -> &TiffHeader {
235 &self.pyramid.header
236 }
237
238 pub fn metadata(&self) -> &SvsMetadata {
240 &self.metadata
241 }
242
243 pub fn level_count(&self) -> usize {
245 self.levels.len()
246 }
247
248 pub fn get_level(&self, level: usize) -> Option<&SvsLevelData> {
250 self.levels.get(level)
251 }
252
253 pub fn dimensions(&self) -> Option<(u32, u32)> {
255 self.levels.first().map(|l| (l.level.width, l.level.height))
256 }
257
258 pub fn level_dimensions(&self, level: usize) -> Option<(u32, u32)> {
260 self.levels
261 .get(level)
262 .map(|l| (l.level.width, l.level.height))
263 }
264
265 pub fn level_downsample(&self, level: usize) -> Option<f64> {
267 self.levels.get(level).map(|l| l.level.downsample)
268 }
269
270 pub fn tile_size(&self, level: usize) -> Option<(u32, u32)> {
272 self.levels
273 .get(level)
274 .map(|l| (l.level.tile_width, l.level.tile_height))
275 }
276
277 pub fn tile_count(&self, level: usize) -> Option<(u32, u32)> {
279 self.levels
280 .get(level)
281 .map(|l| (l.level.tiles_x, l.level.tiles_y))
282 }
283
284 pub async fn read_raw_tile<R: RangeReader>(
289 &self,
290 reader: &R,
291 level: usize,
292 tile_x: u32,
293 tile_y: u32,
294 ) -> Result<Bytes, TiffError> {
295 let level_data = self.levels.get(level).ok_or(TiffError::InvalidTagValue {
296 tag: "level",
297 message: format!("level {} out of range (max {})", level, self.levels.len()),
298 })?;
299
300 let (offset, size) =
301 level_data
302 .get_tile_location(tile_x, tile_y)
303 .ok_or(TiffError::InvalidTagValue {
304 tag: "tile",
305 message: format!(
306 "tile ({}, {}) out of range for level {}",
307 tile_x, tile_y, level
308 ),
309 })?;
310
311 let data = reader.read_exact_at(offset, size as usize).await?;
312 Ok(data)
313 }
314
315 pub 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 let raw_data = self.read_raw_tile(reader, level, tile_x, tile_y).await?;
337
338 let level_data = self.levels.get(level).ok_or(TiffError::InvalidTagValue {
340 tag: "level",
341 message: format!("level {} out of range", level),
342 })?;
343
344 let tables = level_data.jpeg_tables();
345
346 let jpeg_data = prepare_tile_jpeg(tables.map(|t| t.as_ref()), &raw_data);
348
349 Ok(jpeg_data)
350 }
351
352 pub fn best_level_for_downsample(&self, downsample: f64) -> Option<usize> {
356 self.pyramid
357 .best_level_for_downsample(downsample)
358 .map(|l| l.level_index)
359 }
360}
361
362#[async_trait]
367impl SlideReader for SvsReader {
368 fn level_count(&self) -> usize {
369 self.levels.len()
370 }
371
372 fn dimensions(&self) -> Option<(u32, u32)> {
373 self.levels.first().map(|l| (l.level.width, l.level.height))
374 }
375
376 fn level_dimensions(&self, level: usize) -> Option<(u32, u32)> {
377 self.levels
378 .get(level)
379 .map(|l| (l.level.width, l.level.height))
380 }
381
382 fn level_downsample(&self, level: usize) -> Option<f64> {
383 self.levels.get(level).map(|l| l.level.downsample)
384 }
385
386 fn tile_size(&self, level: usize) -> Option<(u32, u32)> {
387 self.levels
388 .get(level)
389 .map(|l| (l.level.tile_width, l.level.tile_height))
390 }
391
392 fn tile_count(&self, level: usize) -> Option<(u32, u32)> {
393 self.levels
394 .get(level)
395 .map(|l| (l.level.tiles_x, l.level.tiles_y))
396 }
397
398 fn best_level_for_downsample(&self, downsample: f64) -> Option<usize> {
399 SvsReader::best_level_for_downsample(self, downsample)
400 }
401
402 async fn read_tile<R: RangeReader>(
403 &self,
404 reader: &R,
405 level: usize,
406 tile_x: u32,
407 tile_y: u32,
408 ) -> Result<Bytes, TiffError> {
409 SvsReader::read_tile(self, reader, level, tile_x, tile_y).await
410 }
411}
412
413#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
426 fn test_parse_metadata_basic() {
427 let description = "Aperio Image Library v12.0.15\n46920x33600 (256x256) JPEG/RGB Q=70|AppMag = 20|MPP = 0.499";
428
429 let metadata = SvsMetadata::parse(description);
430
431 assert_eq!(metadata.vendor, Some("Aperio".to_string()));
432 assert!((metadata.mpp.unwrap() - 0.499).abs() < 0.001);
433 assert!((metadata.magnification.unwrap() - 20.0).abs() < 0.1);
434 }
435
436 #[test]
437 fn test_parse_metadata_with_many_fields() {
438 let description = "Aperio Image Library v12.0.15\n\
439 46920x33600 (256x256) JPEG/RGB Q=70|\
440 AppMag = 40|\
441 StripeWidth = 2040|\
442 ScanScope ID = SS1234|\
443 Filename = test.svs|\
444 MPP = 0.25|\
445 Left = 25.5|\
446 Top = 18.2";
447
448 let metadata = SvsMetadata::parse(description);
449
450 assert_eq!(metadata.vendor, Some("Aperio".to_string()));
451 assert!((metadata.mpp.unwrap() - 0.25).abs() < 0.001);
452 assert!((metadata.magnification.unwrap() - 40.0).abs() < 0.1);
453 assert_eq!(
454 metadata.properties.get("Filename"),
455 Some(&"test.svs".to_string())
456 );
457 assert_eq!(
458 metadata.properties.get("StripeWidth"),
459 Some(&"2040".to_string())
460 );
461 }
462
463 #[test]
464 fn test_parse_metadata_no_mpp() {
465 let description = "Aperio Image Library v12.0.15\n46920x33600|AppMag = 20";
466
467 let metadata = SvsMetadata::parse(description);
468
469 assert_eq!(metadata.vendor, Some("Aperio".to_string()));
470 assert!(metadata.mpp.is_none());
471 assert!((metadata.magnification.unwrap() - 20.0).abs() < 0.1);
472 }
473
474 #[test]
475 fn test_parse_metadata_empty() {
476 let metadata = SvsMetadata::parse("");
477
478 assert!(metadata.vendor.is_none());
479 assert!(metadata.mpp.is_none());
480 assert!(metadata.magnification.is_none());
481 }
482
483 #[test]
484 fn test_parse_metadata_non_aperio() {
485 let description = "Generic TIFF image\nSome other format";
486
487 let metadata = SvsMetadata::parse(description);
488
489 assert!(metadata.vendor.is_none());
490 }
491
492 #[test]
493 fn test_parse_metadata_invalid_mpp() {
494 let description = "Aperio Image Library|MPP = invalid|AppMag = 20";
495
496 let metadata = SvsMetadata::parse(description);
497
498 assert!(metadata.mpp.is_none()); assert!((metadata.magnification.unwrap() - 20.0).abs() < 0.1);
500 }
501
502 #[test]
503 fn test_parse_metadata_whitespace() {
504 let description = "Aperio Image Library | MPP = 0.5 | AppMag = 40 ";
505
506 let metadata = SvsMetadata::parse(description);
507
508 assert!((metadata.mpp.unwrap() - 0.5).abs() < 0.001);
509 assert!((metadata.magnification.unwrap() - 40.0).abs() < 0.1);
510 }
511}