1use std::sync::Arc;
30
31use bytes::Bytes;
32
33use crate::error::TileError;
34use crate::slide::{SlideRegistry, SlideSource};
35
36use super::cache::{TileCache, TileCacheKey};
37use super::encoder::{is_valid_quality, JpegTileEncoder, DEFAULT_JPEG_QUALITY};
38
39#[derive(Debug, Clone)]
47pub struct TileRequest {
48 pub slide_id: String,
50
51 pub level: usize,
53
54 pub tile_x: u32,
56
57 pub tile_y: u32,
59
60 pub quality: u8,
62}
63
64impl TileRequest {
65 pub fn new(slide_id: impl Into<String>, level: usize, tile_x: u32, tile_y: u32) -> Self {
67 Self {
68 slide_id: slide_id.into(),
69 level,
70 tile_x,
71 tile_y,
72 quality: DEFAULT_JPEG_QUALITY,
73 }
74 }
75
76 pub fn with_quality(
78 slide_id: impl Into<String>,
79 level: usize,
80 tile_x: u32,
81 tile_y: u32,
82 quality: u8,
83 ) -> Self {
84 Self {
85 slide_id: slide_id.into(),
86 level,
87 tile_x,
88 tile_y,
89 quality,
90 }
91 }
92}
93
94#[derive(Debug, Clone)]
100pub struct TileResponse {
101 pub data: Bytes,
103
104 pub cache_hit: bool,
106
107 pub quality: u8,
109}
110
111pub struct TileService<S: SlideSource> {
146 registry: Arc<SlideRegistry<S>>,
148
149 cache: TileCache,
151
152 encoder: JpegTileEncoder,
154}
155
156impl<S: SlideSource> TileService<S> {
157 pub fn new(registry: SlideRegistry<S>) -> Self {
161 Self {
162 registry: Arc::new(registry),
163 cache: TileCache::new(),
164 encoder: JpegTileEncoder::new(),
165 }
166 }
167
168 pub fn with_shared_registry(registry: Arc<SlideRegistry<S>>) -> Self {
172 Self {
173 registry,
174 cache: TileCache::new(),
175 encoder: JpegTileEncoder::new(),
176 }
177 }
178
179 pub fn with_cache_capacity(registry: SlideRegistry<S>, cache_capacity: usize) -> Self {
186 Self {
187 registry: Arc::new(registry),
188 cache: TileCache::with_capacity(cache_capacity),
189 encoder: JpegTileEncoder::new(),
190 }
191 }
192
193 pub async fn get_tile(&self, request: TileRequest) -> Result<TileResponse, TileError> {
209 if !is_valid_quality(request.quality) {
211 return Err(TileError::InvalidQuality {
212 quality: request.quality,
213 });
214 }
215 let quality = request.quality;
216
217 let cache_key = TileCacheKey::new(
219 request.slide_id.as_str(),
220 request.level as u32,
221 request.tile_x,
222 request.tile_y,
223 quality,
224 );
225
226 if let Some(cached_data) = self.cache.get(&cache_key).await {
228 return Ok(TileResponse {
229 data: cached_data,
230 cache_hit: true,
231 quality,
232 });
233 }
234
235 let tile_data = self.generate_tile(&request, quality).await?;
237
238 self.cache.put(cache_key, tile_data.clone()).await;
240
241 Ok(TileResponse {
242 data: tile_data,
243 cache_hit: false,
244 quality,
245 })
246 }
247
248 pub async fn generate_tile(
252 &self,
253 request: &TileRequest,
254 quality: u8,
255 ) -> Result<Bytes, TileError> {
256 let slide = self
258 .registry
259 .get_slide(&request.slide_id)
260 .await
261 .map_err(|e| match e {
262 crate::error::FormatError::Io(io_err) => {
263 if matches!(io_err, crate::error::IoError::NotFound(_)) {
264 TileError::SlideNotFound {
265 slide_id: request.slide_id.clone(),
266 }
267 } else {
268 TileError::Io(io_err)
269 }
270 }
271 crate::error::FormatError::Tiff(tiff_err) => TileError::Slide(tiff_err),
272 crate::error::FormatError::UnsupportedFormat { reason } => {
273 TileError::Slide(crate::error::TiffError::InvalidTagValue {
274 tag: "Format",
275 message: reason,
276 })
277 }
278 })?;
279
280 let level_count = slide.level_count();
282 if request.level >= level_count {
283 return Err(TileError::InvalidLevel {
284 level: request.level,
285 max_levels: level_count,
286 });
287 }
288
289 let (max_x, max_y) = slide
291 .tile_count(request.level)
292 .ok_or(TileError::InvalidLevel {
293 level: request.level,
294 max_levels: level_count,
295 })?;
296
297 if request.tile_x >= max_x || request.tile_y >= max_y {
298 return Err(TileError::TileOutOfBounds {
299 level: request.level,
300 x: request.tile_x,
301 y: request.tile_y,
302 max_x,
303 max_y,
304 });
305 }
306
307 let raw_tile = slide
309 .read_tile(request.level, request.tile_x, request.tile_y)
310 .await?;
311
312 let encoded_tile = self.encoder.encode(&raw_tile, quality)?;
314
315 Ok(encoded_tile)
316 }
317
318 pub async fn cache_stats(&self) -> (usize, usize, usize) {
322 let size = self.cache.size().await;
323 let capacity = self.cache.capacity();
324 let count = self.cache.len().await;
325 (size, capacity, count)
326 }
327
328 pub async fn clear_cache(&self) {
330 self.cache.clear().await;
331 }
332
333 pub async fn invalidate_slide(&self, _slide_id: &str) {
338 }
343
344 pub fn registry(&self) -> &Arc<SlideRegistry<S>> {
346 &self.registry
347 }
348
349 pub async fn generate_thumbnail(
364 &self,
365 slide_id: &str,
366 max_dimension: u32,
367 quality: u8,
368 ) -> Result<TileResponse, TileError> {
369 if !is_valid_quality(quality) {
371 return Err(TileError::InvalidQuality { quality });
372 }
373
374 let slide = self
376 .registry
377 .get_slide(slide_id)
378 .await
379 .map_err(|e| match e {
380 crate::error::FormatError::Io(io_err) => {
381 if matches!(io_err, crate::error::IoError::NotFound(_)) {
382 TileError::SlideNotFound {
383 slide_id: slide_id.to_string(),
384 }
385 } else {
386 TileError::Io(io_err)
387 }
388 }
389 crate::error::FormatError::Tiff(tiff_err) => TileError::Slide(tiff_err),
390 crate::error::FormatError::UnsupportedFormat { reason } => {
391 TileError::Slide(crate::error::TiffError::InvalidTagValue {
392 tag: "Format",
393 message: reason,
394 })
395 }
396 })?;
397
398 let (full_width, full_height) = slide.dimensions().ok_or(TileError::InvalidLevel {
399 level: 0,
400 max_levels: 0,
401 })?;
402
403 let max_dim = full_width.max(full_height);
405 let downsample = max_dim as f64 / max_dimension as f64;
406
407 let level = slide
409 .best_level_for_downsample(downsample)
410 .unwrap_or(slide.level_count().saturating_sub(1));
411
412 let info = slide.level_info(level).ok_or(TileError::InvalidLevel {
413 level,
414 max_levels: slide.level_count(),
415 })?;
416
417 if info.tiles_x == 1 && info.tiles_y == 1 {
419 let request = TileRequest::with_quality(slide_id, level, 0, 0, quality);
420 return self.get_tile(request).await;
421 }
422
423 let lowest_level = slide.level_count().saturating_sub(1);
427 let request = TileRequest::with_quality(slide_id, lowest_level, 0, 0, quality);
428 self.get_tile(request).await
429 }
430}
431
432#[cfg(test)]
437mod tests {
438 use super::*;
439 use crate::error::IoError;
440 use crate::io::RangeReader;
441 use crate::slide::SlideSource;
442 use async_trait::async_trait;
443 use image::codecs::jpeg::JpegEncoder;
444 use image::{GrayImage, Luma};
445
446 fn create_test_jpeg() -> Vec<u8> {
448 let img = GrayImage::from_fn(256, 256, |x, y| {
449 let val = ((x + y) % 256) as u8;
450 Luma([val])
451 });
452
453 let mut buf = Vec::new();
454 let mut encoder = JpegEncoder::new_with_quality(&mut buf, 90);
455 encoder.encode_image(&img).unwrap();
456 buf
457 }
458
459 fn create_tiff_with_jpeg_tile() -> Vec<u8> {
461 let jpeg_data = create_test_jpeg();
462 let jpeg_len = jpeg_data.len() as u32;
463
464 let tile_data_offset = 1000u32;
466 let total_size = tile_data_offset as usize + jpeg_data.len() + 100;
467 let mut data = vec![0u8; total_size];
468
469 data[0] = 0x49; data[1] = 0x49; data[2] = 0x2A; data[3] = 0x00;
474 data[4] = 0x08; data[5] = 0x00;
476 data[6] = 0x00;
477 data[7] = 0x00;
478
479 data[8] = 0x08;
482 data[9] = 0x00;
483
484 let mut offset = 10;
485
486 let write_entry =
488 |data: &mut [u8], offset: &mut usize, tag: u16, typ: u16, count: u32, value: u32| {
489 data[*offset..*offset + 2].copy_from_slice(&tag.to_le_bytes());
490 data[*offset + 2..*offset + 4].copy_from_slice(&typ.to_le_bytes());
491 data[*offset + 4..*offset + 8].copy_from_slice(&count.to_le_bytes());
492 data[*offset + 8..*offset + 12].copy_from_slice(&value.to_le_bytes());
493 *offset += 12;
494 };
495
496 write_entry(&mut data, &mut offset, 256, 4, 1, 2048);
498
499 write_entry(&mut data, &mut offset, 257, 4, 1, 1536);
501
502 write_entry(&mut data, &mut offset, 259, 3, 1, 7);
504
505 write_entry(&mut data, &mut offset, 322, 3, 1, 256);
507
508 write_entry(&mut data, &mut offset, 323, 3, 1, 256);
510
511 write_entry(&mut data, &mut offset, 324, 4, 48, 200);
514
515 write_entry(&mut data, &mut offset, 325, 4, 48, 600);
517
518 write_entry(&mut data, &mut offset, 258, 3, 1, 8);
520
521 data[offset..offset + 4].copy_from_slice(&0u32.to_le_bytes());
523
524 for i in 0..48u32 {
526 let arr_offset = 200 + (i as usize) * 4;
527 data[arr_offset..arr_offset + 4].copy_from_slice(&tile_data_offset.to_le_bytes());
528 }
529
530 for i in 0..48u32 {
532 let arr_offset = 600 + (i as usize) * 4;
533 data[arr_offset..arr_offset + 4].copy_from_slice(&jpeg_len.to_le_bytes());
534 }
535
536 data[tile_data_offset as usize..tile_data_offset as usize + jpeg_data.len()]
538 .copy_from_slice(&jpeg_data);
539
540 data
541 }
542
543 struct MockReader {
545 data: Bytes,
546 identifier: String,
547 }
548
549 #[async_trait]
550 impl RangeReader for MockReader {
551 async fn read_exact_at(&self, offset: u64, len: usize) -> Result<Bytes, IoError> {
552 let start = offset as usize;
553 let end = start + len;
554 if end > self.data.len() {
555 return Err(IoError::RangeOutOfBounds {
556 offset,
557 requested: len as u64,
558 size: self.data.len() as u64,
559 });
560 }
561 Ok(self.data.slice(start..end))
562 }
563
564 fn size(&self) -> u64 {
565 self.data.len() as u64
566 }
567
568 fn identifier(&self) -> &str {
569 &self.identifier
570 }
571 }
572
573 struct MockSlideSource {
575 data: Bytes,
576 }
577
578 impl MockSlideSource {
579 fn new(data: Vec<u8>) -> Self {
580 Self {
581 data: Bytes::from(data),
582 }
583 }
584 }
585
586 #[async_trait]
587 impl SlideSource for MockSlideSource {
588 type Reader = MockReader;
589
590 async fn create_reader(&self, slide_id: &str) -> Result<Self::Reader, IoError> {
591 if slide_id.contains("notfound") {
592 return Err(IoError::NotFound(slide_id.to_string()));
593 }
594 Ok(MockReader {
595 data: self.data.clone(),
596 identifier: format!("mock://{}", slide_id),
597 })
598 }
599 }
600
601 #[tokio::test]
602 async fn test_tile_request_creation() {
603 let request = TileRequest::new("test.svs", 0, 1, 2);
604 assert_eq!(request.slide_id, "test.svs");
605 assert_eq!(request.level, 0);
606 assert_eq!(request.tile_x, 1);
607 assert_eq!(request.tile_y, 2);
608 assert_eq!(request.quality, DEFAULT_JPEG_QUALITY);
609
610 let request_q = TileRequest::with_quality("test.svs", 1, 3, 4, 95);
611 assert_eq!(request_q.quality, 95);
612 }
613
614 #[tokio::test]
615 async fn test_get_tile_success() {
616 let tiff_data = create_tiff_with_jpeg_tile();
617 let source = MockSlideSource::new(tiff_data);
618 let registry = SlideRegistry::new(source);
619 let service = TileService::new(registry);
620
621 let request = TileRequest::new("test.tif", 0, 0, 0);
622 let response = service.get_tile(request).await;
623
624 assert!(response.is_ok());
625 let response = response.unwrap();
626
627 assert!(!response.cache_hit);
629 assert_eq!(response.quality, DEFAULT_JPEG_QUALITY);
630
631 assert!(response.data.len() > 2);
633 assert_eq!(response.data[0], 0xFF);
634 assert_eq!(response.data[1], 0xD8);
635 }
636
637 #[tokio::test]
638 async fn test_get_tile_cache_hit() {
639 let tiff_data = create_tiff_with_jpeg_tile();
640 let source = MockSlideSource::new(tiff_data);
641 let registry = SlideRegistry::new(source);
642 let service = TileService::new(registry);
643
644 let request = TileRequest::new("test.tif", 0, 0, 0);
645
646 let response1 = service.get_tile(request.clone()).await.unwrap();
648 assert!(!response1.cache_hit);
649
650 let response2 = service.get_tile(request).await.unwrap();
652 assert!(response2.cache_hit);
653 assert_eq!(response1.data, response2.data);
654 }
655
656 #[tokio::test]
657 async fn test_different_quality_different_cache() {
658 let tiff_data = create_tiff_with_jpeg_tile();
659 let source = MockSlideSource::new(tiff_data);
660 let registry = SlideRegistry::new(source);
661 let service = TileService::new(registry);
662
663 let request_q80 = TileRequest::with_quality("test.tif", 0, 0, 0, 80);
664 let request_q95 = TileRequest::with_quality("test.tif", 0, 0, 0, 95);
665
666 let response1 = service.get_tile(request_q80.clone()).await.unwrap();
668 assert!(!response1.cache_hit);
669
670 let response2 = service.get_tile(request_q95).await.unwrap();
672 assert!(!response2.cache_hit);
673
674 let response3 = service.get_tile(request_q80).await.unwrap();
676 assert!(response3.cache_hit);
677 }
678
679 #[tokio::test]
680 async fn test_invalid_level() {
681 let tiff_data = create_tiff_with_jpeg_tile();
682 let source = MockSlideSource::new(tiff_data);
683 let registry = SlideRegistry::new(source);
684 let service = TileService::new(registry);
685
686 let request = TileRequest::new("test.tif", 5, 0, 0);
688 let result = service.get_tile(request).await;
689
690 assert!(result.is_err());
691 match result.unwrap_err() {
692 TileError::InvalidLevel { level, max_levels } => {
693 assert_eq!(level, 5);
694 assert_eq!(max_levels, 1);
695 }
696 e => panic!("Expected InvalidLevel error, got {:?}", e),
697 }
698 }
699
700 #[tokio::test]
701 async fn test_tile_out_of_bounds() {
702 let tiff_data = create_tiff_with_jpeg_tile();
703 let source = MockSlideSource::new(tiff_data);
704 let registry = SlideRegistry::new(source);
705 let service = TileService::new(registry);
706
707 let request = TileRequest::new("test.tif", 0, 100, 100);
709 let result = service.get_tile(request).await;
710
711 assert!(result.is_err());
712 match result.unwrap_err() {
713 TileError::TileOutOfBounds {
714 level,
715 x,
716 y,
717 max_x,
718 max_y,
719 } => {
720 assert_eq!(level, 0);
721 assert_eq!(x, 100);
722 assert_eq!(y, 100);
723 assert_eq!(max_x, 8);
724 assert_eq!(max_y, 6);
725 }
726 e => panic!("Expected TileOutOfBounds error, got {:?}", e),
727 }
728 }
729
730 #[tokio::test]
731 async fn test_slide_not_found() {
732 let tiff_data = create_tiff_with_jpeg_tile();
733 let source = MockSlideSource::new(tiff_data);
734 let registry = SlideRegistry::new(source);
735 let service = TileService::new(registry);
736
737 let request = TileRequest::new("notfound.tif", 0, 0, 0);
738 let result = service.get_tile(request).await;
739
740 assert!(result.is_err());
741 match result.unwrap_err() {
742 TileError::SlideNotFound { slide_id } => {
743 assert_eq!(slide_id, "notfound.tif");
744 }
745 e => panic!("Expected SlideNotFound error, got {:?}", e),
746 }
747 }
748
749 #[tokio::test]
750 async fn test_cache_stats() {
751 let tiff_data = create_tiff_with_jpeg_tile();
752 let source = MockSlideSource::new(tiff_data);
753 let registry = SlideRegistry::new(source);
754 let service = TileService::with_cache_capacity(registry, 10 * 1024 * 1024); let (size, capacity, count) = service.cache_stats().await;
757 assert_eq!(size, 0);
758 assert_eq!(capacity, 10 * 1024 * 1024);
759 assert_eq!(count, 0);
760
761 let request = TileRequest::new("test.tif", 0, 0, 0);
763 service.get_tile(request).await.unwrap();
764
765 let (size, _, count) = service.cache_stats().await;
766 assert!(size > 0);
767 assert_eq!(count, 1);
768 }
769
770 #[tokio::test]
771 async fn test_clear_cache() {
772 let tiff_data = create_tiff_with_jpeg_tile();
773 let source = MockSlideSource::new(tiff_data);
774 let registry = SlideRegistry::new(source);
775 let service = TileService::new(registry);
776
777 service
779 .get_tile(TileRequest::new("test.tif", 0, 0, 0))
780 .await
781 .unwrap();
782 service
783 .get_tile(TileRequest::new("test.tif", 0, 1, 0))
784 .await
785 .unwrap();
786
787 let (_, _, count) = service.cache_stats().await;
788 assert_eq!(count, 2);
789
790 service.clear_cache().await;
792
793 let (size, _, count) = service.cache_stats().await;
794 assert_eq!(size, 0);
795 assert_eq!(count, 0);
796 }
797
798 #[tokio::test]
799 async fn test_quality_validation() {
800 let tiff_data = create_tiff_with_jpeg_tile();
801 let source = MockSlideSource::new(tiff_data);
802 let registry = SlideRegistry::new(source);
803 let service = TileService::new(registry);
804
805 let request = TileRequest::with_quality("test.tif", 0, 0, 0, 0);
807 let result = service.get_tile(request).await;
808 assert!(matches!(
809 result,
810 Err(TileError::InvalidQuality { quality: 0 })
811 ));
812
813 let request = TileRequest::with_quality("test.tif", 0, 1, 0, 255);
815 let result = service.get_tile(request).await;
816 assert!(matches!(
817 result,
818 Err(TileError::InvalidQuality { quality: 255 })
819 ));
820 }
821}