use crate::error::{IoError, Result};
use crate::image::{ColorMode, ImageData, ImageFormat};
use scirs2_core::ndarray::{Array3, ArrayView1};
use scirs2_core::simd_ops::SimdUnifiedOps;
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CompressionQuality {
Lossless,
High,
Medium,
Low,
Custom(u8),
}
impl CompressionQuality {
pub fn value(&self) -> u8 {
match self {
CompressionQuality::Lossless => 100,
CompressionQuality::High => 95,
CompressionQuality::Medium => 80,
CompressionQuality::Low => 60,
CompressionQuality::Custom(v) => (*v).min(100),
}
}
}
#[derive(Debug, Clone)]
pub struct CompressionOptions {
pub quality: CompressionQuality,
pub progressive: bool,
pub optimize: bool,
pub compression_level: Option<u8>,
}
impl Default for CompressionOptions {
fn default() -> Self {
Self {
quality: CompressionQuality::High,
progressive: false,
optimize: true,
compression_level: None,
}
}
}
#[derive(Debug, Clone)]
pub struct PyramidConfig {
pub levels: usize,
pub scale_factor: f64,
pub min_size: u32,
pub interpolation: InterpolationMethod,
}
impl Default for PyramidConfig {
fn default() -> Self {
Self {
levels: 4,
scale_factor: 0.5,
min_size: 32,
interpolation: InterpolationMethod::Lanczos,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum InterpolationMethod {
Nearest,
Linear,
Cubic,
Lanczos,
}
#[derive(Debug, Clone)]
pub struct ImagePyramid {
pub original: ImageData,
pub levels: Vec<ImageData>,
pub config: PyramidConfig,
}
#[derive(Debug, Clone)]
pub struct EnhancedImageProcessor {
pub compression: CompressionOptions,
cache: HashMap<String, ImageData>,
max_cache_size: usize,
}
impl Default for EnhancedImageProcessor {
fn default() -> Self {
Self {
compression: CompressionOptions::default(),
cache: HashMap::new(),
max_cache_size: 256, }
}
}
impl EnhancedImageProcessor {
pub fn new() -> Self {
Self::default()
}
pub fn with_compression(mut self, compression: CompressionOptions) -> Self {
self.compression = compression;
self
}
pub fn with_cache_size(mut self, size_mb: usize) -> Self {
self.max_cache_size = size_mb;
self
}
pub fn create_pyramid(&self, image: &ImageData, config: PyramidConfig) -> Result<ImagePyramid> {
let mut levels = Vec::new();
let mut current_image = image.clone();
for level in 1..=config.levels {
let scale = config.scale_factor.powi(level as i32);
let new_width =
((image.metadata.width as f64) * scale).max(config.min_size as f64) as u32;
let new_height =
((image.metadata.height as f64) * scale).max(config.min_size as f64) as u32;
if new_width < config.min_size || new_height < config.min_size {
break;
}
current_image = self.resize_with_interpolation(
¤t_image,
new_width,
new_height,
config.interpolation,
)?;
levels.push(current_image.clone());
}
Ok(ImagePyramid {
original: image.clone(),
levels,
config,
})
}
pub fn resize_with_interpolation(
&self,
image: &ImageData,
new_width: u32,
new_height: u32,
method: InterpolationMethod,
) -> Result<ImageData> {
let (_height, width, channels) = image.data.dim();
let raw_data = image.data.iter().cloned().collect::<Vec<u8>>();
let img_buffer = if channels == 3 {
image::RgbImage::from_raw(width as u32, _height as u32, raw_data)
.ok_or_else(|| IoError::FormatError("Invalid RGB image dimensions".to_string()))?
} else {
return Err(IoError::FormatError(
"Unsupported number of channels".to_string(),
));
};
let dynamic_img = image::DynamicImage::ImageRgb8(img_buffer);
let filter = match method {
InterpolationMethod::Nearest => image::imageops::FilterType::Nearest,
InterpolationMethod::Linear => image::imageops::FilterType::Triangle,
InterpolationMethod::Cubic => image::imageops::FilterType::CatmullRom,
InterpolationMethod::Lanczos => image::imageops::FilterType::Lanczos3,
};
let resized_img = dynamic_img.resize(new_width, new_height, filter);
let rgb_img = resized_img.to_rgb8();
let resized_raw = rgb_img.into_raw();
let resized_data = Array3::from_shape_vec(
(new_height as usize, new_width as usize, channels),
resized_raw,
)
.map_err(|e| IoError::FormatError(e.to_string()))?;
let mut new_metadata = image.metadata.clone();
new_metadata.width = new_width;
new_metadata.height = new_height;
Ok(ImageData {
data: resized_data,
metadata: new_metadata,
})
}
pub fn save_with_compression<P: AsRef<Path>>(
&self,
image: &ImageData,
path: P,
format: ImageFormat,
compression: Option<CompressionOptions>,
) -> Result<()> {
let path = path.as_ref();
let compression = compression.unwrap_or(self.compression.clone());
let (height, width_, _) = image.data.dim();
let raw_data = image.data.iter().cloned().collect::<Vec<u8>>();
let img_buffer = image::RgbImage::from_raw(width_ as u32, height as u32, raw_data)
.ok_or_else(|| IoError::FormatError("Invalid image dimensions".to_string()))?;
let dynamic_img = image::DynamicImage::ImageRgb8(img_buffer);
match format {
ImageFormat::PNG => {
dynamic_img
.save_with_format(path, image::ImageFormat::Png)
.map_err(|e| IoError::FileError(e.to_string()))?;
}
ImageFormat::JPEG => {
let file =
std::fs::File::create(path).map_err(|e| IoError::FileError(e.to_string()))?;
let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(
file,
compression.quality.value(),
);
if compression.progressive {
}
encoder
.encode(
dynamic_img.as_bytes(),
width_ as u32,
height as u32,
image::ColorType::Rgb8.into(),
)
.map_err(|e| IoError::FileError(e.to_string()))?;
}
ImageFormat::WEBP => {
if compression.quality == CompressionQuality::Lossless {
dynamic_img
.save_with_format(path, image::ImageFormat::WebP)
.map_err(|e| IoError::FileError(e.to_string()))?;
} else {
dynamic_img
.save_with_format(path, image::ImageFormat::WebP)
.map_err(|e| IoError::FileError(e.to_string()))?;
}
}
_ => {
dynamic_img
.save_with_format(path, format.into())
.map_err(|e| IoError::FileError(e.to_string()))?;
}
}
Ok(())
}
pub fn to_grayscale(&self, image: &ImageData) -> Result<ImageData> {
let (height, width, channels) = image.data.dim();
if channels != 3 {
return Err(IoError::FormatError("Expected RGB image".to_string()));
}
let mut gray_data = Array3::zeros((height, width, 3));
for y in 0..height {
let _row_size = width * 3;
let mut r_values = vec![0f32; width];
let mut g_values = vec![0f32; width];
let mut b_values = vec![0f32; width];
for x in 0..width {
r_values[x] = image.data[[y, x, 0]] as f32;
g_values[x] = image.data[[y, x, 1]] as f32;
b_values[x] = image.data[[y, x, 2]] as f32;
}
let r_coeff = vec![0.299f32; width];
let g_coeff = vec![0.587f32; width];
let b_coeff = vec![0.114f32; width];
let _r_weighted = vec![0f32; width];
let _g_weighted = vec![0f32; width];
let _b_weighted = vec![0f32; width];
let r_values_view = ArrayView1::from(&r_values);
let g_values_view = ArrayView1::from(&g_values);
let b_values_view = ArrayView1::from(&b_values);
let r_coeff_view = ArrayView1::from(&r_coeff);
let g_coeff_view = ArrayView1::from(&g_coeff);
let b_coeff_view = ArrayView1::from(&b_coeff);
let r_weighted = f32::simd_mul(&r_values_view, &r_coeff_view);
let g_weighted = f32::simd_mul(&g_values_view, &g_coeff_view);
let b_weighted = f32::simd_mul(&b_values_view, &b_coeff_view);
let r_weighted_view = ArrayView1::from(&r_weighted);
let g_weighted_view = ArrayView1::from(&g_weighted);
let b_weighted_view = ArrayView1::from(&b_weighted);
let gray_values = f32::simd_add(&r_weighted_view, &g_weighted_view);
let gray_values_view = ArrayView1::from(&gray_values);
let gray_values_final = f32::simd_add(&gray_values_view, &b_weighted_view);
for x in 0..width {
let gray = gray_values_final[x].clamp(0.0, 255.0) as u8;
gray_data[[y, x, 0]] = gray;
gray_data[[y, x, 1]] = gray;
gray_data[[y, x, 2]] = gray;
}
}
let mut new_metadata = image.metadata.clone();
new_metadata.color_mode = ColorMode::Grayscale;
Ok(ImageData {
data: gray_data,
metadata: new_metadata,
})
}
pub fn histogram_equalization(&self, image: &ImageData) -> Result<ImageData> {
let (height, width, channels) = image.data.dim();
let mut enhanced_data = image.data.clone();
for c in 0..channels {
let mut histogram = [0u32; 256];
for y in 0..height {
for x in 0..width {
let pixel = image.data[[y, x, c]] as usize;
histogram[pixel] += 1;
}
}
let mut cdf = [0u32; 256];
cdf[0] = histogram[0];
for i in 1..256 {
cdf[i] = cdf[i - 1] + histogram[i];
}
let total_pixels = (height * width) as f32;
let mut lookup = [0u8; 256];
for i in 0..256 {
lookup[i] = ((cdf[i] as f32 / total_pixels) * 255.0) as u8;
}
for y in 0..height {
for x in 0..width {
let pixel = image.data[[y, x, c]] as usize;
enhanced_data[[y, x, c]] = lookup[pixel];
}
}
}
Ok(ImageData {
data: enhanced_data,
metadata: image.metadata.clone(),
})
}
pub fn gaussian_blur(&self, image: &ImageData, radius: f32) -> Result<ImageData> {
let (height, width_, _) = image.data.dim();
let raw_data = image.data.iter().cloned().collect::<Vec<u8>>();
let img_buffer = image::RgbImage::from_raw(width_ as u32, height as u32, raw_data)
.ok_or_else(|| IoError::FormatError("Invalid image dimensions".to_string()))?;
let dynamic_img = image::DynamicImage::ImageRgb8(img_buffer);
let blurred = dynamic_img.blur(radius);
let rgb_blurred = blurred.to_rgb8();
let blurred_raw = rgb_blurred.into_raw();
let blurred_data = Array3::from_shape_vec((height, width_, 3), blurred_raw)
.map_err(|e| IoError::FormatError(e.to_string()))?;
Ok(ImageData {
data: blurred_data,
metadata: image.metadata.clone(),
})
}
pub fn sharpen(&self, image: &ImageData, amount: f32, radius: f32) -> Result<ImageData> {
let blurred = self.gaussian_blur(image, radius)?;
let (height, width, channels) = image.data.dim();
let mut sharpened_data = Array3::zeros((height, width, channels));
for y in 0..height {
for x in 0..width {
for c in 0..channels {
let original = image.data[[y, x, c]] as f32;
let blur = blurred.data[[y, x, c]] as f32;
let difference = original - blur;
let sharpened = original + amount * difference;
sharpened_data[[y, x, c]] = sharpened.clamp(0.0, 255.0) as u8;
}
}
}
Ok(ImageData {
data: sharpened_data,
metadata: image.metadata.clone(),
})
}
pub fn clear_cache(&mut self) {
self.cache.clear();
}
pub fn cache_stats(&self) -> (usize, usize) {
(self.cache.len(), self.max_cache_size)
}
}
impl From<ImageFormat> for image::ImageFormat {
fn from(format: ImageFormat) -> Self {
match format {
ImageFormat::PNG => image::ImageFormat::Png,
ImageFormat::JPEG => image::ImageFormat::Jpeg,
ImageFormat::BMP => image::ImageFormat::Bmp,
ImageFormat::TIFF => image::ImageFormat::Tiff,
ImageFormat::GIF => image::ImageFormat::Gif,
ImageFormat::WEBP => image::ImageFormat::WebP,
ImageFormat::Other => image::ImageFormat::Png, }
}
}
impl ImagePyramid {
pub fn get_level(&self, level: usize) -> Option<&ImageData> {
if level == 0 {
Some(&self.original)
} else {
self.levels.get(level - 1)
}
}
pub fn num_levels(&self) -> usize {
self.levels.len() + 1
}
pub fn find_best_level(&self, target_width: u32, target_height: u32) -> usize {
let mut best_level = 0;
let mut best_diff = u32::MAX;
for level in 0..self.num_levels() {
if let Some(level_image) = self.get_level(level) {
let width_diff = level_image.metadata.width.abs_diff(target_width);
let height_diff = level_image.metadata.height.abs_diff(target_height);
let total_diff = width_diff + height_diff;
if total_diff < best_diff {
best_diff = total_diff;
best_level = level;
}
}
}
best_level
}
pub fn get_level_for_size(&self, target_width: u32, target_height: u32) -> Option<&ImageData> {
let level = self.find_best_level(target_width, target_height);
self.get_level(level)
}
}
#[allow(dead_code)]
pub fn create_image_pyramid(image: &ImageData) -> Result<ImagePyramid> {
let processor = EnhancedImageProcessor::new();
processor.create_pyramid(image, PyramidConfig::default())
}
#[allow(dead_code)]
pub fn save_lossless<P: AsRef<Path>>(
image: &ImageData,
path: P,
format: ImageFormat,
) -> Result<()> {
let processor = EnhancedImageProcessor::new();
let compression = CompressionOptions {
quality: CompressionQuality::Lossless,
progressive: false,
optimize: true,
compression_level: None,
};
processor.save_with_compression(image, path, format, Some(compression))
}
#[allow(dead_code)]
pub fn save_high_quality<P: AsRef<Path>>(
image: &ImageData,
path: P,
format: ImageFormat,
) -> Result<()> {
let processor = EnhancedImageProcessor::new();
let compression = CompressionOptions {
quality: CompressionQuality::High,
progressive: true,
optimize: true,
compression_level: Some(9),
};
processor.save_with_compression(image, path, format, Some(compression))
}
#[allow(dead_code)]
pub fn batch_convert_with_compression<P1: AsRef<Path>, P2: AsRef<Path>>(
input_dir: P1,
output_dir: P2,
target_format: ImageFormat,
compression: CompressionOptions,
) -> Result<()> {
use crate::image::{find_images, load_image};
use std::fs;
let input_dir = input_dir.as_ref();
let output_dir = output_dir.as_ref();
fs::create_dir_all(output_dir).map_err(|e| IoError::FileError(e.to_string()))?;
let processor = EnhancedImageProcessor::new().with_compression(compression);
let image_files = find_images(input_dir, "*", false)?;
for input_path in image_files {
let file_stem = input_path
.file_stem()
.ok_or_else(|| IoError::FileError("Invalid file name".to_string()))?;
let output_filename = format!(
"{}.{}",
file_stem.to_string_lossy(),
target_format.extension()
);
let output_path = output_dir.join(output_filename);
let image_data = load_image(&input_path)?;
processor.save_with_compression(&image_data, &output_path, target_format, None)?;
println!(
"Converted: {} -> {} ({})",
input_path.display(),
output_path.display(),
target_format.extension().to_uppercase()
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::image::ImageMetadata;
use scirs2_core::ndarray::Array3;
fn create_test_image() -> ImageData {
let data = Array3::zeros((100, 100, 3));
let metadata = ImageMetadata {
width: 100,
height: 100,
color_mode: ColorMode::RGB,
format: ImageFormat::PNG,
file_size: 0,
exif: None,
};
ImageData { data, metadata }
}
#[test]
fn test_compression_quality_values() {
assert_eq!(CompressionQuality::Lossless.value(), 100);
assert_eq!(CompressionQuality::High.value(), 95);
assert_eq!(CompressionQuality::Medium.value(), 80);
assert_eq!(CompressionQuality::Low.value(), 60);
assert_eq!(CompressionQuality::Custom(75).value(), 75);
assert_eq!(CompressionQuality::Custom(150).value(), 100); }
#[test]
fn test_pyramid_config_default() {
let config = PyramidConfig::default();
assert_eq!(config.levels, 4);
assert_eq!(config.scale_factor, 0.5);
assert_eq!(config.min_size, 32);
assert_eq!(config.interpolation, InterpolationMethod::Lanczos);
}
#[test]
fn test_enhanced_processor_creation() {
let processor = EnhancedImageProcessor::new();
assert_eq!(processor.compression.quality.value(), 95); assert!(processor.compression.optimize);
}
#[test]
fn test_processor_with_compression() {
let compression = CompressionOptions {
quality: CompressionQuality::Lossless,
progressive: true,
optimize: false,
compression_level: Some(5),
};
let processor = EnhancedImageProcessor::new().with_compression(compression.clone());
assert_eq!(processor.compression.quality.value(), 100);
assert!(processor.compression.progressive);
assert!(!processor.compression.optimize);
assert_eq!(processor.compression.compression_level, Some(5));
}
#[test]
fn test_interpolation_methods() {
assert_eq!(InterpolationMethod::Nearest, InterpolationMethod::Nearest);
assert_ne!(InterpolationMethod::Nearest, InterpolationMethod::Linear);
}
#[test]
fn test_image_pyramid_creation() {
let image = create_test_image();
let config = PyramidConfig {
levels: 2,
scale_factor: 0.5,
min_size: 10,
interpolation: InterpolationMethod::Linear,
};
let processor = EnhancedImageProcessor::new();
let pyramid = processor
.create_pyramid(&image, config)
.expect("Operation failed");
assert_eq!(pyramid.original.metadata.width, 100);
assert_eq!(pyramid.original.metadata.height, 100);
assert!(pyramid.levels.len() <= 2);
}
#[test]
fn test_pyramid_level_access() {
let image = create_test_image();
let processor = EnhancedImageProcessor::new();
let pyramid = processor
.create_pyramid(&image, PyramidConfig::default())
.expect("Operation failed");
assert!(pyramid.get_level(0).is_some());
assert_eq!(
pyramid
.get_level(0)
.expect("Operation failed")
.metadata
.width,
100
);
assert!(pyramid.num_levels() >= 1);
}
#[test]
fn test_find_best_pyramid_level() {
let image = create_test_image();
let processor = EnhancedImageProcessor::new();
let pyramid = processor
.create_pyramid(&image, PyramidConfig::default())
.expect("Operation failed");
let best_level = pyramid.find_best_level(100, 100);
assert_eq!(best_level, 0);
let best_level = pyramid.find_best_level(10, 10);
assert!(best_level > 0 || pyramid.num_levels() == 1);
}
#[test]
fn test_cache_operations() {
let mut processor = EnhancedImageProcessor::new().with_cache_size(128);
let (count, max_size) = processor.cache_stats();
assert_eq!(count, 0);
assert_eq!(max_size, 128);
processor.clear_cache();
let count_ = processor.cache_stats();
assert_eq!(count, 0);
}
}