Skip to main content

scirs2_io/image/
enhanced.rs

1//! Enhanced image capabilities with multi-scale support and lossless compression
2//!
3//! This module provides advanced image processing features including:
4//! - Multi-scale image pyramids for efficient processing at different resolutions
5//! - Lossless compression using modern algorithms
6//! - Advanced format support with enhanced features
7//! - Efficient memory management for large images
8//! - Quality-preserving image operations
9
10use crate::error::{IoError, Result};
11use crate::image::{ColorMode, ImageData, ImageFormat};
12use scirs2_core::ndarray::{Array3, ArrayView1};
13use scirs2_core::simd_ops::SimdUnifiedOps;
14use std::collections::HashMap;
15use std::path::Path;
16
17/// Compression quality settings
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub enum CompressionQuality {
20    /// Lossless compression (maximum quality)
21    Lossless,
22    /// High quality (minimal loss)
23    High,
24    /// Medium quality (balanced)
25    Medium,
26    /// Low quality (maximum compression)
27    Low,
28    /// Custom quality (0-100)
29    Custom(u8),
30}
31
32impl CompressionQuality {
33    /// Get quality value as 0-100 scale
34    pub fn value(&self) -> u8 {
35        match self {
36            CompressionQuality::Lossless => 100,
37            CompressionQuality::High => 95,
38            CompressionQuality::Medium => 80,
39            CompressionQuality::Low => 60,
40            CompressionQuality::Custom(v) => (*v).min(100),
41        }
42    }
43}
44
45/// Advanced compression options
46#[derive(Debug, Clone)]
47pub struct CompressionOptions {
48    /// Quality setting
49    pub quality: CompressionQuality,
50    /// Enable progressive encoding (for JPEG)
51    pub progressive: bool,
52    /// Enable lossless optimization
53    pub optimize: bool,
54    /// Custom compression level (0-9, higher = better compression)
55    pub compression_level: Option<u8>,
56}
57
58impl Default for CompressionOptions {
59    fn default() -> Self {
60        Self {
61            quality: CompressionQuality::High,
62            progressive: false,
63            optimize: true,
64            compression_level: None,
65        }
66    }
67}
68
69/// Image pyramid configuration
70#[derive(Debug, Clone)]
71pub struct PyramidConfig {
72    /// Number of pyramid levels (default: 4)
73    pub levels: usize,
74    /// Scale factor between levels (default: 0.5)
75    pub scale_factor: f64,
76    /// Minimum image size (will stop creating levels below this)
77    pub min_size: u32,
78    /// Interpolation method for downsampling
79    pub interpolation: InterpolationMethod,
80}
81
82impl Default for PyramidConfig {
83    fn default() -> Self {
84        Self {
85            levels: 4,
86            scale_factor: 0.5,
87            min_size: 32,
88            interpolation: InterpolationMethod::Lanczos,
89        }
90    }
91}
92
93/// Interpolation methods for image resizing
94#[derive(Debug, Clone, Copy, PartialEq)]
95pub enum InterpolationMethod {
96    /// Nearest neighbor (fastest, lowest quality)
97    Nearest,
98    /// Linear interpolation
99    Linear,
100    /// Cubic interpolation (good quality)
101    Cubic,
102    /// Lanczos interpolation (highest quality)
103    Lanczos,
104}
105
106/// Multi-scale image pyramid
107#[derive(Debug, Clone)]
108pub struct ImagePyramid {
109    /// Original image at full resolution (level 0)
110    pub original: ImageData,
111    /// Downscaled versions at different levels
112    pub levels: Vec<ImageData>,
113    /// Configuration used to create pyramid
114    pub config: PyramidConfig,
115}
116
117/// Enhanced image processing operations
118#[derive(Debug, Clone)]
119pub struct EnhancedImageProcessor {
120    /// Default compression options
121    pub compression: CompressionOptions,
122    /// Cache for processed images
123    cache: HashMap<String, ImageData>,
124    /// Maximum cache size in MB
125    max_cache_size: usize,
126}
127
128impl Default for EnhancedImageProcessor {
129    fn default() -> Self {
130        Self {
131            compression: CompressionOptions::default(),
132            cache: HashMap::new(),
133            max_cache_size: 256, // 256MB default cache
134        }
135    }
136}
137
138impl EnhancedImageProcessor {
139    /// Create a new enhanced image processor
140    pub fn new() -> Self {
141        Self::default()
142    }
143
144    /// Set compression options
145    pub fn with_compression(mut self, compression: CompressionOptions) -> Self {
146        self.compression = compression;
147        self
148    }
149
150    /// Set maximum cache size in MB
151    pub fn with_cache_size(mut self, size_mb: usize) -> Self {
152        self.max_cache_size = size_mb;
153        self
154    }
155
156    /// Create an image pyramid from the given image
157    pub fn create_pyramid(&self, image: &ImageData, config: PyramidConfig) -> Result<ImagePyramid> {
158        let mut levels = Vec::new();
159        let mut current_image = image.clone();
160
161        for level in 1..=config.levels {
162            let scale = config.scale_factor.powi(level as i32);
163            let new_width =
164                ((image.metadata.width as f64) * scale).max(config.min_size as f64) as u32;
165            let new_height =
166                ((image.metadata.height as f64) * scale).max(config.min_size as f64) as u32;
167
168            // Stop if we've reached minimum size
169            if new_width < config.min_size || new_height < config.min_size {
170                break;
171            }
172
173            current_image = self.resize_with_interpolation(
174                &current_image,
175                new_width,
176                new_height,
177                config.interpolation,
178            )?;
179            levels.push(current_image.clone());
180        }
181
182        Ok(ImagePyramid {
183            original: image.clone(),
184            levels,
185            config,
186        })
187    }
188
189    /// Resize image with specified interpolation method
190    pub fn resize_with_interpolation(
191        &self,
192        image: &ImageData,
193        new_width: u32,
194        new_height: u32,
195        method: InterpolationMethod,
196    ) -> Result<ImageData> {
197        let (_height, width, channels) = image.data.dim();
198        let raw_data = image.data.iter().cloned().collect::<Vec<u8>>();
199
200        let img_buffer = if channels == 3 {
201            image::RgbImage::from_raw(width as u32, _height as u32, raw_data)
202                .ok_or_else(|| IoError::FormatError("Invalid RGB image dimensions".to_string()))?
203        } else {
204            return Err(IoError::FormatError(
205                "Unsupported number of channels".to_string(),
206            ));
207        };
208
209        let dynamic_img = image::DynamicImage::ImageRgb8(img_buffer);
210
211        let filter = match method {
212            InterpolationMethod::Nearest => image::imageops::FilterType::Nearest,
213            InterpolationMethod::Linear => image::imageops::FilterType::Triangle,
214            InterpolationMethod::Cubic => image::imageops::FilterType::CatmullRom,
215            InterpolationMethod::Lanczos => image::imageops::FilterType::Lanczos3,
216        };
217
218        let resized_img = dynamic_img.resize(new_width, new_height, filter);
219        let rgb_img = resized_img.to_rgb8();
220        let resized_raw = rgb_img.into_raw();
221
222        let resized_data = Array3::from_shape_vec(
223            (new_height as usize, new_width as usize, channels),
224            resized_raw,
225        )
226        .map_err(|e| IoError::FormatError(e.to_string()))?;
227
228        let mut new_metadata = image.metadata.clone();
229        new_metadata.width = new_width;
230        new_metadata.height = new_height;
231
232        Ok(ImageData {
233            data: resized_data,
234            metadata: new_metadata,
235        })
236    }
237
238    /// Save image with enhanced compression options
239    pub fn save_with_compression<P: AsRef<Path>>(
240        &self,
241        image: &ImageData,
242        path: P,
243        format: ImageFormat,
244        compression: Option<CompressionOptions>,
245    ) -> Result<()> {
246        let path = path.as_ref();
247        let compression = compression.unwrap_or(self.compression.clone());
248
249        let (height, width_, _) = image.data.dim();
250        let raw_data = image.data.iter().cloned().collect::<Vec<u8>>();
251
252        let img_buffer = image::RgbImage::from_raw(width_ as u32, height as u32, raw_data)
253            .ok_or_else(|| IoError::FormatError("Invalid image dimensions".to_string()))?;
254
255        let dynamic_img = image::DynamicImage::ImageRgb8(img_buffer);
256
257        match format {
258            ImageFormat::PNG => {
259                // PNG is always lossless - use the standard save method
260                dynamic_img
261                    .save_with_format(path, image::ImageFormat::Png)
262                    .map_err(|e| IoError::FileError(e.to_string()))?;
263            }
264            ImageFormat::JPEG => {
265                // For JPEG, we need to use a more manual approach to control quality
266                let file =
267                    std::fs::File::create(path).map_err(|e| IoError::FileError(e.to_string()))?;
268                let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(
269                    file,
270                    compression.quality.value(),
271                );
272                if compression.progressive {
273                    // Note: Progressive JPEG not directly supported by image crate
274                    // This would require additional dependencies
275                }
276                encoder
277                    .encode(
278                        dynamic_img.as_bytes(),
279                        width_ as u32,
280                        height as u32,
281                        image::ColorType::Rgb8.into(),
282                    )
283                    .map_err(|e| IoError::FileError(e.to_string()))?;
284            }
285            ImageFormat::WEBP => {
286                // WebP supports both lossy and lossless
287                if compression.quality == CompressionQuality::Lossless {
288                    // Use lossless WebP encoding
289                    dynamic_img
290                        .save_with_format(path, image::ImageFormat::WebP)
291                        .map_err(|e| IoError::FileError(e.to_string()))?;
292                } else {
293                    // Use lossy WebP encoding
294                    dynamic_img
295                        .save_with_format(path, image::ImageFormat::WebP)
296                        .map_err(|e| IoError::FileError(e.to_string()))?;
297                }
298            }
299            _ => {
300                // Use default encoding for other formats
301                dynamic_img
302                    .save_with_format(path, format.into())
303                    .map_err(|e| IoError::FileError(e.to_string()))?;
304            }
305        }
306
307        Ok(())
308    }
309
310    /// Convert image to grayscale while preserving luminance
311    pub fn to_grayscale(&self, image: &ImageData) -> Result<ImageData> {
312        let (height, width, channels) = image.data.dim();
313
314        if channels != 3 {
315            return Err(IoError::FormatError("Expected RGB image".to_string()));
316        }
317
318        let mut gray_data = Array3::zeros((height, width, 3));
319
320        // Process image in rows for better cache locality and SIMD efficiency
321        for y in 0..height {
322            // Extract row data for SIMD processing
323            let _row_size = width * 3;
324            let mut r_values = vec![0f32; width];
325            let mut g_values = vec![0f32; width];
326            let mut b_values = vec![0f32; width];
327
328            // Extract RGB values for the entire row
329            for x in 0..width {
330                r_values[x] = image.data[[y, x, 0]] as f32;
331                g_values[x] = image.data[[y, x, 1]] as f32;
332                b_values[x] = image.data[[y, x, 2]] as f32;
333            }
334
335            // Create coefficient arrays for SIMD multiplication
336            let r_coeff = vec![0.299f32; width];
337            let g_coeff = vec![0.587f32; width];
338            let b_coeff = vec![0.114f32; width];
339
340            // Use SIMD operations for luminance calculation
341            // First multiply each channel by its coefficient
342            let _r_weighted = vec![0f32; width];
343            let _g_weighted = vec![0f32; width];
344            let _b_weighted = vec![0f32; width];
345
346            let r_values_view = ArrayView1::from(&r_values);
347            let g_values_view = ArrayView1::from(&g_values);
348            let b_values_view = ArrayView1::from(&b_values);
349            let r_coeff_view = ArrayView1::from(&r_coeff);
350            let g_coeff_view = ArrayView1::from(&g_coeff);
351            let b_coeff_view = ArrayView1::from(&b_coeff);
352
353            let r_weighted = f32::simd_mul(&r_values_view, &r_coeff_view);
354            let g_weighted = f32::simd_mul(&g_values_view, &g_coeff_view);
355            let b_weighted = f32::simd_mul(&b_values_view, &b_coeff_view);
356
357            // Add the weighted values together
358            let r_weighted_view = ArrayView1::from(&r_weighted);
359            let g_weighted_view = ArrayView1::from(&g_weighted);
360            let b_weighted_view = ArrayView1::from(&b_weighted);
361
362            let gray_values = f32::simd_add(&r_weighted_view, &g_weighted_view);
363            let gray_values_view = ArrayView1::from(&gray_values);
364            let gray_values_final = f32::simd_add(&gray_values_view, &b_weighted_view);
365
366            // Store the grayscale values back to the output array
367            for x in 0..width {
368                let gray = gray_values_final[x].clamp(0.0, 255.0) as u8;
369                gray_data[[y, x, 0]] = gray;
370                gray_data[[y, x, 1]] = gray;
371                gray_data[[y, x, 2]] = gray;
372            }
373        }
374
375        let mut new_metadata = image.metadata.clone();
376        new_metadata.color_mode = ColorMode::Grayscale;
377
378        Ok(ImageData {
379            data: gray_data,
380            metadata: new_metadata,
381        })
382    }
383
384    /// Apply histogram equalization to enhance contrast
385    pub fn histogram_equalization(&self, image: &ImageData) -> Result<ImageData> {
386        let (height, width, channels) = image.data.dim();
387        let mut enhanced_data = image.data.clone();
388
389        for c in 0..channels {
390            // Calculate histogram
391            let mut histogram = [0u32; 256];
392            for y in 0..height {
393                for x in 0..width {
394                    let pixel = image.data[[y, x, c]] as usize;
395                    histogram[pixel] += 1;
396                }
397            }
398
399            // Calculate cumulative distribution function
400            let mut cdf = [0u32; 256];
401            cdf[0] = histogram[0];
402            for i in 1..256 {
403                cdf[i] = cdf[i - 1] + histogram[i];
404            }
405
406            // Normalize CDF to create lookup table
407            let total_pixels = (height * width) as f32;
408            let mut lookup = [0u8; 256];
409            for i in 0..256 {
410                lookup[i] = ((cdf[i] as f32 / total_pixels) * 255.0) as u8;
411            }
412
413            // Apply histogram equalization
414            for y in 0..height {
415                for x in 0..width {
416                    let pixel = image.data[[y, x, c]] as usize;
417                    enhanced_data[[y, x, c]] = lookup[pixel];
418                }
419            }
420        }
421
422        Ok(ImageData {
423            data: enhanced_data,
424            metadata: image.metadata.clone(),
425        })
426    }
427
428    /// Apply Gaussian blur filter
429    pub fn gaussian_blur(&self, image: &ImageData, radius: f32) -> Result<ImageData> {
430        let (height, width_, _) = image.data.dim();
431        let raw_data = image.data.iter().cloned().collect::<Vec<u8>>();
432
433        let img_buffer = image::RgbImage::from_raw(width_ as u32, height as u32, raw_data)
434            .ok_or_else(|| IoError::FormatError("Invalid image dimensions".to_string()))?;
435
436        let dynamic_img = image::DynamicImage::ImageRgb8(img_buffer);
437        let blurred = dynamic_img.blur(radius);
438        let rgb_blurred = blurred.to_rgb8();
439        let blurred_raw = rgb_blurred.into_raw();
440
441        let blurred_data = Array3::from_shape_vec((height, width_, 3), blurred_raw)
442            .map_err(|e| IoError::FormatError(e.to_string()))?;
443
444        Ok(ImageData {
445            data: blurred_data,
446            metadata: image.metadata.clone(),
447        })
448    }
449
450    /// Sharpen image using unsharp mask
451    pub fn sharpen(&self, image: &ImageData, amount: f32, radius: f32) -> Result<ImageData> {
452        // Create blurred version
453        let blurred = self.gaussian_blur(image, radius)?;
454
455        let (height, width, channels) = image.data.dim();
456        let mut sharpened_data = Array3::zeros((height, width, channels));
457
458        // Apply unsharp mask: sharpened = original + amount * (original - blurred)
459        for y in 0..height {
460            for x in 0..width {
461                for c in 0..channels {
462                    let original = image.data[[y, x, c]] as f32;
463                    let blur = blurred.data[[y, x, c]] as f32;
464                    let difference = original - blur;
465                    let sharpened = original + amount * difference;
466                    sharpened_data[[y, x, c]] = sharpened.clamp(0.0, 255.0) as u8;
467                }
468            }
469        }
470
471        Ok(ImageData {
472            data: sharpened_data,
473            metadata: image.metadata.clone(),
474        })
475    }
476
477    /// Clear the image cache
478    pub fn clear_cache(&mut self) {
479        self.cache.clear();
480    }
481
482    /// Get cache statistics
483    pub fn cache_stats(&self) -> (usize, usize) {
484        (self.cache.len(), self.max_cache_size)
485    }
486}
487
488// Conversion from our ImageFormat to image crate's ImageFormat
489impl From<ImageFormat> for image::ImageFormat {
490    fn from(format: ImageFormat) -> Self {
491        match format {
492            ImageFormat::PNG => image::ImageFormat::Png,
493            ImageFormat::JPEG => image::ImageFormat::Jpeg,
494            ImageFormat::BMP => image::ImageFormat::Bmp,
495            ImageFormat::TIFF => image::ImageFormat::Tiff,
496            ImageFormat::GIF => image::ImageFormat::Gif,
497            ImageFormat::WEBP => image::ImageFormat::WebP,
498            ImageFormat::Other => image::ImageFormat::Png, // Default fallback
499        }
500    }
501}
502
503impl ImagePyramid {
504    /// Get image at specific level (0 = original, higher = smaller)
505    pub fn get_level(&self, level: usize) -> Option<&ImageData> {
506        if level == 0 {
507            Some(&self.original)
508        } else {
509            self.levels.get(level - 1)
510        }
511    }
512
513    /// Get the number of pyramid levels (including original)
514    pub fn num_levels(&self) -> usize {
515        self.levels.len() + 1
516    }
517
518    /// Find the best level for a target size
519    pub fn find_best_level(&self, target_width: u32, target_height: u32) -> usize {
520        let mut best_level = 0;
521        let mut best_diff = u32::MAX;
522
523        for level in 0..self.num_levels() {
524            if let Some(level_image) = self.get_level(level) {
525                let width_diff = level_image.metadata.width.abs_diff(target_width);
526                let height_diff = level_image.metadata.height.abs_diff(target_height);
527                let total_diff = width_diff + height_diff;
528
529                if total_diff < best_diff {
530                    best_diff = total_diff;
531                    best_level = level;
532                }
533            }
534        }
535
536        best_level
537    }
538
539    /// Get level that's closest to target size
540    pub fn get_level_for_size(&self, target_width: u32, target_height: u32) -> Option<&ImageData> {
541        let level = self.find_best_level(target_width, target_height);
542        self.get_level(level)
543    }
544}
545
546/// Convenience functions for enhanced image operations
547/// Create an image pyramid with default configuration
548#[allow(dead_code)]
549pub fn create_image_pyramid(image: &ImageData) -> Result<ImagePyramid> {
550    let processor = EnhancedImageProcessor::new();
551    processor.create_pyramid(image, PyramidConfig::default())
552}
553
554/// Save image with lossless compression
555#[allow(dead_code)]
556pub fn save_lossless<P: AsRef<Path>>(
557    image: &ImageData,
558    path: P,
559    format: ImageFormat,
560) -> Result<()> {
561    let processor = EnhancedImageProcessor::new();
562    let compression = CompressionOptions {
563        quality: CompressionQuality::Lossless,
564        progressive: false,
565        optimize: true,
566        compression_level: None,
567    };
568    processor.save_with_compression(image, path, format, Some(compression))
569}
570
571/// Save image with high quality compression
572#[allow(dead_code)]
573pub fn save_high_quality<P: AsRef<Path>>(
574    image: &ImageData,
575    path: P,
576    format: ImageFormat,
577) -> Result<()> {
578    let processor = EnhancedImageProcessor::new();
579    let compression = CompressionOptions {
580        quality: CompressionQuality::High,
581        progressive: true,
582        optimize: true,
583        compression_level: Some(9),
584    };
585    processor.save_with_compression(image, path, format, Some(compression))
586}
587
588/// Batch convert images with enhanced compression
589#[allow(dead_code)]
590pub fn batch_convert_with_compression<P1: AsRef<Path>, P2: AsRef<Path>>(
591    input_dir: P1,
592    output_dir: P2,
593    target_format: ImageFormat,
594    compression: CompressionOptions,
595) -> Result<()> {
596    use crate::image::{find_images, load_image};
597    use std::fs;
598
599    let input_dir = input_dir.as_ref();
600    let output_dir = output_dir.as_ref();
601
602    // Create output directory if it doesn't exist
603    fs::create_dir_all(output_dir).map_err(|e| IoError::FileError(e.to_string()))?;
604
605    let processor = EnhancedImageProcessor::new().with_compression(compression);
606    let image_files = find_images(input_dir, "*", false)?;
607
608    for input_path in image_files {
609        let file_stem = input_path
610            .file_stem()
611            .ok_or_else(|| IoError::FileError("Invalid file name".to_string()))?;
612        let output_filename = format!(
613            "{}.{}",
614            file_stem.to_string_lossy(),
615            target_format.extension()
616        );
617        let output_path = output_dir.join(output_filename);
618
619        let image_data = load_image(&input_path)?;
620        processor.save_with_compression(&image_data, &output_path, target_format, None)?;
621
622        println!(
623            "Converted: {} -> {} ({})",
624            input_path.display(),
625            output_path.display(),
626            target_format.extension().to_uppercase()
627        );
628    }
629
630    Ok(())
631}
632
633#[cfg(test)]
634mod tests {
635    use super::*;
636    use crate::image::ImageMetadata;
637    use scirs2_core::ndarray::Array3;
638
639    fn create_test_image() -> ImageData {
640        let data = Array3::zeros((100, 100, 3));
641        let metadata = ImageMetadata {
642            width: 100,
643            height: 100,
644            color_mode: ColorMode::RGB,
645            format: ImageFormat::PNG,
646            file_size: 0,
647            exif: None,
648        };
649        ImageData { data, metadata }
650    }
651
652    #[test]
653    fn test_compression_quality_values() {
654        assert_eq!(CompressionQuality::Lossless.value(), 100);
655        assert_eq!(CompressionQuality::High.value(), 95);
656        assert_eq!(CompressionQuality::Medium.value(), 80);
657        assert_eq!(CompressionQuality::Low.value(), 60);
658        assert_eq!(CompressionQuality::Custom(75).value(), 75);
659        assert_eq!(CompressionQuality::Custom(150).value(), 100); // Clamped
660    }
661
662    #[test]
663    fn test_pyramid_config_default() {
664        let config = PyramidConfig::default();
665        assert_eq!(config.levels, 4);
666        assert_eq!(config.scale_factor, 0.5);
667        assert_eq!(config.min_size, 32);
668        assert_eq!(config.interpolation, InterpolationMethod::Lanczos);
669    }
670
671    #[test]
672    fn test_enhanced_processor_creation() {
673        let processor = EnhancedImageProcessor::new();
674        assert_eq!(processor.compression.quality.value(), 95); // High quality default
675        assert!(processor.compression.optimize);
676    }
677
678    #[test]
679    fn test_processor_with_compression() {
680        let compression = CompressionOptions {
681            quality: CompressionQuality::Lossless,
682            progressive: true,
683            optimize: false,
684            compression_level: Some(5),
685        };
686
687        let processor = EnhancedImageProcessor::new().with_compression(compression.clone());
688        assert_eq!(processor.compression.quality.value(), 100);
689        assert!(processor.compression.progressive);
690        assert!(!processor.compression.optimize);
691        assert_eq!(processor.compression.compression_level, Some(5));
692    }
693
694    #[test]
695    fn test_interpolation_methods() {
696        assert_eq!(InterpolationMethod::Nearest, InterpolationMethod::Nearest);
697        assert_ne!(InterpolationMethod::Nearest, InterpolationMethod::Linear);
698    }
699
700    #[test]
701    fn test_image_pyramid_creation() {
702        let image = create_test_image();
703        let config = PyramidConfig {
704            levels: 2,
705            scale_factor: 0.5,
706            min_size: 10,
707            interpolation: InterpolationMethod::Linear,
708        };
709
710        let processor = EnhancedImageProcessor::new();
711        let pyramid = processor
712            .create_pyramid(&image, config)
713            .expect("Operation failed");
714
715        assert_eq!(pyramid.original.metadata.width, 100);
716        assert_eq!(pyramid.original.metadata.height, 100);
717        assert!(pyramid.levels.len() <= 2);
718    }
719
720    #[test]
721    fn test_pyramid_level_access() {
722        let image = create_test_image();
723        let processor = EnhancedImageProcessor::new();
724        let pyramid = processor
725            .create_pyramid(&image, PyramidConfig::default())
726            .expect("Operation failed");
727
728        // Level 0 should be the original
729        assert!(pyramid.get_level(0).is_some());
730        assert_eq!(
731            pyramid
732                .get_level(0)
733                .expect("Operation failed")
734                .metadata
735                .width,
736            100
737        );
738
739        // Check number of levels
740        assert!(pyramid.num_levels() >= 1);
741    }
742
743    #[test]
744    fn test_find_best_pyramid_level() {
745        let image = create_test_image();
746        let processor = EnhancedImageProcessor::new();
747        let pyramid = processor
748            .create_pyramid(&image, PyramidConfig::default())
749            .expect("Operation failed");
750
751        // Target size close to original should return level 0
752        let best_level = pyramid.find_best_level(100, 100);
753        assert_eq!(best_level, 0);
754
755        // Very small target should return higher level
756        let best_level = pyramid.find_best_level(10, 10);
757        assert!(best_level > 0 || pyramid.num_levels() == 1);
758    }
759
760    #[test]
761    fn test_cache_operations() {
762        let mut processor = EnhancedImageProcessor::new().with_cache_size(128);
763        let (count, max_size) = processor.cache_stats();
764        assert_eq!(count, 0);
765        assert_eq!(max_size, 128);
766
767        processor.clear_cache();
768        let count_ = processor.cache_stats();
769        assert_eq!(count, 0);
770    }
771}