use super::types::*;
use async_trait::async_trait;
use bytes::Bytes;
use image::{imageops::FilterType, DynamicImage, ImageFormat as ImgFormat, ImageReader};
use std::io::Cursor;
#[async_trait]
pub trait Transformer: Send + Sync {
fn supports(&self, transformation: &TransformationType) -> bool;
async fn transform(
&self,
data: &[u8],
transformation: &TransformationType,
) -> Result<TransformationResult, TransformationError>;
}
pub struct ImageTransformer;
#[async_trait]
impl Transformer for ImageTransformer {
fn supports(&self, transformation: &TransformationType) -> bool {
matches!(transformation, TransformationType::Image(_))
}
async fn transform(
&self,
data: &[u8],
transformation: &TransformationType,
) -> Result<TransformationResult, TransformationError> {
let TransformationType::Image(params) = transformation else {
return Err(TransformationError::UnsupportedFormat(
"Not an image transformation".to_string(),
));
};
let img = ImageReader::new(Cursor::new(data))
.with_guessed_format()
.map_err(|e| TransformationError::ImageError(format!("Failed to guess format: {}", e)))?
.decode()
.map_err(|e| {
TransformationError::ImageError(format!("Failed to decode image: {}", e))
})?;
let original_width = img.width();
let original_height = img.height();
let mut transformed = img;
if params.width.is_some() || params.height.is_some() {
transformed = resize_image(transformed, params)?;
}
let (output_format, content_type) = if let Some(fmt) = params.format {
(img_format_to_image_format(fmt), fmt.content_type())
} else {
(ImgFormat::Png, "image/png")
};
let mut output = Vec::new();
transformed
.write_to(&mut Cursor::new(&mut output), output_format)
.map_err(|e| {
TransformationError::ImageError(format!("Failed to encode image: {}", e))
})?;
let output_len = output.len();
let mut result = TransformationResult::new(Bytes::from(output), content_type);
result = result
.with_metadata("original_width", original_width.to_string())
.with_metadata("original_height", original_height.to_string())
.with_metadata("transformed_width", transformed.width().to_string())
.with_metadata("transformed_height", transformed.height().to_string());
if let Some(quality) = params.quality {
result = result.with_metadata("quality", quality.to_string());
}
result = result.with_metadata("resize_mode", format!("{:?}", params.resize_mode));
result = result.with_metadata("output_size", output_len.to_string());
Ok(result)
}
}
fn resize_image(
img: DynamicImage,
params: &ImageTransformParams,
) -> Result<DynamicImage, TransformationError> {
let original_width = img.width();
let original_height = img.height();
let (target_width, target_height) = match params.resize_mode {
ResizeMode::ByWidth => {
let width = params.width.ok_or_else(|| {
TransformationError::InvalidParameters(
"Width required for ByWidth mode".to_string(),
)
})?;
let ratio = width as f64 / original_width as f64;
let height = (original_height as f64 * ratio).round() as u32;
(width, height)
}
ResizeMode::ByHeight => {
let height = params.height.ok_or_else(|| {
TransformationError::InvalidParameters(
"Height required for ByHeight mode".to_string(),
)
})?;
let ratio = height as f64 / original_height as f64;
let width = (original_width as f64 * ratio).round() as u32;
(width, height)
}
ResizeMode::Exact => {
let width = params.width.unwrap_or(original_width);
let height = params.height.unwrap_or(original_height);
(width, height)
}
ResizeMode::Fit => {
let width = params.width.unwrap_or(original_width);
let height = params.height.unwrap_or(original_height);
let width_ratio = width as f64 / original_width as f64;
let height_ratio = height as f64 / original_height as f64;
let ratio = width_ratio.min(height_ratio);
let new_width = (original_width as f64 * ratio).round() as u32;
let new_height = (original_height as f64 * ratio).round() as u32;
(new_width, new_height)
}
ResizeMode::Fill => {
let width = params.width.unwrap_or(original_width);
let height = params.height.unwrap_or(original_height);
let width_ratio = width as f64 / original_width as f64;
let height_ratio = height as f64 / original_height as f64;
let ratio = width_ratio.max(height_ratio);
let new_width = (original_width as f64 * ratio).round() as u32;
let new_height = (original_height as f64 * ratio).round() as u32;
let resized = img.resize(new_width, new_height, FilterType::Lanczos3);
let x_offset = (new_width - width) / 2;
let y_offset = (new_height - height) / 2;
return Ok(resized.crop_imm(x_offset, y_offset, width, height));
}
};
Ok(img.resize(target_width, target_height, FilterType::Lanczos3))
}
fn img_format_to_image_format(fmt: ImageFormat) -> ImgFormat {
match fmt {
ImageFormat::Jpeg => ImgFormat::Jpeg,
ImageFormat::Png => ImgFormat::Png,
ImageFormat::WebP => ImgFormat::WebP,
ImageFormat::Gif => ImgFormat::Gif,
ImageFormat::Bmp => ImgFormat::Bmp,
ImageFormat::Tiff => ImgFormat::Tiff,
}
}