use image::{DynamicImage, ImageFormat};
use std::io::Cursor;
#[derive(Debug, Clone, Copy)]
pub enum OutputFormat {
Png,
Jpeg { quality: u8 },
WebP { quality: u8 },
}
#[derive(Debug, Clone)]
pub struct ImageProcessingOptions {
pub max_width: Option<u32>,
pub max_height: Option<u32>,
pub format: Option<OutputFormat>,
pub strip_exif: bool,
}
impl Default for ImageProcessingOptions {
fn default() -> Self {
Self {
max_width: None,
max_height: None,
format: None,
strip_exif: true,
}
}
}
pub fn process_image(input: &[u8], options: ImageProcessingOptions) -> Result<Vec<u8>, String> {
let mut img =
image::load_from_memory(input).map_err(|e| format!("Failed to load image: {}", e))?;
if options.max_width.is_some() || options.max_height.is_some() {
img = resize_image(img, options.max_width, options.max_height);
}
let output_bytes = encode_image(&img, options.format)?;
Ok(output_bytes)
}
fn resize_image(
img: DynamicImage,
max_width: Option<u32>,
max_height: Option<u32>,
) -> DynamicImage {
let (orig_width, orig_height) = (img.width(), img.height());
let width_scale = max_width
.map(|mw| mw as f64 / orig_width as f64)
.unwrap_or(1.0);
let height_scale = max_height
.map(|mh| mh as f64 / orig_height as f64)
.unwrap_or(1.0);
let scale = width_scale.min(height_scale).min(1.0);
if scale >= 1.0 {
return img;
}
let new_width = (orig_width as f64 * scale).round() as u32;
let new_height = (orig_height as f64 * scale).round() as u32;
img.resize(new_width, new_height, image::imageops::FilterType::Lanczos3)
}
fn encode_image(img: &DynamicImage, format: Option<OutputFormat>) -> Result<Vec<u8>, String> {
let mut buffer = Vec::new();
let mut cursor = Cursor::new(&mut buffer);
match format {
Some(OutputFormat::Png) => {
img.write_to(&mut cursor, ImageFormat::Png)
.map_err(|e| format!("Failed to encode PNG: {}", e))?;
}
Some(OutputFormat::Jpeg { quality }) => {
let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut cursor, quality);
img.write_with_encoder(encoder)
.map_err(|e| format!("Failed to encode JPEG: {}", e))?;
}
Some(OutputFormat::WebP { quality }) => {
#[cfg(feature = "webp")]
{
let encoder = image::codecs::webp::WebPEncoder::new_with_quality(
&mut cursor,
image::codecs::webp::WebPQuality::lossy(quality),
);
img.write_with_encoder(encoder)
.map_err(|e| format!("Failed to encode WebP: {}", e))?;
}
#[cfg(not(feature = "webp"))]
{
let _ = quality;
return Err("WebP encoding requires the 'webp' feature".to_string());
}
}
None => {
img.write_to(&mut cursor, ImageFormat::Png)
.map_err(|e| format!("Failed to encode image: {}", e))?;
}
}
Ok(buffer)
}
pub fn detect_image_mime(data: &[u8]) -> Option<&'static str> {
if data.len() < 8 {
return None;
}
if data.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
return Some("image/png");
}
if data.starts_with(&[0xFF, 0xD8, 0xFF]) {
return Some("image/jpeg");
}
if data.starts_with(&[0x47, 0x49, 0x46, 0x38]) {
return Some("image/gif");
}
if data.len() >= 12
&& data.starts_with(&[0x52, 0x49, 0x46, 0x46])
&& data[8..12].starts_with(&[0x57, 0x45, 0x42, 0x50])
{
return Some("image/webp");
}
if data.starts_with(&[0x42, 0x4D]) {
return Some("image/bmp");
}
None
}
pub fn get_image_dimensions(data: &[u8]) -> Result<(u32, u32), String> {
let img = image::load_from_memory(data).map_err(|e| format!("Failed to load image: {}", e))?;
Ok((img.width(), img.height()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_image_mime_png() {
let png_header = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
assert_eq!(detect_image_mime(&png_header), Some("image/png"));
}
#[test]
fn test_detect_image_mime_jpeg() {
let jpeg_header = vec![0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46];
assert_eq!(detect_image_mime(&jpeg_header), Some("image/jpeg"));
}
#[test]
fn test_detect_image_mime_unknown() {
let unknown = vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
assert_eq!(detect_image_mime(&unknown), None);
}
#[test]
fn test_detect_image_mime_too_short() {
let short = vec![0x89, 0x50];
assert_eq!(detect_image_mime(&short), None);
}
}