use super::types::{StorageError, StorageResult, UploadedFile};
use image::{
imageops::FilterType, DynamicImage, ImageFormat, ImageReader,
};
use std::io::Cursor;
#[derive(Debug, Clone)]
pub struct ImageProcessor {
filter: FilterType,
}
impl Default for ImageProcessor {
fn default() -> Self {
Self::new()
}
}
impl ImageProcessor {
#[must_use]
pub const fn new() -> Self {
Self {
filter: FilterType::Lanczos3,
}
}
#[must_use]
pub const fn with_filter(filter: FilterType) -> Self {
Self { filter }
}
fn load_image(file: &UploadedFile) -> StorageResult<DynamicImage> {
let reader = ImageReader::new(Cursor::new(&file.data))
.with_guessed_format()
.map_err(|e| StorageError::Other(format!("Failed to read image: {e}")))?;
reader
.decode()
.map_err(|e| StorageError::Other(format!("Failed to decode image: {e}")))
}
fn detect_format(file: &UploadedFile) -> StorageResult<ImageFormat> {
ImageFormat::from_mime_type(&file.content_type)
.ok_or_else(|| StorageError::Other(format!("Unsupported image format: {}", file.content_type)))
}
fn encode_image(
image: &DynamicImage,
format: ImageFormat,
) -> StorageResult<Vec<u8>> {
let mut buffer = Vec::new();
image
.write_to(&mut Cursor::new(&mut buffer), format)
.map_err(|e| StorageError::Other(format!("Failed to encode image: {e}")))?;
Ok(buffer)
}
pub fn generate_thumbnail(
&self,
file: &UploadedFile,
max_width: u32,
max_height: u32,
) -> StorageResult<UploadedFile> {
let img = Self::load_image(file)?;
let format = Self::detect_format(file)?;
let thumbnail = img.thumbnail(max_width, max_height);
let data = Self::encode_image(&thumbnail, format)?;
Ok(UploadedFile {
filename: format!("thumb_{}", file.filename),
content_type: file.content_type.clone(),
data,
})
}
pub fn resize(
&self,
file: &UploadedFile,
width: u32,
height: u32,
) -> StorageResult<UploadedFile> {
let img = Self::load_image(file)?;
let format = Self::detect_format(file)?;
let resized = img.resize_exact(width, height, self.filter);
let data = Self::encode_image(&resized, format)?;
Ok(UploadedFile {
filename: format!("{}x{}_{}", width, height, file.filename),
content_type: file.content_type.clone(),
data,
})
}
pub fn convert_format(
&self,
file: &UploadedFile,
target_format: &str,
) -> StorageResult<UploadedFile> {
let img = Self::load_image(file)?;
let format = ImageFormat::from_mime_type(target_format)
.ok_or_else(|| StorageError::Other(format!("Unsupported target format: {target_format}")))?;
let data = Self::encode_image(&img, format)?;
let new_filename = file.extension().map_or_else(
|| format!("{}.{}", file.filename, format_extension(format)),
|ext| file.filename.replace(&format!(".{ext}"), &format!(".{}", format_extension(format))),
);
Ok(UploadedFile {
filename: new_filename,
content_type: target_format.to_string(),
data,
})
}
pub fn strip_exif(&self, file: &UploadedFile) -> StorageResult<UploadedFile> {
let img = Self::load_image(file)?;
let format = Self::detect_format(file)?;
let data = Self::encode_image(&img, format)?;
Ok(UploadedFile {
filename: file.filename.clone(),
content_type: file.content_type.clone(),
data,
})
}
pub fn get_dimensions(&self, file: &UploadedFile) -> StorageResult<(u32, u32)> {
let reader = ImageReader::new(Cursor::new(&file.data))
.with_guessed_format()
.map_err(|e| StorageError::Other(format!("Failed to read image: {e}")))?;
reader
.into_dimensions()
.map_err(|e| StorageError::Other(format!("Failed to get dimensions: {e}")))
}
}
const fn format_extension(format: ImageFormat) -> &'static str {
match format {
ImageFormat::Png => "png",
ImageFormat::Jpeg => "jpg",
ImageFormat::Gif => "gif",
ImageFormat::WebP => "webp",
ImageFormat::Tiff => "tiff",
ImageFormat::Bmp => "bmp",
ImageFormat::Ico => "ico",
ImageFormat::Avif => "avif",
_ => "bin",
}
}
#[cfg(test)]
mod tests {
use super::*;
use image::{ImageBuffer, Rgb};
fn create_test_png(width: u32, height: u32) -> Vec<u8> {
let img: ImageBuffer<Rgb<u8>, Vec<u8>> = ImageBuffer::from_fn(width, height, |_, _| {
Rgb([255, 0, 0]) });
let mut buffer = Vec::new();
DynamicImage::ImageRgb8(img)
.write_to(&mut Cursor::new(&mut buffer), ImageFormat::Png)
.unwrap();
buffer
}
#[test]
fn test_get_dimensions() {
let png_data = create_test_png(10, 20);
let file = UploadedFile::new("test.png", "image/png", png_data);
let processor = ImageProcessor::new();
let (width, height) = processor.get_dimensions(&file).unwrap();
assert_eq!(width, 10);
assert_eq!(height, 20);
}
#[test]
fn test_load_image() {
let png_data = create_test_png(5, 5);
let file = UploadedFile::new("test.png", "image/png", png_data);
let img = ImageProcessor::load_image(&file).unwrap();
assert_eq!(img.width(), 5);
assert_eq!(img.height(), 5);
}
#[test]
fn test_strip_exif() {
let png_data = create_test_png(10, 10);
let file = UploadedFile::new("test.png", "image/png", png_data);
let processor = ImageProcessor::new();
let stripped = processor.strip_exif(&file).unwrap();
assert_eq!(stripped.content_type, "image/png");
assert!(!stripped.data.is_empty());
}
#[test]
fn test_resize() {
let png_data = create_test_png(20, 30);
let file = UploadedFile::new("test.png", "image/png", png_data);
let processor = ImageProcessor::new();
let resized = processor.resize(&file, 10, 15).unwrap();
assert_eq!(resized.content_type, "image/png");
let (width, height) = processor.get_dimensions(&resized).unwrap();
assert_eq!(width, 10);
assert_eq!(height, 15);
}
#[test]
fn test_thumbnail() {
let png_data = create_test_png(100, 100);
let file = UploadedFile::new("test.png", "image/png", png_data);
let processor = ImageProcessor::new();
let thumb = processor.generate_thumbnail(&file, 50, 50).unwrap();
assert_eq!(thumb.content_type, "image/png");
assert!(thumb.filename.starts_with("thumb_"));
let (width, height) = processor.get_dimensions(&thumb).unwrap();
assert!(width <= 50);
assert!(height <= 50);
}
#[test]
fn test_invalid_image() {
let file = UploadedFile::new("test.png", "image/png", b"not an image".to_vec());
let processor = ImageProcessor::new();
assert!(processor.get_dimensions(&file).is_err());
}
#[test]
fn test_format_extension() {
assert_eq!(format_extension(ImageFormat::Png), "png");
assert_eq!(format_extension(ImageFormat::Jpeg), "jpg");
assert_eq!(format_extension(ImageFormat::Gif), "gif");
assert_eq!(format_extension(ImageFormat::WebP), "webp");
}
}