1use image::{DynamicImage, ImageFormat};
32use std::io::Cursor;
33
34#[derive(Debug, Clone, Copy)]
36pub enum OutputFormat {
37 Png,
39 Jpeg { quality: u8 },
41 WebP { quality: u8 },
43}
44
45#[derive(Debug, Clone)]
47pub struct ImageProcessingOptions {
48 pub max_width: Option<u32>,
50 pub max_height: Option<u32>,
52 pub format: Option<OutputFormat>,
54 pub strip_exif: bool,
56}
57
58impl Default for ImageProcessingOptions {
59 fn default() -> Self {
60 Self {
61 max_width: None,
62 max_height: None,
63 format: None,
64 strip_exif: true,
65 }
66 }
67}
68
69pub fn process_image(input: &[u8], options: ImageProcessingOptions) -> Result<Vec<u8>, String> {
99 let mut img =
101 image::load_from_memory(input).map_err(|e| format!("Failed to load image: {}", e))?;
102
103 if options.max_width.is_some() || options.max_height.is_some() {
105 img = resize_image(img, options.max_width, options.max_height);
106 }
107
108 let output_bytes = encode_image(&img, options.format)?;
110
111 Ok(output_bytes)
112}
113
114fn resize_image(
116 img: DynamicImage,
117 max_width: Option<u32>,
118 max_height: Option<u32>,
119) -> DynamicImage {
120 let (orig_width, orig_height) = (img.width(), img.height());
121
122 let width_scale = max_width
124 .map(|mw| mw as f64 / orig_width as f64)
125 .unwrap_or(1.0);
126 let height_scale = max_height
127 .map(|mh| mh as f64 / orig_height as f64)
128 .unwrap_or(1.0);
129
130 let scale = width_scale.min(height_scale).min(1.0); if scale >= 1.0 {
133 return img;
135 }
136
137 let new_width = (orig_width as f64 * scale).round() as u32;
138 let new_height = (orig_height as f64 * scale).round() as u32;
139
140 img.resize(new_width, new_height, image::imageops::FilterType::Lanczos3)
141}
142
143fn encode_image(img: &DynamicImage, format: Option<OutputFormat>) -> Result<Vec<u8>, String> {
145 let mut buffer = Vec::new();
146 let mut cursor = Cursor::new(&mut buffer);
147
148 match format {
149 Some(OutputFormat::Png) => {
150 img.write_to(&mut cursor, ImageFormat::Png)
151 .map_err(|e| format!("Failed to encode PNG: {}", e))?;
152 }
153 Some(OutputFormat::Jpeg { quality }) => {
154 let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut cursor, quality);
156 img.write_with_encoder(encoder)
157 .map_err(|e| format!("Failed to encode JPEG: {}", e))?;
158 }
159 Some(OutputFormat::WebP { quality }) => {
160 #[cfg(feature = "webp")]
162 {
163 let encoder = image::codecs::webp::WebPEncoder::new_with_quality(
164 &mut cursor,
165 image::codecs::webp::WebPQuality::lossy(quality),
166 );
167 img.write_with_encoder(encoder)
168 .map_err(|e| format!("Failed to encode WebP: {}", e))?;
169 }
170 #[cfg(not(feature = "webp"))]
171 {
172 let _ = quality;
173 return Err("WebP encoding requires the 'webp' feature".to_string());
174 }
175 }
176 None => {
177 img.write_to(&mut cursor, ImageFormat::Png)
179 .map_err(|e| format!("Failed to encode image: {}", e))?;
180 }
181 }
182
183 Ok(buffer)
184}
185
186pub fn detect_image_mime(data: &[u8]) -> Option<&'static str> {
197 if data.len() < 8 {
199 return None;
200 }
201
202 if data.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
204 return Some("image/png");
205 }
206
207 if data.starts_with(&[0xFF, 0xD8, 0xFF]) {
209 return Some("image/jpeg");
210 }
211
212 if data.starts_with(&[0x47, 0x49, 0x46, 0x38]) {
214 return Some("image/gif");
215 }
216
217 if data.len() >= 12
219 && data.starts_with(&[0x52, 0x49, 0x46, 0x46])
220 && data[8..12].starts_with(&[0x57, 0x45, 0x42, 0x50])
221 {
222 return Some("image/webp");
223 }
224
225 if data.starts_with(&[0x42, 0x4D]) {
227 return Some("image/bmp");
228 }
229
230 None
231}
232
233pub fn get_image_dimensions(data: &[u8]) -> Result<(u32, u32), String> {
244 let img = image::load_from_memory(data).map_err(|e| format!("Failed to load image: {}", e))?;
245 Ok((img.width(), img.height()))
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn test_detect_image_mime_png() {
254 let png_header = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
255 assert_eq!(detect_image_mime(&png_header), Some("image/png"));
256 }
257
258 #[test]
259 fn test_detect_image_mime_jpeg() {
260 let jpeg_header = vec![0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46];
261 assert_eq!(detect_image_mime(&jpeg_header), Some("image/jpeg"));
262 }
263
264 #[test]
265 fn test_detect_image_mime_unknown() {
266 let unknown = vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
267 assert_eq!(detect_image_mime(&unknown), None);
268 }
269
270 #[test]
271 fn test_detect_image_mime_too_short() {
272 let short = vec![0x89, 0x50];
273 assert_eq!(detect_image_mime(&short), None);
274 }
275}