1use std::sync::Arc;
30
31use bytes::Bytes;
32use image::codecs::jpeg::JpegEncoder;
33use image::{DynamicImage, ImageReader, RgbImage};
34use std::io::Cursor;
35
36use crate::error::TileError;
37use crate::slide::{SlideRegistry, SlideSource};
38
39use super::cache::{TileCache, TileCacheKey};
40use super::encoder::{is_valid_quality, JpegTileEncoder, DEFAULT_JPEG_QUALITY};
41
42#[derive(Debug, Clone)]
50pub struct TileRequest {
51 pub slide_id: String,
53
54 pub level: usize,
56
57 pub tile_x: u32,
59
60 pub tile_y: u32,
62
63 pub quality: u8,
65}
66
67impl TileRequest {
68 pub fn new(slide_id: impl Into<String>, level: usize, tile_x: u32, tile_y: u32) -> Self {
70 Self {
71 slide_id: slide_id.into(),
72 level,
73 tile_x,
74 tile_y,
75 quality: DEFAULT_JPEG_QUALITY,
76 }
77 }
78
79 pub fn with_quality(
81 slide_id: impl Into<String>,
82 level: usize,
83 tile_x: u32,
84 tile_y: u32,
85 quality: u8,
86 ) -> Self {
87 Self {
88 slide_id: slide_id.into(),
89 level,
90 tile_x,
91 tile_y,
92 quality,
93 }
94 }
95}
96
97#[derive(Debug, Clone)]
103pub struct TileResponse {
104 pub data: Bytes,
106
107 pub cache_hit: bool,
109
110 pub quality: u8,
112}
113
114pub struct TileService<S: SlideSource> {
149 registry: Arc<SlideRegistry<S>>,
151
152 cache: TileCache,
154
155 encoder: JpegTileEncoder,
157}
158
159impl<S: SlideSource> TileService<S> {
160 pub fn new(registry: SlideRegistry<S>) -> Self {
164 Self {
165 registry: Arc::new(registry),
166 cache: TileCache::new(),
167 encoder: JpegTileEncoder::new(),
168 }
169 }
170
171 pub fn with_shared_registry(registry: Arc<SlideRegistry<S>>) -> Self {
175 Self {
176 registry,
177 cache: TileCache::new(),
178 encoder: JpegTileEncoder::new(),
179 }
180 }
181
182 pub fn with_cache_capacity(registry: SlideRegistry<S>, cache_capacity: usize) -> Self {
189 Self {
190 registry: Arc::new(registry),
191 cache: TileCache::with_capacity(cache_capacity),
192 encoder: JpegTileEncoder::new(),
193 }
194 }
195
196 pub async fn get_tile(&self, request: TileRequest) -> Result<TileResponse, TileError> {
212 if !is_valid_quality(request.quality) {
214 return Err(TileError::InvalidQuality {
215 quality: request.quality,
216 });
217 }
218 let quality = request.quality;
219
220 let cache_key = TileCacheKey::new(
222 request.slide_id.as_str(),
223 request.level as u32,
224 request.tile_x,
225 request.tile_y,
226 quality,
227 );
228
229 if let Some(cached_data) = self.cache.get(&cache_key).await {
231 return Ok(TileResponse {
232 data: cached_data,
233 cache_hit: true,
234 quality,
235 });
236 }
237
238 let tile_data = self.generate_tile(&request, quality).await?;
240
241 self.cache.put(cache_key, tile_data.clone()).await;
243
244 Ok(TileResponse {
245 data: tile_data,
246 cache_hit: false,
247 quality,
248 })
249 }
250
251 pub async fn generate_tile(
255 &self,
256 request: &TileRequest,
257 quality: u8,
258 ) -> Result<Bytes, TileError> {
259 let slide = self
261 .registry
262 .get_slide(&request.slide_id)
263 .await
264 .map_err(|e| match e {
265 crate::error::FormatError::Io(io_err) => {
266 if matches!(io_err, crate::error::IoError::NotFound(_)) {
267 TileError::SlideNotFound {
268 slide_id: request.slide_id.clone(),
269 }
270 } else {
271 TileError::Io(io_err)
272 }
273 }
274 crate::error::FormatError::Tiff(tiff_err) => TileError::Slide(tiff_err),
275 crate::error::FormatError::UnsupportedFormat { reason } => {
276 TileError::Slide(crate::error::TiffError::InvalidTagValue {
277 tag: "Format",
278 message: reason,
279 })
280 }
281 })?;
282
283 let level_count = slide.level_count();
285 if request.level >= level_count {
286 return Err(TileError::InvalidLevel {
287 level: request.level,
288 max_levels: level_count,
289 });
290 }
291
292 let (max_x, max_y) = slide
294 .tile_count(request.level)
295 .ok_or(TileError::InvalidLevel {
296 level: request.level,
297 max_levels: level_count,
298 })?;
299
300 if request.tile_x >= max_x || request.tile_y >= max_y {
301 return Err(TileError::TileOutOfBounds {
302 level: request.level,
303 x: request.tile_x,
304 y: request.tile_y,
305 max_x,
306 max_y,
307 });
308 }
309
310 let raw_tile = slide
312 .read_tile(request.level, request.tile_x, request.tile_y)
313 .await?;
314
315 let encoded_tile = self.encoder.encode(&raw_tile, quality)?;
317
318 Ok(encoded_tile)
319 }
320
321 pub async fn cache_stats(&self) -> (usize, usize, usize) {
325 let size = self.cache.size().await;
326 let capacity = self.cache.capacity();
327 let count = self.cache.len().await;
328 (size, capacity, count)
329 }
330
331 pub async fn clear_cache(&self) {
333 self.cache.clear().await;
334 }
335
336 pub async fn invalidate_slide(&self, _slide_id: &str) {
341 }
346
347 pub fn registry(&self) -> &Arc<SlideRegistry<S>> {
349 &self.registry
350 }
351
352 pub async fn generate_thumbnail(
367 &self,
368 slide_id: &str,
369 max_dimension: u32,
370 quality: u8,
371 ) -> Result<TileResponse, TileError> {
372 if !is_valid_quality(quality) {
374 return Err(TileError::InvalidQuality { quality });
375 }
376
377 let slide = self
379 .registry
380 .get_slide(slide_id)
381 .await
382 .map_err(|e| match e {
383 crate::error::FormatError::Io(io_err) => {
384 if matches!(io_err, crate::error::IoError::NotFound(_)) {
385 TileError::SlideNotFound {
386 slide_id: slide_id.to_string(),
387 }
388 } else {
389 TileError::Io(io_err)
390 }
391 }
392 crate::error::FormatError::Tiff(tiff_err) => TileError::Slide(tiff_err),
393 crate::error::FormatError::UnsupportedFormat { reason } => {
394 TileError::Slide(crate::error::TiffError::InvalidTagValue {
395 tag: "Format",
396 message: reason,
397 })
398 }
399 })?;
400
401 let (full_width, full_height) = slide.dimensions().ok_or(TileError::InvalidLevel {
402 level: 0,
403 max_levels: 0,
404 })?;
405
406 let max_dim = full_width.max(full_height);
408 let downsample = max_dim as f64 / max_dimension as f64;
409
410 let level = slide
412 .best_level_for_downsample(downsample)
413 .unwrap_or(slide.level_count().saturating_sub(1));
414
415 let info = slide.level_info(level).ok_or(TileError::InvalidLevel {
416 level,
417 max_levels: slide.level_count(),
418 })?;
419
420 if info.tiles_x == 1 && info.tiles_y == 1 {
423 let request = TileRequest::with_quality(slide_id, level, 0, 0, quality);
424 let tile_response = self.get_tile(request).await?;
425
426 if info.width > max_dimension || info.height > max_dimension {
428 let resized = self.resize_image(&tile_response.data, max_dimension, quality)?;
429 return Ok(TileResponse {
430 data: resized,
431 cache_hit: false,
432 quality,
433 });
434 }
435
436 return Ok(tile_response);
437 }
438
439 let composite = self
441 .composite_level_tiles(slide_id, level, &info, quality)
442 .await?;
443
444 let resized = self.resize_image(&composite, max_dimension, quality)?;
446
447 Ok(TileResponse {
448 data: resized,
449 cache_hit: false,
450 quality,
451 })
452 }
453
454 async fn composite_level_tiles(
456 &self,
457 slide_id: &str,
458 level: usize,
459 info: &crate::slide::LevelInfo,
460 quality: u8,
461 ) -> Result<Bytes, TileError> {
462 let mut canvas = RgbImage::new(info.width, info.height);
464
465 for tile_y in 0..info.tiles_y {
467 for tile_x in 0..info.tiles_x {
468 let request = TileRequest::with_quality(slide_id, level, tile_x, tile_y, quality);
469 let tile_response = self.get_tile(request).await?;
470
471 let cursor = Cursor::new(&tile_response.data[..]);
473 let reader = ImageReader::with_format(cursor, image::ImageFormat::Jpeg);
474 let tile_img = reader.decode().map_err(|e| TileError::DecodeError {
475 message: format!("Failed to decode tile ({}, {}): {}", tile_x, tile_y, e),
476 })?;
477
478 let x_pos = tile_x * info.tile_width;
480 let y_pos = tile_y * info.tile_height;
481
482 let tile_rgb = tile_img.to_rgb8();
484
485 for (ty, row) in tile_rgb.rows().enumerate() {
487 for (tx, pixel) in row.enumerate() {
488 let canvas_x = x_pos + tx as u32;
489 let canvas_y = y_pos + ty as u32;
490 if canvas_x < info.width && canvas_y < info.height {
491 canvas.put_pixel(canvas_x, canvas_y, *pixel);
492 }
493 }
494 }
495 }
496 }
497
498 let mut output = Vec::new();
500 let mut encoder = JpegEncoder::new_with_quality(&mut output, quality);
501 encoder
502 .encode_image(&DynamicImage::ImageRgb8(canvas))
503 .map_err(|e| TileError::EncodeError {
504 message: format!("Failed to encode composite: {}", e),
505 })?;
506
507 Ok(Bytes::from(output))
508 }
509
510 fn resize_image(
512 &self,
513 jpeg_data: &[u8],
514 max_dimension: u32,
515 quality: u8,
516 ) -> Result<Bytes, TileError> {
517 let cursor = Cursor::new(jpeg_data);
519 let reader = ImageReader::with_format(cursor, image::ImageFormat::Jpeg);
520 let img = reader.decode().map_err(|e| TileError::DecodeError {
521 message: format!("Failed to decode image for resize: {}", e),
522 })?;
523
524 let (width, height) = (img.width(), img.height());
525
526 let scale = max_dimension as f64 / width.max(height) as f64;
528 let new_width = (width as f64 * scale).round() as u32;
529 let new_height = (height as f64 * scale).round() as u32;
530
531 let resized =
533 img.resize_exact(new_width, new_height, image::imageops::FilterType::Lanczos3);
534
535 let mut output = Vec::new();
537 let mut encoder = JpegEncoder::new_with_quality(&mut output, quality);
538 encoder
539 .encode_image(&resized)
540 .map_err(|e| TileError::EncodeError {
541 message: format!("Failed to encode resized image: {}", e),
542 })?;
543
544 Ok(Bytes::from(output))
545 }
546}
547
548#[cfg(test)]
553mod tests {
554 use super::*;
555 use crate::error::IoError;
556 use crate::io::RangeReader;
557 use crate::slide::SlideSource;
558 use async_trait::async_trait;
559 use image::codecs::jpeg::JpegEncoder;
560 use image::{GrayImage, Luma};
561
562 fn create_test_jpeg() -> Vec<u8> {
564 let img = GrayImage::from_fn(256, 256, |x, y| {
565 let val = ((x + y) % 256) as u8;
566 Luma([val])
567 });
568
569 let mut buf = Vec::new();
570 let mut encoder = JpegEncoder::new_with_quality(&mut buf, 90);
571 encoder.encode_image(&img).unwrap();
572 buf
573 }
574
575 fn create_tiff_with_jpeg_tile() -> Vec<u8> {
577 let jpeg_data = create_test_jpeg();
578 let jpeg_len = jpeg_data.len() as u32;
579
580 let tile_data_offset = 1000u32;
582 let total_size = tile_data_offset as usize + jpeg_data.len() + 100;
583 let mut data = vec![0u8; total_size];
584
585 data[0] = 0x49; data[1] = 0x49; data[2] = 0x2A; data[3] = 0x00;
590 data[4] = 0x08; data[5] = 0x00;
592 data[6] = 0x00;
593 data[7] = 0x00;
594
595 data[8] = 0x08;
598 data[9] = 0x00;
599
600 let mut offset = 10;
601
602 let write_entry =
604 |data: &mut [u8], offset: &mut usize, tag: u16, typ: u16, count: u32, value: u32| {
605 data[*offset..*offset + 2].copy_from_slice(&tag.to_le_bytes());
606 data[*offset + 2..*offset + 4].copy_from_slice(&typ.to_le_bytes());
607 data[*offset + 4..*offset + 8].copy_from_slice(&count.to_le_bytes());
608 data[*offset + 8..*offset + 12].copy_from_slice(&value.to_le_bytes());
609 *offset += 12;
610 };
611
612 write_entry(&mut data, &mut offset, 256, 4, 1, 2048);
614
615 write_entry(&mut data, &mut offset, 257, 4, 1, 1536);
617
618 write_entry(&mut data, &mut offset, 259, 3, 1, 7);
620
621 write_entry(&mut data, &mut offset, 322, 3, 1, 256);
623
624 write_entry(&mut data, &mut offset, 323, 3, 1, 256);
626
627 write_entry(&mut data, &mut offset, 324, 4, 48, 200);
630
631 write_entry(&mut data, &mut offset, 325, 4, 48, 600);
633
634 write_entry(&mut data, &mut offset, 258, 3, 1, 8);
636
637 data[offset..offset + 4].copy_from_slice(&0u32.to_le_bytes());
639
640 for i in 0..48u32 {
642 let arr_offset = 200 + (i as usize) * 4;
643 data[arr_offset..arr_offset + 4].copy_from_slice(&tile_data_offset.to_le_bytes());
644 }
645
646 for i in 0..48u32 {
648 let arr_offset = 600 + (i as usize) * 4;
649 data[arr_offset..arr_offset + 4].copy_from_slice(&jpeg_len.to_le_bytes());
650 }
651
652 data[tile_data_offset as usize..tile_data_offset as usize + jpeg_data.len()]
654 .copy_from_slice(&jpeg_data);
655
656 data
657 }
658
659 struct MockReader {
661 data: Bytes,
662 identifier: String,
663 }
664
665 #[async_trait]
666 impl RangeReader for MockReader {
667 async fn read_exact_at(&self, offset: u64, len: usize) -> Result<Bytes, IoError> {
668 let start = offset as usize;
669 let end = start + len;
670 if end > self.data.len() {
671 return Err(IoError::RangeOutOfBounds {
672 offset,
673 requested: len as u64,
674 size: self.data.len() as u64,
675 });
676 }
677 Ok(self.data.slice(start..end))
678 }
679
680 fn size(&self) -> u64 {
681 self.data.len() as u64
682 }
683
684 fn identifier(&self) -> &str {
685 &self.identifier
686 }
687 }
688
689 struct MockSlideSource {
691 data: Bytes,
692 }
693
694 impl MockSlideSource {
695 fn new(data: Vec<u8>) -> Self {
696 Self {
697 data: Bytes::from(data),
698 }
699 }
700 }
701
702 #[async_trait]
703 impl SlideSource for MockSlideSource {
704 type Reader = MockReader;
705
706 async fn create_reader(&self, slide_id: &str) -> Result<Self::Reader, IoError> {
707 if slide_id.contains("notfound") {
708 return Err(IoError::NotFound(slide_id.to_string()));
709 }
710 Ok(MockReader {
711 data: self.data.clone(),
712 identifier: format!("mock://{}", slide_id),
713 })
714 }
715 }
716
717 #[tokio::test]
718 async fn test_tile_request_creation() {
719 let request = TileRequest::new("test.svs", 0, 1, 2);
720 assert_eq!(request.slide_id, "test.svs");
721 assert_eq!(request.level, 0);
722 assert_eq!(request.tile_x, 1);
723 assert_eq!(request.tile_y, 2);
724 assert_eq!(request.quality, DEFAULT_JPEG_QUALITY);
725
726 let request_q = TileRequest::with_quality("test.svs", 1, 3, 4, 95);
727 assert_eq!(request_q.quality, 95);
728 }
729
730 #[tokio::test]
731 async fn test_get_tile_success() {
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("test.tif", 0, 0, 0);
738 let response = service.get_tile(request).await;
739
740 assert!(response.is_ok());
741 let response = response.unwrap();
742
743 assert!(!response.cache_hit);
745 assert_eq!(response.quality, DEFAULT_JPEG_QUALITY);
746
747 assert!(response.data.len() > 2);
749 assert_eq!(response.data[0], 0xFF);
750 assert_eq!(response.data[1], 0xD8);
751 }
752
753 #[tokio::test]
754 async fn test_get_tile_cache_hit() {
755 let tiff_data = create_tiff_with_jpeg_tile();
756 let source = MockSlideSource::new(tiff_data);
757 let registry = SlideRegistry::new(source);
758 let service = TileService::new(registry);
759
760 let request = TileRequest::new("test.tif", 0, 0, 0);
761
762 let response1 = service.get_tile(request.clone()).await.unwrap();
764 assert!(!response1.cache_hit);
765
766 let response2 = service.get_tile(request).await.unwrap();
768 assert!(response2.cache_hit);
769 assert_eq!(response1.data, response2.data);
770 }
771
772 #[tokio::test]
773 async fn test_different_quality_different_cache() {
774 let tiff_data = create_tiff_with_jpeg_tile();
775 let source = MockSlideSource::new(tiff_data);
776 let registry = SlideRegistry::new(source);
777 let service = TileService::new(registry);
778
779 let request_q80 = TileRequest::with_quality("test.tif", 0, 0, 0, 80);
780 let request_q95 = TileRequest::with_quality("test.tif", 0, 0, 0, 95);
781
782 let response1 = service.get_tile(request_q80.clone()).await.unwrap();
784 assert!(!response1.cache_hit);
785
786 let response2 = service.get_tile(request_q95).await.unwrap();
788 assert!(!response2.cache_hit);
789
790 let response3 = service.get_tile(request_q80).await.unwrap();
792 assert!(response3.cache_hit);
793 }
794
795 #[tokio::test]
796 async fn test_invalid_level() {
797 let tiff_data = create_tiff_with_jpeg_tile();
798 let source = MockSlideSource::new(tiff_data);
799 let registry = SlideRegistry::new(source);
800 let service = TileService::new(registry);
801
802 let request = TileRequest::new("test.tif", 5, 0, 0);
804 let result = service.get_tile(request).await;
805
806 assert!(result.is_err());
807 match result.unwrap_err() {
808 TileError::InvalidLevel { level, max_levels } => {
809 assert_eq!(level, 5);
810 assert_eq!(max_levels, 1);
811 }
812 e => panic!("Expected InvalidLevel error, got {:?}", e),
813 }
814 }
815
816 #[tokio::test]
817 async fn test_tile_out_of_bounds() {
818 let tiff_data = create_tiff_with_jpeg_tile();
819 let source = MockSlideSource::new(tiff_data);
820 let registry = SlideRegistry::new(source);
821 let service = TileService::new(registry);
822
823 let request = TileRequest::new("test.tif", 0, 100, 100);
825 let result = service.get_tile(request).await;
826
827 assert!(result.is_err());
828 match result.unwrap_err() {
829 TileError::TileOutOfBounds {
830 level,
831 x,
832 y,
833 max_x,
834 max_y,
835 } => {
836 assert_eq!(level, 0);
837 assert_eq!(x, 100);
838 assert_eq!(y, 100);
839 assert_eq!(max_x, 8);
840 assert_eq!(max_y, 6);
841 }
842 e => panic!("Expected TileOutOfBounds error, got {:?}", e),
843 }
844 }
845
846 #[tokio::test]
847 async fn test_slide_not_found() {
848 let tiff_data = create_tiff_with_jpeg_tile();
849 let source = MockSlideSource::new(tiff_data);
850 let registry = SlideRegistry::new(source);
851 let service = TileService::new(registry);
852
853 let request = TileRequest::new("notfound.tif", 0, 0, 0);
854 let result = service.get_tile(request).await;
855
856 assert!(result.is_err());
857 match result.unwrap_err() {
858 TileError::SlideNotFound { slide_id } => {
859 assert_eq!(slide_id, "notfound.tif");
860 }
861 e => panic!("Expected SlideNotFound error, got {:?}", e),
862 }
863 }
864
865 #[tokio::test]
866 async fn test_cache_stats() {
867 let tiff_data = create_tiff_with_jpeg_tile();
868 let source = MockSlideSource::new(tiff_data);
869 let registry = SlideRegistry::new(source);
870 let service = TileService::with_cache_capacity(registry, 10 * 1024 * 1024); let (size, capacity, count) = service.cache_stats().await;
873 assert_eq!(size, 0);
874 assert_eq!(capacity, 10 * 1024 * 1024);
875 assert_eq!(count, 0);
876
877 let request = TileRequest::new("test.tif", 0, 0, 0);
879 service.get_tile(request).await.unwrap();
880
881 let (size, _, count) = service.cache_stats().await;
882 assert!(size > 0);
883 assert_eq!(count, 1);
884 }
885
886 #[tokio::test]
887 async fn test_clear_cache() {
888 let tiff_data = create_tiff_with_jpeg_tile();
889 let source = MockSlideSource::new(tiff_data);
890 let registry = SlideRegistry::new(source);
891 let service = TileService::new(registry);
892
893 service
895 .get_tile(TileRequest::new("test.tif", 0, 0, 0))
896 .await
897 .unwrap();
898 service
899 .get_tile(TileRequest::new("test.tif", 0, 1, 0))
900 .await
901 .unwrap();
902
903 let (_, _, count) = service.cache_stats().await;
904 assert_eq!(count, 2);
905
906 service.clear_cache().await;
908
909 let (size, _, count) = service.cache_stats().await;
910 assert_eq!(size, 0);
911 assert_eq!(count, 0);
912 }
913
914 #[tokio::test]
915 async fn test_quality_validation() {
916 let tiff_data = create_tiff_with_jpeg_tile();
917 let source = MockSlideSource::new(tiff_data);
918 let registry = SlideRegistry::new(source);
919 let service = TileService::new(registry);
920
921 let request = TileRequest::with_quality("test.tif", 0, 0, 0, 0);
923 let result = service.get_tile(request).await;
924 assert!(matches!(
925 result,
926 Err(TileError::InvalidQuality { quality: 0 })
927 ));
928
929 let request = TileRequest::with_quality("test.tif", 0, 1, 0, 255);
931 let result = service.get_tile(request).await;
932 assert!(matches!(
933 result,
934 Err(TileError::InvalidQuality { quality: 255 })
935 ));
936 }
937
938 #[tokio::test]
943 async fn test_generate_thumbnail_returns_valid_jpeg() {
944 let tiff_data = create_tiff_with_jpeg_tile();
945 let source = MockSlideSource::new(tiff_data);
946 let registry = SlideRegistry::new(source);
947 let service = TileService::new(registry);
948
949 let result = service.generate_thumbnail("test.tif", 512, 80).await;
951
952 assert!(result.is_ok());
953 let response = result.unwrap();
954
955 assert!(response.data.len() > 2);
957 assert_eq!(response.data[0], 0xFF); assert_eq!(response.data[1], 0xD8);
959 assert_eq!(response.data[response.data.len() - 2], 0xFF); assert_eq!(response.data[response.data.len() - 1], 0xD9);
961 }
962
963 #[tokio::test]
964 async fn test_generate_thumbnail_composites_multiple_tiles() {
965 let tiff_data = create_tiff_with_jpeg_tile();
966 let source = MockSlideSource::new(tiff_data);
967 let registry = SlideRegistry::new(source);
968 let service = TileService::new(registry);
969
970 let thumbnail_result = service.generate_thumbnail("test.tif", 512, 80).await;
973 assert!(thumbnail_result.is_ok());
974 let thumbnail = thumbnail_result.unwrap();
975
976 let tile_result = service
978 .get_tile(TileRequest::with_quality("test.tif", 0, 0, 0, 80))
979 .await;
980 assert!(tile_result.is_ok());
981 let single_tile = tile_result.unwrap();
982
983 let cursor = std::io::Cursor::new(&thumbnail.data[..]);
986 let reader = image::ImageReader::with_format(cursor, image::ImageFormat::Jpeg);
987 let img = reader.decode();
988 assert!(img.is_ok(), "Thumbnail should be a valid decodable image");
989
990 let decoded = img.unwrap();
991 assert!(
993 decoded.width() <= 512 && decoded.height() <= 512,
994 "Thumbnail dimensions should fit within max_dimension"
995 );
996
997 let tile_cursor = std::io::Cursor::new(&single_tile.data[..]);
1000 let tile_reader = image::ImageReader::with_format(tile_cursor, image::ImageFormat::Jpeg);
1001 let tile_img = tile_reader.decode().unwrap();
1002
1003 assert_eq!(tile_img.width(), 256);
1005 assert_eq!(tile_img.height(), 256);
1006 }
1007
1008 #[tokio::test]
1009 async fn test_generate_thumbnail_respects_max_dimension() {
1010 let tiff_data = create_tiff_with_jpeg_tile();
1011 let source = MockSlideSource::new(tiff_data);
1012 let registry = SlideRegistry::new(source);
1013 let service = TileService::new(registry);
1014
1015 for max_dim in [128, 256, 512, 1024] {
1017 let result = service.generate_thumbnail("test.tif", max_dim, 80).await;
1018 assert!(result.is_ok());
1019
1020 let response = result.unwrap();
1021 let cursor = std::io::Cursor::new(&response.data[..]);
1022 let reader = image::ImageReader::with_format(cursor, image::ImageFormat::Jpeg);
1023 let img = reader.decode().unwrap();
1024
1025 assert!(
1027 img.width() <= max_dim,
1028 "Width {} should be <= max_dim {}",
1029 img.width(),
1030 max_dim
1031 );
1032 assert!(
1033 img.height() <= max_dim,
1034 "Height {} should be <= max_dim {}",
1035 img.height(),
1036 max_dim
1037 );
1038
1039 let max_actual = img.width().max(img.height());
1042 assert!(
1043 max_actual >= max_dim - 1,
1044 "Max dimension {} should be close to {}",
1045 max_actual,
1046 max_dim
1047 );
1048 }
1049 }
1050
1051 #[tokio::test]
1052 async fn test_generate_thumbnail_preserves_aspect_ratio() {
1053 let tiff_data = create_tiff_with_jpeg_tile();
1054 let source = MockSlideSource::new(tiff_data);
1055 let registry = SlideRegistry::new(source);
1056 let service = TileService::new(registry);
1057
1058 let result = service.generate_thumbnail("test.tif", 400, 80).await;
1060 assert!(result.is_ok());
1061
1062 let response = result.unwrap();
1063 let cursor = std::io::Cursor::new(&response.data[..]);
1064 let reader = image::ImageReader::with_format(cursor, image::ImageFormat::Jpeg);
1065 let img = reader.decode().unwrap();
1066
1067 let aspect_ratio = img.width() as f64 / img.height() as f64;
1069 let expected_ratio = 2048.0 / 1536.0; assert!(
1072 (aspect_ratio - expected_ratio).abs() < 0.1,
1073 "Aspect ratio {} should be close to expected {}",
1074 aspect_ratio,
1075 expected_ratio
1076 );
1077 }
1078
1079 #[tokio::test]
1080 async fn test_generate_thumbnail_invalid_quality() {
1081 let tiff_data = create_tiff_with_jpeg_tile();
1082 let source = MockSlideSource::new(tiff_data);
1083 let registry = SlideRegistry::new(source);
1084 let service = TileService::new(registry);
1085
1086 let result = service.generate_thumbnail("test.tif", 256, 0).await;
1088 assert!(matches!(
1089 result,
1090 Err(TileError::InvalidQuality { quality: 0 })
1091 ));
1092
1093 let result = service.generate_thumbnail("test.tif", 256, 255).await;
1095 assert!(matches!(
1096 result,
1097 Err(TileError::InvalidQuality { quality: 255 })
1098 ));
1099 }
1100
1101 #[tokio::test]
1102 async fn test_generate_thumbnail_slide_not_found() {
1103 let tiff_data = create_tiff_with_jpeg_tile();
1104 let source = MockSlideSource::new(tiff_data);
1105 let registry = SlideRegistry::new(source);
1106 let service = TileService::new(registry);
1107
1108 let result = service.generate_thumbnail("notfound.tif", 256, 80).await;
1109 assert!(result.is_err());
1110 match result.unwrap_err() {
1111 TileError::SlideNotFound { slide_id } => {
1112 assert_eq!(slide_id, "notfound.tif");
1113 }
1114 e => panic!("Expected SlideNotFound error, got {:?}", e),
1115 }
1116 }
1117}