this repo has no description
1use image::{DynamicImage, ImageFormat, ImageReader, imageops::FilterType}; 2use std::io::Cursor; 3pub const THUMB_SIZE_FEED: u32 = 200; 4pub const THUMB_SIZE_FULL: u32 = 1000; 5#[derive(Debug, Clone)] 6pub struct ProcessedImage { 7 pub data: Vec<u8>, 8 pub mime_type: String, 9 pub width: u32, 10 pub height: u32, 11} 12#[derive(Debug, Clone)] 13pub struct ImageProcessingResult { 14 pub original: ProcessedImage, 15 pub thumbnail_feed: Option<ProcessedImage>, 16 pub thumbnail_full: Option<ProcessedImage>, 17} 18#[derive(Debug, thiserror::Error)] 19pub enum ImageError { 20 #[error("Failed to decode image: {0}")] 21 DecodeError(String), 22 #[error("Failed to encode image: {0}")] 23 EncodeError(String), 24 #[error("Unsupported image format: {0}")] 25 UnsupportedFormat(String), 26 #[error("Image too large: {width}x{height} exceeds maximum {max_dimension}")] 27 TooLarge { 28 width: u32, 29 height: u32, 30 max_dimension: u32, 31 }, 32 #[error("File too large: {size} bytes exceeds maximum {max_size} bytes")] 33 FileTooLarge { size: usize, max_size: usize }, 34} 35pub const DEFAULT_MAX_FILE_SIZE: usize = 10 * 1024 * 1024; // 10MB 36pub struct ImageProcessor { 37 max_dimension: u32, 38 max_file_size: usize, 39 output_format: OutputFormat, 40 generate_thumbnails: bool, 41} 42#[derive(Debug, Clone, Copy)] 43pub enum OutputFormat { 44 WebP, 45 Jpeg, 46 Png, 47 Original, 48} 49impl Default for ImageProcessor { 50 fn default() -> Self { 51 Self { 52 max_dimension: 4096, 53 max_file_size: DEFAULT_MAX_FILE_SIZE, 54 output_format: OutputFormat::WebP, 55 generate_thumbnails: true, 56 } 57 } 58} 59impl ImageProcessor { 60 pub fn new() -> Self { 61 Self::default() 62 } 63 pub fn with_max_dimension(mut self, max: u32) -> Self { 64 self.max_dimension = max; 65 self 66 } 67 pub fn with_max_file_size(mut self, max: usize) -> Self { 68 self.max_file_size = max; 69 self 70 } 71 pub fn with_output_format(mut self, format: OutputFormat) -> Self { 72 self.output_format = format; 73 self 74 } 75 pub fn with_thumbnails(mut self, generate: bool) -> Self { 76 self.generate_thumbnails = generate; 77 self 78 } 79 pub fn process(&self, data: &[u8], mime_type: &str) -> Result<ImageProcessingResult, ImageError> { 80 if data.len() > self.max_file_size { 81 return Err(ImageError::FileTooLarge { 82 size: data.len(), 83 max_size: self.max_file_size, 84 }); 85 } 86 let format = self.detect_format(mime_type, data)?; 87 let img = self.decode_image(data, format)?; 88 if img.width() > self.max_dimension || img.height() > self.max_dimension { 89 return Err(ImageError::TooLarge { 90 width: img.width(), 91 height: img.height(), 92 max_dimension: self.max_dimension, 93 }); 94 } 95 let original = self.encode_image(&img)?; 96 let thumbnail_feed = if self.generate_thumbnails && (img.width() > THUMB_SIZE_FEED || img.height() > THUMB_SIZE_FEED) { 97 Some(self.generate_thumbnail(&img, THUMB_SIZE_FEED)?) 98 } else { 99 None 100 }; 101 let thumbnail_full = if self.generate_thumbnails && (img.width() > THUMB_SIZE_FULL || img.height() > THUMB_SIZE_FULL) { 102 Some(self.generate_thumbnail(&img, THUMB_SIZE_FULL)?) 103 } else { 104 None 105 }; 106 Ok(ImageProcessingResult { 107 original, 108 thumbnail_feed, 109 thumbnail_full, 110 }) 111 } 112 fn detect_format(&self, mime_type: &str, data: &[u8]) -> Result<ImageFormat, ImageError> { 113 match mime_type.to_lowercase().as_str() { 114 "image/jpeg" | "image/jpg" => Ok(ImageFormat::Jpeg), 115 "image/png" => Ok(ImageFormat::Png), 116 "image/gif" => Ok(ImageFormat::Gif), 117 "image/webp" => Ok(ImageFormat::WebP), 118 _ => { 119 if let Ok(format) = image::guess_format(data) { 120 Ok(format) 121 } else { 122 Err(ImageError::UnsupportedFormat(mime_type.to_string())) 123 } 124 } 125 } 126 } 127 fn decode_image(&self, data: &[u8], format: ImageFormat) -> Result<DynamicImage, ImageError> { 128 let cursor = Cursor::new(data); 129 let reader = ImageReader::with_format(cursor, format); 130 reader 131 .decode() 132 .map_err(|e| ImageError::DecodeError(e.to_string())) 133 } 134 fn encode_image(&self, img: &DynamicImage) -> Result<ProcessedImage, ImageError> { 135 let (data, mime_type) = match self.output_format { 136 OutputFormat::WebP => { 137 let mut buf = Vec::new(); 138 img.write_to(&mut Cursor::new(&mut buf), ImageFormat::WebP) 139 .map_err(|e| ImageError::EncodeError(e.to_string()))?; 140 (buf, "image/webp".to_string()) 141 } 142 OutputFormat::Jpeg => { 143 let mut buf = Vec::new(); 144 img.write_to(&mut Cursor::new(&mut buf), ImageFormat::Jpeg) 145 .map_err(|e| ImageError::EncodeError(e.to_string()))?; 146 (buf, "image/jpeg".to_string()) 147 } 148 OutputFormat::Png => { 149 let mut buf = Vec::new(); 150 img.write_to(&mut Cursor::new(&mut buf), ImageFormat::Png) 151 .map_err(|e| ImageError::EncodeError(e.to_string()))?; 152 (buf, "image/png".to_string()) 153 } 154 OutputFormat::Original => { 155 let mut buf = Vec::new(); 156 img.write_to(&mut Cursor::new(&mut buf), ImageFormat::Png) 157 .map_err(|e| ImageError::EncodeError(e.to_string()))?; 158 (buf, "image/png".to_string()) 159 } 160 }; 161 Ok(ProcessedImage { 162 data, 163 mime_type, 164 width: img.width(), 165 height: img.height(), 166 }) 167 } 168 fn generate_thumbnail(&self, img: &DynamicImage, max_size: u32) -> Result<ProcessedImage, ImageError> { 169 let (orig_width, orig_height) = (img.width(), img.height()); 170 let (new_width, new_height) = if orig_width > orig_height { 171 let ratio = max_size as f64 / orig_width as f64; 172 (max_size, (orig_height as f64 * ratio) as u32) 173 } else { 174 let ratio = max_size as f64 / orig_height as f64; 175 ((orig_width as f64 * ratio) as u32, max_size) 176 }; 177 let thumb = img.resize(new_width, new_height, FilterType::Lanczos3); 178 self.encode_image(&thumb) 179 } 180 pub fn is_supported_mime_type(mime_type: &str) -> bool { 181 matches!( 182 mime_type.to_lowercase().as_str(), 183 "image/jpeg" | "image/jpg" | "image/png" | "image/gif" | "image/webp" 184 ) 185 } 186 pub fn strip_exif(data: &[u8]) -> Result<Vec<u8>, ImageError> { 187 let format = image::guess_format(data) 188 .map_err(|e| ImageError::DecodeError(e.to_string()))?; 189 let cursor = Cursor::new(data); 190 let img = ImageReader::with_format(cursor, format) 191 .decode() 192 .map_err(|e| ImageError::DecodeError(e.to_string()))?; 193 let mut buf = Vec::new(); 194 img.write_to(&mut Cursor::new(&mut buf), format) 195 .map_err(|e| ImageError::EncodeError(e.to_string()))?; 196 Ok(buf) 197 } 198} 199#[cfg(test)] 200mod tests { 201 use super::*; 202 fn create_test_image(width: u32, height: u32) -> Vec<u8> { 203 let img = DynamicImage::new_rgb8(width, height); 204 let mut buf = Vec::new(); 205 img.write_to(&mut Cursor::new(&mut buf), ImageFormat::Png).unwrap(); 206 buf 207 } 208 #[test] 209 fn test_process_small_image() { 210 let processor = ImageProcessor::new(); 211 let data = create_test_image(100, 100); 212 let result = processor.process(&data, "image/png").unwrap(); 213 assert!(result.thumbnail_feed.is_none()); 214 assert!(result.thumbnail_full.is_none()); 215 } 216 #[test] 217 fn test_process_large_image_generates_thumbnails() { 218 let processor = ImageProcessor::new(); 219 let data = create_test_image(2000, 1500); 220 let result = processor.process(&data, "image/png").unwrap(); 221 assert!(result.thumbnail_feed.is_some()); 222 assert!(result.thumbnail_full.is_some()); 223 let feed_thumb = result.thumbnail_feed.unwrap(); 224 assert!(feed_thumb.width <= THUMB_SIZE_FEED); 225 assert!(feed_thumb.height <= THUMB_SIZE_FEED); 226 let full_thumb = result.thumbnail_full.unwrap(); 227 assert!(full_thumb.width <= THUMB_SIZE_FULL); 228 assert!(full_thumb.height <= THUMB_SIZE_FULL); 229 } 230 #[test] 231 fn test_webp_conversion() { 232 let processor = ImageProcessor::new().with_output_format(OutputFormat::WebP); 233 let data = create_test_image(500, 500); 234 let result = processor.process(&data, "image/png").unwrap(); 235 assert_eq!(result.original.mime_type, "image/webp"); 236 } 237 #[test] 238 fn test_reject_too_large() { 239 let processor = ImageProcessor::new().with_max_dimension(1000); 240 let data = create_test_image(2000, 2000); 241 let result = processor.process(&data, "image/png"); 242 assert!(matches!(result, Err(ImageError::TooLarge { .. }))); 243 } 244 #[test] 245 fn test_is_supported_mime_type() { 246 assert!(ImageProcessor::is_supported_mime_type("image/jpeg")); 247 assert!(ImageProcessor::is_supported_mime_type("image/png")); 248 assert!(ImageProcessor::is_supported_mime_type("image/gif")); 249 assert!(ImageProcessor::is_supported_mime_type("image/webp")); 250 assert!(!ImageProcessor::is_supported_mime_type("image/bmp")); 251 assert!(!ImageProcessor::is_supported_mime_type("text/plain")); 252 } 253}