1use std::path::Path;
6use crate::core::{Positioned, ElementSized, Dimension};
7
8fn format_and_ext(format: &str) -> (String, String) {
10 let upper = format.to_uppercase();
11 let ext = match upper.as_str() {
12 "JPEG" => "jpg".to_string(),
13 _ => upper.to_lowercase(),
14 };
15 (upper, ext)
16}
17
18fn generate_image_filename(format: &str) -> (String, String) {
20 let (upper, ext) = format_and_ext(format);
21 let filename = format!("image_{}.{}", uuid::Uuid::new_v4(), ext);
22 (filename, upper)
23}
24
25#[derive(Clone, Debug)]
27pub enum ImageSource {
28 File(String),
30 Base64(String),
32 Bytes(Vec<u8>),
34 #[cfg(feature = "web2ppt")]
36 Url(String),
37}
38
39#[derive(Clone, Debug, Default)]
41pub struct Crop {
42 pub left: f64,
43 pub top: f64,
44 pub right: f64,
45 pub bottom: f64,
46}
47
48impl Crop {
49 pub fn new(left: f64, top: f64, right: f64, bottom: f64) -> Self {
51 Self { left, top, right, bottom }
52 }
53}
54
55#[derive(Clone, Debug)]
57pub enum ImageEffect {
58 Shadow,
60 Reflection,
62}
63
64#[derive(Clone, Debug)]
66pub struct Image {
67 pub filename: String,
68 pub width: u32, pub height: u32, pub x: u32, pub y: u32, pub format: String, pub source: Option<ImageSource>,
75 pub crop: Option<Crop>,
77 pub effects: Vec<ImageEffect>,
79}
80
81impl Image {
82 pub fn new(filename: &str, width: u32, height: u32, format: &str) -> Self {
84 Image {
85 filename: filename.to_string(),
86 width,
87 height,
88 x: 0,
89 y: 0,
90 format: format.to_uppercase(),
91 source: Some(ImageSource::File(filename.to_string())),
92 crop: None,
93 effects: Vec::new(),
94 }
95 }
96
97 pub fn from_path<P: AsRef<Path>>(path: P) -> std::result::Result<Self, String> {
99 let path = path.as_ref();
100 let filename = path.file_name().map(|s| s.to_string_lossy().to_string()).unwrap_or_else(|| "image.png".to_string());
101 let path_str = path.to_string_lossy().to_string();
102
103 let data = std::fs::read(path)
104 .map_err(|e| format!("Failed to open image: {e}"))?;
105 let (w, h, format) = read_image_dimensions(&data)
106 .ok_or_else(|| "Failed to detect image dimensions (unsupported format)".to_string())?;
107
108 let w_emu = w * 9525;
110 let h_emu = h * 9525;
111
112 Ok(Image {
113 filename,
114 width: w_emu,
115 height: h_emu,
116 x: 0,
117 y: 0,
118 format,
119 source: Some(ImageSource::File(path_str)),
120 crop: None,
121 effects: Vec::new(),
122 })
123 }
124
125 pub fn from_base64(data: &str, width: u32, height: u32, format: &str) -> Self {
140 let (filename, fmt) = generate_image_filename(format);
141 Self::with_source(filename, width, height, fmt, ImageSource::Base64(data.to_string()))
142 }
143
144 pub fn from_bytes(data: Vec<u8>, width: u32, height: u32, format: &str) -> Self {
146 let (filename, fmt) = generate_image_filename(format);
147 Self::with_source(filename, width, height, fmt, ImageSource::Bytes(data))
148 }
149
150 #[cfg(feature = "web2ppt")]
152 pub fn from_url(url: &str, width: u32, height: u32, format: &str) -> Self {
153 let (filename, fmt) = generate_image_filename(format);
154 Self::with_source(filename, width, height, fmt, ImageSource::Url(url.to_string()))
155 }
156
157 fn with_source(filename: String, width: u32, height: u32, format: String, source: ImageSource) -> Self {
159 Image {
160 filename,
161 width,
162 height,
163 x: 0,
164 y: 0,
165 format,
166 source: Some(source),
167 crop: None,
168 effects: Vec::new(),
169 }
170 }
171
172 pub fn get_bytes(&self) -> Option<Vec<u8>> {
174 match &self.source {
175 Some(ImageSource::Base64(data)) => {
176 base64_decode(data).ok()
178 }
179 Some(ImageSource::Bytes(data)) => Some(data.clone()),
180 Some(ImageSource::File(path)) => {
181 std::fs::read(path).ok()
182 }
183 #[cfg(feature = "web2ppt")]
184 Some(ImageSource::Url(url)) => {
185 let client = reqwest::blocking::Client::builder()
188 .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")
189 .build()
190 .ok()?;
191
192 match client.get(url).send() {
193 Ok(resp) => {
194 if resp.status().is_success() {
195 resp.bytes().ok().map(|b| b.to_vec())
196 } else {
197 None
198 }
199 },
200 Err(_) => None,
201 }
202 }
203 None => None,
204 }
205 }
206
207 pub fn position(mut self, x: u32, y: u32) -> Self {
209 self.x = x;
210 self.y = y;
211 self
212 }
213
214 pub fn with_crop(mut self, left: f64, top: f64, right: f64, bottom: f64) -> Self {
216 self.crop = Some(Crop::new(left, top, right, bottom));
217 self
218 }
219
220 pub fn with_effect(mut self, effect: ImageEffect) -> Self {
222 self.effects.push(effect);
223 self
224 }
225
226 pub fn aspect_ratio(&self) -> f64 {
228 self.width as f64 / self.height as f64
229 }
230
231 pub fn scale_to_width(mut self, width: u32) -> Self {
233 let ratio = self.aspect_ratio();
234 self.width = width;
235 self.height = (width as f64 / ratio) as u32;
236 self
237 }
238
239 pub fn scale_to_height(mut self, height: u32) -> Self {
241 let ratio = self.aspect_ratio();
242 self.height = height;
243 self.width = (height as f64 * ratio) as u32;
244 self
245 }
246
247 pub fn extension(&self) -> String {
249 Path::new(&self.filename)
250 .extension()
251 .and_then(|ext| ext.to_str())
252 .map(|s| s.to_lowercase())
253 .unwrap_or_else(|| self.format.to_lowercase())
254 }
255
256 pub fn mime_type(&self) -> String {
258 match self.format.as_str() {
259 "PNG" => "image/png".to_string(),
260 "JPG" | "JPEG" => "image/jpeg".to_string(),
261 "GIF" => "image/gif".to_string(),
262 "BMP" => "image/bmp".to_string(),
263 "TIFF" => "image/tiff".to_string(),
264 "SVG" => "image/svg+xml".to_string(),
265 _ => "application/octet-stream".to_string(),
266 }
267 }
268
269 pub fn at(mut self, x: Dimension, y: Dimension) -> Self {
271 self.x = x.to_emu_x();
272 self.y = y.to_emu_y();
273 self
274 }
275
276 pub fn with_dimensions(mut self, width: Dimension, height: Dimension) -> Self {
278 self.width = width.to_emu_x();
279 self.height = height.to_emu_y();
280 self
281 }
282}
283
284impl Positioned for Image {
285 fn x(&self) -> u32 { self.x }
286 fn y(&self) -> u32 { self.y }
287 fn set_position(&mut self, x: u32, y: u32) {
288 self.x = x;
289 self.y = y;
290 }
291}
292
293impl ElementSized for Image {
294 fn width(&self) -> u32 { self.width }
295 fn height(&self) -> u32 { self.height }
296 fn set_size(&mut self, width: u32, height: u32) {
297 self.width = width;
298 self.height = height;
299 }
300}
301
302fn base64_decode(input: &str) -> Result<Vec<u8>, std::io::Error> {
304 const DECODE_TABLE: [i8; 128] = [
306 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
307 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
308 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
309 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
310 -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
311 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
312 -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
313 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
314 ];
315
316 let input = input.trim().replace(['\n', '\r', ' '], "");
317 let mut output = Vec::with_capacity(input.len() * 3 / 4);
318 let bytes: Vec<u8> = input.bytes().collect();
319
320 let mut i = 0;
321 while i < bytes.len() {
322 let mut buf = [0u8; 4];
323 let mut pad = 0;
324
325 for j in 0..4 {
326 if i + j >= bytes.len() || bytes[i + j] == b'=' {
327 buf[j] = 0;
328 pad += 1;
329 } else if bytes[i + j] < 128 && DECODE_TABLE[bytes[i + j] as usize] >= 0 {
330 buf[j] = DECODE_TABLE[bytes[i + j] as usize] as u8;
331 } else {
332 return Err(std::io::Error::new(
333 std::io::ErrorKind::InvalidData,
334 "Invalid base64 character",
335 ));
336 }
337 }
338
339 output.push((buf[0] << 2) | (buf[1] >> 4));
340 if pad < 2 {
341 output.push((buf[1] << 4) | (buf[2] >> 2));
342 }
343 if pad < 1 {
344 output.push((buf[2] << 6) | buf[3]);
345 }
346
347 i += 4;
348 }
349
350 Ok(output)
351}
352
353pub struct ImageBuilder {
355 filename: String,
356 width: u32,
357 height: u32,
358 x: u32,
359 y: u32,
360 format: String,
361 source: Option<ImageSource>,
362}
363
364impl ImageBuilder {
365 pub fn new(filename: &str, width: u32, height: u32) -> Self {
367 let format = Path::new(filename)
368 .extension()
369 .and_then(|ext| ext.to_str())
370 .map(|s| s.to_uppercase())
371 .unwrap_or_else(|| "PNG".to_string());
372
373 ImageBuilder {
374 filename: filename.to_string(),
375 width,
376 height,
377 x: 0,
378 y: 0,
379 format,
380 source: Some(ImageSource::File(filename.to_string())),
381 }
382 }
383
384 pub fn from_base64(data: &str, width: u32, height: u32, format: &str) -> Self {
386 let (upper, ext) = format_and_ext(format);
387 ImageBuilder {
388 filename: format!("image.{}", ext),
389 width, height, x: 0, y: 0,
390 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 (upper, ext) = format_and_ext(format);
398 ImageBuilder {
399 filename: format!("image.{}", ext),
400 width, height, x: 0, y: 0,
401 format: upper,
402 source: Some(ImageSource::Bytes(data)),
403 }
404 }
405
406 pub fn position(mut self, x: u32, y: u32) -> Self {
408 self.x = x;
409 self.y = y;
410 self
411 }
412
413 pub fn format(mut self, format: &str) -> Self {
415 self.format = format.to_uppercase();
416 self
417 }
418
419 pub fn scale_to_width(mut self, width: u32) -> Self {
421 let ratio = self.width as f64 / self.height as f64;
422 self.width = width;
423 self.height = (width as f64 / ratio) as u32;
424 self
425 }
426
427 pub fn scale_to_height(mut self, height: u32) -> Self {
429 let ratio = self.width as f64 / self.height as f64;
430 self.height = height;
431 self.width = (height as f64 * ratio) as u32;
432 self
433 }
434
435 pub fn build(self) -> Image {
437 Image {
438 filename: self.filename,
439 width: self.width,
440 height: self.height,
441 x: self.x,
442 y: self.y,
443 format: self.format,
444 source: self.source,
445 crop: None,
446 effects: Vec::new(),
447 }
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454
455 #[test]
456 fn test_image_creation() {
457 let img = Image::new("test.png", 1920, 1080, "PNG");
458 assert_eq!(img.filename, "test.png");
459 assert_eq!(img.width, 1920);
460 assert_eq!(img.height, 1080);
461 }
462
463 #[test]
464 fn test_image_position() {
465 let img = Image::new("test.png", 1920, 1080, "PNG")
466 .position(500000, 1000000);
467 assert_eq!(img.x, 500000);
468 assert_eq!(img.y, 1000000);
469 }
470
471 #[test]
472 fn test_image_aspect_ratio() {
473 let img = Image::new("test.png", 1920, 1080, "PNG");
474 let ratio = img.aspect_ratio();
475 assert!((ratio - 1.777).abs() < 0.01);
476 }
477
478 #[test]
479 fn test_image_scale_to_width() {
480 let img = Image::new("test.png", 1920, 1080, "PNG")
481 .scale_to_width(960);
482 assert_eq!(img.width, 960);
483 assert_eq!(img.height, 540);
484 }
485
486 #[test]
487 fn test_image_scale_to_height() {
488 let img = Image::new("test.png", 1920, 1080, "PNG")
489 .scale_to_height(540);
490 assert_eq!(img.width, 960);
491 assert_eq!(img.height, 540);
492 }
493
494 #[test]
495 fn test_image_extension() {
496 let img = Image::new("photo.jpg", 1920, 1080, "JPEG");
497 assert_eq!(img.extension(), "jpg");
498 }
499
500 #[test]
501 fn test_image_mime_types() {
502 assert_eq!(
503 Image::new("test.png", 100, 100, "PNG").mime_type(),
504 "image/png"
505 );
506 assert_eq!(
507 Image::new("test.jpg", 100, 100, "JPG").mime_type(),
508 "image/jpeg"
509 );
510 assert_eq!(
511 Image::new("test.gif", 100, 100, "GIF").mime_type(),
512 "image/gif"
513 );
514 }
515
516 #[test]
517 fn test_image_builder() {
518 let img = ImageBuilder::new("photo.png", 1920, 1080)
519 .position(500000, 1000000)
520 .scale_to_width(960)
521 .build();
522
523 assert_eq!(img.filename, "photo.png");
524 assert_eq!(img.width, 960);
525 assert_eq!(img.height, 540);
526 assert_eq!(img.x, 500000);
527 assert_eq!(img.y, 1000000);
528 }
529
530 #[test]
531 fn test_image_builder_auto_format() {
532 let img = ImageBuilder::new("photo.jpg", 1920, 1080).build();
533 assert_eq!(img.format, "JPG");
534 }
535
536 #[test]
537 fn test_image_from_base64() {
538 let base64_png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
540 let img = Image::from_base64(base64_png, 100, 100, "PNG");
541
542 assert!(img.filename.ends_with(".png"));
543 assert_eq!(img.format, "PNG");
544 assert!(matches!(img.source, Some(ImageSource::Base64(_))));
545 }
546
547 #[test]
548 fn test_image_from_bytes() {
549 let data = vec![0x89, 0x50, 0x4E, 0x47]; let img = Image::from_bytes(data.clone(), 100, 100, "PNG");
551
552 assert_eq!(img.format, "PNG");
553 assert!(matches!(img.source, Some(ImageSource::Bytes(_))));
554 }
555
556 #[test]
557 fn test_base64_decode() {
558 let result = base64_decode("SGVsbG8=").unwrap();
560 assert_eq!(result, b"Hello");
561
562 let result = base64_decode("SGVsbG8gV29ybGQ=").unwrap();
564 assert_eq!(result, b"Hello World");
565 }
566
567 #[test]
568 fn test_image_get_bytes_base64() {
569 let base64_png = "SGVsbG8="; let img = Image::from_base64(base64_png, 100, 100, "PNG");
571
572 let bytes = img.get_bytes().unwrap();
573 assert_eq!(bytes, b"Hello");
574 }
575
576 #[test]
577 fn test_image_builder_from_base64() {
578 let base64_data = "SGVsbG8=";
579 let img = ImageBuilder::from_base64(base64_data, 200, 150, "JPEG")
580 .position(1000, 2000)
581 .build();
582
583 assert_eq!(img.width, 200);
584 assert_eq!(img.height, 150);
585 assert_eq!(img.x, 1000);
586 assert_eq!(img.y, 2000);
587 assert_eq!(img.format, "JPEG");
588 }
589
590 #[test]
591 fn test_read_png_dimensions() {
592 let png: Vec<u8> = vec![
594 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, ];
601 let (w, h, fmt) = read_image_dimensions(&png).unwrap();
602 assert_eq!((w, h), (1, 1));
603 assert_eq!(fmt, "PNG");
604 }
605
606 #[test]
607 fn test_read_gif_dimensions() {
608 let gif: Vec<u8> = vec![
609 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x0A, 0x00, 0x14, 0x00, ];
613 let (w, h, fmt) = read_image_dimensions(&gif).unwrap();
614 assert_eq!((w, h), (10, 20));
615 assert_eq!(fmt, "GIF");
616 }
617
618 #[test]
619 fn test_read_bmp_dimensions() {
620 let mut bmp = vec![0u8; 26];
621 bmp[0] = 0x42; bmp[1] = 0x4D; bmp[18..22].copy_from_slice(&100u32.to_le_bytes()); bmp[22..26].copy_from_slice(&200u32.to_le_bytes()); let (w, h, fmt) = read_image_dimensions(&bmp).unwrap();
625 assert_eq!((w, h), (100, 200));
626 assert_eq!(fmt, "BMP");
627 }
628}
629
630fn read_image_dimensions(data: &[u8]) -> Option<(u32, u32, String)> {
633 if data.len() < 10 {
634 return None;
635 }
636 if data.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) && data.len() >= 24 {
638 let w = u32::from_be_bytes([data[16], data[17], data[18], data[19]]);
639 let h = u32::from_be_bytes([data[20], data[21], data[22], data[23]]);
640 return Some((w, h, "PNG".into()));
641 }
642 if data.starts_with(&[0xFF, 0xD8]) {
644 return read_jpeg_dimensions(data);
645 }
646 if data.starts_with(b"GIF8") && data.len() >= 10 {
648 let w = u16::from_le_bytes([data[6], data[7]]) as u32;
649 let h = u16::from_le_bytes([data[8], data[9]]) as u32;
650 return Some((w, h, "GIF".into()));
651 }
652 if data.starts_with(b"BM") && data.len() >= 26 {
654 let w = u32::from_le_bytes([data[18], data[19], data[20], data[21]]);
655 let h = u32::from_le_bytes([data[22], data[23], data[24], data[25]]);
656 return Some((w, h, "BMP".into()));
657 }
658 if data.len() >= 30 && &data[0..4] == b"RIFF" && &data[8..12] == b"WEBP" {
660 if &data[12..16] == b"VP8 " && data.len() >= 30 {
662 let w = u16::from_le_bytes([data[26], data[27]]) as u32 & 0x3FFF;
663 let h = u16::from_le_bytes([data[28], data[29]]) as u32 & 0x3FFF;
664 return Some((w, h, "WEBP".into()));
665 }
666 if &data[12..16] == b"VP8L" && data.len() >= 25 {
668 let b0 = data[21] as u32;
669 let b1 = data[22] as u32;
670 let b2 = data[23] as u32;
671 let b3 = data[24] as u32;
672 let bits = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
673 let w = (bits & 0x3FFF) + 1;
674 let h = ((bits >> 14) & 0x3FFF) + 1;
675 return Some((w, h, "WEBP".into()));
676 }
677 }
678 None
679}
680
681fn read_jpeg_dimensions(data: &[u8]) -> Option<(u32, u32, String)> {
683 let mut i = 2;
684 while i + 1 < data.len() {
685 if data[i] != 0xFF {
686 i += 1;
687 continue;
688 }
689 let marker = data[i + 1];
690 i += 2;
691 if (marker == 0xC0 || marker == 0xC2) && i + 7 < data.len() {
693 let h = u16::from_be_bytes([data[i + 3], data[i + 4]]) as u32;
694 let w = u16::from_be_bytes([data[i + 5], data[i + 6]]) as u32;
695 return Some((w, h, "JPEG".into()));
696 }
697 if marker >= 0xC0 && marker != 0xD9 && marker != 0xDA && i + 1 < data.len() {
699 let len = u16::from_be_bytes([data[i], data[i + 1]]) as usize;
700 i += len;
701 }
702 }
703 None
704}