1use std::path::Path;
6
7#[derive(Clone, Debug)]
9pub enum ImageSource {
10 File(String),
12 Base64(String),
14 Bytes(Vec<u8>),
16 #[cfg(feature = "web2ppt")]
18 Url(String),
19}
20
21#[derive(Clone, Debug, Default)]
23pub struct Crop {
24 pub left: f64,
25 pub top: f64,
26 pub right: f64,
27 pub bottom: f64,
28}
29
30impl Crop {
31 pub fn new(left: f64, top: f64, right: f64, bottom: f64) -> Self {
33 Self { left, top, right, bottom }
34 }
35}
36
37#[derive(Clone, Debug)]
39pub enum ImageEffect {
40 Shadow,
42 Reflection,
44}
45
46#[derive(Clone, Debug)]
48pub struct Image {
49 pub filename: String,
50 pub width: u32, pub height: u32, pub x: u32, pub y: u32, pub format: String, pub source: Option<ImageSource>,
57 pub crop: Option<Crop>,
59 pub effects: Vec<ImageEffect>,
61}
62
63impl Image {
64 pub fn new(filename: &str, width: u32, height: u32, format: &str) -> Self {
66 Image {
67 filename: filename.to_string(),
68 width,
69 height,
70 x: 0,
71 y: 0,
72 format: format.to_uppercase(),
73 source: Some(ImageSource::File(filename.to_string())),
74 crop: None,
75 effects: Vec::new(),
76 }
77 }
78
79 pub fn from_path<P: AsRef<Path>>(path: P) -> std::result::Result<Self, String> {
81 let path = path.as_ref();
82 let filename = path.file_name().map(|s| s.to_string_lossy().to_string()).unwrap_or_else(|| "image.png".to_string());
83 let path_str = path.to_string_lossy().to_string();
84
85 let reader = ::image::io::Reader::open(path)
87 .map_err(|e| format!("Failed to open image: {}", e))?
88 .with_guessed_format()
89 .map_err(|e| format!("Failed to guess image format: {}", e))?;
90
91 let format = reader.format().map(|f| format!("{:?}", f)).unwrap_or("PNG".to_string());
92 let (w, h) = reader.into_dimensions()
93 .map_err(|e| format!("Failed to get image dimensions: {}", e))?;
94
95 let w_emu = w * 9525;
98 let h_emu = h * 9525;
99
100 Ok(Image {
101 filename,
102 width: w_emu,
103 height: h_emu,
104 x: 0,
105 y: 0,
106 format,
107 source: Some(ImageSource::File(path_str)),
108 crop: None,
109 effects: Vec::new(),
110 })
111 }
112
113 pub fn from_base64(data: &str, width: u32, height: u32, format: &str) -> Self {
128 let format_upper = format.to_uppercase();
129 let ext = match format_upper.as_str() {
130 "JPEG" => "jpg",
131 _ => &format_upper.to_lowercase(),
132 };
133 let filename = format!("image_{}.{}", uuid::Uuid::new_v4(), ext);
134
135 Image {
136 filename,
137 width,
138 height,
139 x: 0,
140 y: 0,
141 format: format_upper,
142 source: Some(ImageSource::Base64(data.to_string())),
143 crop: None,
144 effects: Vec::new(),
145 }
146 }
147
148 pub fn from_bytes(data: Vec<u8>, width: u32, height: u32, format: &str) -> Self {
150 let format_upper = format.to_uppercase();
151 let ext = match format_upper.as_str() {
152 "JPEG" => "jpg",
153 _ => &format_upper.to_lowercase(),
154 };
155 let filename = format!("image_{}.{}", uuid::Uuid::new_v4(), ext);
156
157 Image {
158 filename,
159 width,
160 height,
161 x: 0,
162 y: 0,
163 format: format_upper,
164 source: Some(ImageSource::Bytes(data)),
165 crop: None,
166 effects: Vec::new(),
167 }
168 }
169
170 #[cfg(feature = "web2ppt")]
172 pub fn from_url(url: &str, width: u32, height: u32, format: &str) -> Self {
173 let format_upper = format.to_uppercase();
174 let ext = match format_upper.as_str() {
175 "JPEG" => "jpg",
176 _ => &format_upper.to_lowercase(),
177 };
178 let filename = format!("image_{}.{}", uuid::Uuid::new_v4(), ext);
179
180 Image {
181 filename,
182 width,
183 height,
184 x: 0,
185 y: 0,
186 format: format_upper,
187 source: Some(ImageSource::Url(url.to_string())),
188 crop: None,
189 effects: Vec::new(),
190 }
191 }
192
193 pub fn get_bytes(&self) -> Option<Vec<u8>> {
195 match &self.source {
196 Some(ImageSource::Base64(data)) => {
197 base64_decode(data).ok()
199 }
200 Some(ImageSource::Bytes(data)) => Some(data.clone()),
201 Some(ImageSource::File(path)) => {
202 std::fs::read(path).ok()
203 }
204 #[cfg(feature = "web2ppt")]
205 Some(ImageSource::Url(url)) => {
206 let client = reqwest::blocking::Client::builder()
209 .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
210 .build()
211 .ok()?;
212
213 match client.get(url).send() {
214 Ok(resp) => {
215 if resp.status().is_success() {
216 resp.bytes().ok().map(|b| b.to_vec())
217 } else {
218 None
219 }
220 },
221 Err(_) => None,
222 }
223 }
224 None => None,
225 }
226 }
227
228 pub fn position(mut self, x: u32, y: u32) -> Self {
230 self.x = x;
231 self.y = y;
232 self
233 }
234
235 pub fn with_crop(mut self, left: f64, top: f64, right: f64, bottom: f64) -> Self {
237 self.crop = Some(Crop::new(left, top, right, bottom));
238 self
239 }
240
241 pub fn with_effect(mut self, effect: ImageEffect) -> Self {
243 self.effects.push(effect);
244 self
245 }
246
247 pub fn aspect_ratio(&self) -> f64 {
249 self.width as f64 / self.height as f64
250 }
251
252 pub fn scale_to_width(mut self, width: u32) -> Self {
254 let ratio = self.aspect_ratio();
255 self.width = width;
256 self.height = (width as f64 / ratio) as u32;
257 self
258 }
259
260 pub fn scale_to_height(mut self, height: u32) -> Self {
262 let ratio = self.aspect_ratio();
263 self.height = height;
264 self.width = (height as f64 * ratio) as u32;
265 self
266 }
267
268 pub fn extension(&self) -> String {
270 Path::new(&self.filename)
271 .extension()
272 .and_then(|ext| ext.to_str())
273 .map(|s| s.to_lowercase())
274 .unwrap_or_else(|| self.format.to_lowercase())
275 }
276
277 pub fn mime_type(&self) -> String {
279 match self.format.as_str() {
280 "PNG" => "image/png".to_string(),
281 "JPG" | "JPEG" => "image/jpeg".to_string(),
282 "GIF" => "image/gif".to_string(),
283 "BMP" => "image/bmp".to_string(),
284 "TIFF" => "image/tiff".to_string(),
285 "SVG" => "image/svg+xml".to_string(),
286 _ => "application/octet-stream".to_string(),
287 }
288 }
289}
290
291fn base64_decode(input: &str) -> Result<Vec<u8>, std::io::Error> {
293 const DECODE_TABLE: [i8; 128] = [
295 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
296 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
297 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
298 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
299 -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
300 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
301 -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
302 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
303 ];
304
305 let input = input.trim().replace(['\n', '\r', ' '], "");
306 let mut output = Vec::with_capacity(input.len() * 3 / 4);
307 let bytes: Vec<u8> = input.bytes().collect();
308
309 let mut i = 0;
310 while i < bytes.len() {
311 let mut buf = [0u8; 4];
312 let mut pad = 0;
313
314 for j in 0..4 {
315 if i + j >= bytes.len() {
316 buf[j] = 0;
317 pad += 1;
318 } else if bytes[i + j] == b'=' {
319 buf[j] = 0;
320 pad += 1;
321 } else if bytes[i + j] < 128 && DECODE_TABLE[bytes[i + j] as usize] >= 0 {
322 buf[j] = DECODE_TABLE[bytes[i + j] as usize] as u8;
323 } else {
324 return Err(std::io::Error::new(
325 std::io::ErrorKind::InvalidData,
326 "Invalid base64 character",
327 ));
328 }
329 }
330
331 output.push((buf[0] << 2) | (buf[1] >> 4));
332 if pad < 2 {
333 output.push((buf[1] << 4) | (buf[2] >> 2));
334 }
335 if pad < 1 {
336 output.push((buf[2] << 6) | buf[3]);
337 }
338
339 i += 4;
340 }
341
342 Ok(output)
343}
344
345pub struct ImageBuilder {
347 filename: String,
348 width: u32,
349 height: u32,
350 x: u32,
351 y: u32,
352 format: String,
353 source: Option<ImageSource>,
354}
355
356impl ImageBuilder {
357 pub fn new(filename: &str, width: u32, height: u32) -> Self {
359 let format = Path::new(filename)
360 .extension()
361 .and_then(|ext| ext.to_str())
362 .map(|s| s.to_uppercase())
363 .unwrap_or_else(|| "PNG".to_string());
364
365 ImageBuilder {
366 filename: filename.to_string(),
367 width,
368 height,
369 x: 0,
370 y: 0,
371 format,
372 source: Some(ImageSource::File(filename.to_string())),
373 }
374 }
375
376 pub fn from_base64(data: &str, width: u32, height: u32, format: &str) -> Self {
378 let format_upper = format.to_uppercase();
379 let ext = match format_upper.as_str() {
380 "JPEG" => "jpg",
381 _ => &format_upper.to_lowercase(),
382 };
383
384 ImageBuilder {
385 filename: format!("image.{}", ext),
386 width,
387 height,
388 x: 0,
389 y: 0,
390 format: format_upper,
391 source: Some(ImageSource::Base64(data.to_string())),
392 }
393 }
394
395 pub fn from_bytes(data: Vec<u8>, width: u32, height: u32, format: &str) -> Self {
397 let format_upper = format.to_uppercase();
398 let ext = match format_upper.as_str() {
399 "JPEG" => "jpg",
400 _ => &format_upper.to_lowercase(),
401 };
402
403 ImageBuilder {
404 filename: format!("image.{}", ext),
405 width,
406 height,
407 x: 0,
408 y: 0,
409 format: format_upper,
410 source: Some(ImageSource::Bytes(data)),
411 }
412 }
413
414 pub fn position(mut self, x: u32, y: u32) -> Self {
416 self.x = x;
417 self.y = y;
418 self
419 }
420
421 pub fn format(mut self, format: &str) -> Self {
423 self.format = format.to_uppercase();
424 self
425 }
426
427 pub fn scale_to_width(mut self, width: u32) -> Self {
429 let ratio = self.width as f64 / self.height as f64;
430 self.width = width;
431 self.height = (width as f64 / ratio) as u32;
432 self
433 }
434
435 pub fn scale_to_height(mut self, height: u32) -> Self {
437 let ratio = self.width as f64 / self.height as f64;
438 self.height = height;
439 self.width = (height as f64 * ratio) as u32;
440 self
441 }
442
443 pub fn build(self) -> Image {
445 Image {
446 filename: self.filename,
447 width: self.width,
448 height: self.height,
449 x: self.x,
450 y: self.y,
451 format: self.format,
452 source: self.source,
453 crop: None,
454 effects: Vec::new(),
455 }
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462
463 #[test]
464 fn test_image_creation() {
465 let img = Image::new("test.png", 1920, 1080, "PNG");
466 assert_eq!(img.filename, "test.png");
467 assert_eq!(img.width, 1920);
468 assert_eq!(img.height, 1080);
469 }
470
471 #[test]
472 fn test_image_position() {
473 let img = Image::new("test.png", 1920, 1080, "PNG")
474 .position(500000, 1000000);
475 assert_eq!(img.x, 500000);
476 assert_eq!(img.y, 1000000);
477 }
478
479 #[test]
480 fn test_image_aspect_ratio() {
481 let img = Image::new("test.png", 1920, 1080, "PNG");
482 let ratio = img.aspect_ratio();
483 assert!((ratio - 1.777).abs() < 0.01);
484 }
485
486 #[test]
487 fn test_image_scale_to_width() {
488 let img = Image::new("test.png", 1920, 1080, "PNG")
489 .scale_to_width(960);
490 assert_eq!(img.width, 960);
491 assert_eq!(img.height, 540);
492 }
493
494 #[test]
495 fn test_image_scale_to_height() {
496 let img = Image::new("test.png", 1920, 1080, "PNG")
497 .scale_to_height(540);
498 assert_eq!(img.width, 960);
499 assert_eq!(img.height, 540);
500 }
501
502 #[test]
503 fn test_image_extension() {
504 let img = Image::new("photo.jpg", 1920, 1080, "JPEG");
505 assert_eq!(img.extension(), "jpg");
506 }
507
508 #[test]
509 fn test_image_mime_types() {
510 assert_eq!(
511 Image::new("test.png", 100, 100, "PNG").mime_type(),
512 "image/png"
513 );
514 assert_eq!(
515 Image::new("test.jpg", 100, 100, "JPG").mime_type(),
516 "image/jpeg"
517 );
518 assert_eq!(
519 Image::new("test.gif", 100, 100, "GIF").mime_type(),
520 "image/gif"
521 );
522 }
523
524 #[test]
525 fn test_image_builder() {
526 let img = ImageBuilder::new("photo.png", 1920, 1080)
527 .position(500000, 1000000)
528 .scale_to_width(960)
529 .build();
530
531 assert_eq!(img.filename, "photo.png");
532 assert_eq!(img.width, 960);
533 assert_eq!(img.height, 540);
534 assert_eq!(img.x, 500000);
535 assert_eq!(img.y, 1000000);
536 }
537
538 #[test]
539 fn test_image_builder_auto_format() {
540 let img = ImageBuilder::new("photo.jpg", 1920, 1080).build();
541 assert_eq!(img.format, "JPG");
542 }
543
544 #[test]
545 fn test_image_from_base64() {
546 let base64_png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
548 let img = Image::from_base64(base64_png, 100, 100, "PNG");
549
550 assert!(img.filename.ends_with(".png"));
551 assert_eq!(img.format, "PNG");
552 assert!(matches!(img.source, Some(ImageSource::Base64(_))));
553 }
554
555 #[test]
556 fn test_image_from_bytes() {
557 let data = vec![0x89, 0x50, 0x4E, 0x47]; let img = Image::from_bytes(data.clone(), 100, 100, "PNG");
559
560 assert_eq!(img.format, "PNG");
561 assert!(matches!(img.source, Some(ImageSource::Bytes(_))));
562 }
563
564 #[test]
565 fn test_base64_decode() {
566 let result = base64_decode("SGVsbG8=").unwrap();
568 assert_eq!(result, b"Hello");
569
570 let result = base64_decode("SGVsbG8gV29ybGQ=").unwrap();
572 assert_eq!(result, b"Hello World");
573 }
574
575 #[test]
576 fn test_image_get_bytes_base64() {
577 let base64_png = "SGVsbG8="; let img = Image::from_base64(base64_png, 100, 100, "PNG");
579
580 let bytes = img.get_bytes().unwrap();
581 assert_eq!(bytes, b"Hello");
582 }
583
584 #[test]
585 fn test_image_builder_from_base64() {
586 let base64_data = "SGVsbG8=";
587 let img = ImageBuilder::from_base64(base64_data, 200, 150, "JPEG")
588 .position(1000, 2000)
589 .build();
590
591 assert_eq!(img.width, 200);
592 assert_eq!(img.height, 150);
593 assert_eq!(img.x, 1000);
594 assert_eq!(img.y, 2000);
595 assert_eq!(img.format, "JPEG");
596 }
597}