1use std::path::Path;
6use crate::core::{Dimension, ElementPlacement, ElementSized, Positioned};
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 Glow,
64 SoftEdges,
66 InnerShadow,
68 Blur,
70}
71
72#[derive(Clone, Debug)]
74pub struct Image {
75 pub filename: String,
76 pub width: u32, pub height: u32, pub x: u32, pub y: u32, pub format: String, pub source: Option<ImageSource>,
83 pub crop: Option<Crop>,
85 pub effects: Vec<ImageEffect>,
87 pub alt_text: Option<String>,
89}
90
91impl Image {
92 pub fn new(filename: &str, width: u32, height: u32, format: &str) -> Self {
94 Image {
95 filename: filename.to_string(),
96 width,
97 height,
98 x: 0,
99 y: 0,
100 format: format.to_uppercase(),
101 source: Some(ImageSource::File(filename.to_string())),
102 crop: None,
103 effects: Vec::new(),
104 alt_text: None,
105 }
106 }
107
108 pub fn from_path<P: AsRef<Path>>(path: P) -> std::result::Result<Self, String> {
110 let path = path.as_ref();
111 let filename = path.file_name().map(|s| s.to_string_lossy().to_string()).unwrap_or_else(|| "image.png".to_string());
112 let path_str = path.to_string_lossy().to_string();
113
114 let data = std::fs::read(path)
115 .map_err(|e| format!("Failed to open image: {e}"))?;
116 let (w, h, format) = read_image_dimensions(&data)
117 .ok_or_else(|| "Failed to detect image dimensions (unsupported format)".to_string())?;
118
119 let w_emu = w * 9525;
121 let h_emu = h * 9525;
122
123 Ok(Image {
124 filename,
125 width: w_emu,
126 height: h_emu,
127 x: 0,
128 y: 0,
129 format,
130 source: Some(ImageSource::File(path_str)),
131 crop: None,
132 effects: Vec::new(),
133 alt_text: None,
134 })
135 }
136
137 pub fn from_base64(data: &str, width: u32, height: u32, format: &str) -> Self {
152 let (filename, fmt) = generate_image_filename(format);
153 Self::with_source(filename, width, height, fmt, ImageSource::Base64(data.to_string()))
154 }
155
156 pub fn from_bytes(data: Vec<u8>, width: u32, height: u32, format: &str) -> Self {
158 let (filename, fmt) = generate_image_filename(format);
159 Self::with_source(filename, width, height, fmt, ImageSource::Bytes(data))
160 }
161
162 #[cfg(feature = "web2ppt")]
164 pub fn from_url(url: &str, width: u32, height: u32, format: &str) -> Self {
165 let (filename, fmt) = generate_image_filename(format);
166 Self::with_source(filename, width, height, fmt, ImageSource::Url(url.to_string()))
167 }
168
169 fn with_source(filename: String, width: u32, height: u32, format: String, source: ImageSource) -> Self {
171 Image {
172 filename,
173 width,
174 height,
175 x: 0,
176 y: 0,
177 format,
178 source: Some(source),
179 crop: None,
180 effects: Vec::new(),
181 alt_text: None,
182 }
183 }
184
185 pub fn with_alt_text(mut self, alt: &str) -> Self {
187 self.alt_text = Some(alt.to_string());
188 self
189 }
190
191 pub fn get_bytes(&self) -> Option<Vec<u8>> {
193 match &self.source {
194 Some(ImageSource::Base64(data)) => {
195 base64_decode(data).ok()
197 }
198 Some(ImageSource::Bytes(data)) => Some(data.clone()),
199 Some(ImageSource::File(path)) => {
200 std::fs::read(path).ok()
201 }
202 #[cfg(feature = "web2ppt")]
203 Some(ImageSource::Url(url)) => {
204 let client = reqwest::blocking::Client::builder()
207 .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")
208 .build()
209 .ok()?;
210
211 match client.get(url).send() {
212 Ok(resp) => {
213 if resp.status().is_success() {
214 resp.bytes().ok().map(|b| b.to_vec())
215 } else {
216 None
217 }
218 },
219 Err(_) => None,
220 }
221 }
222 None => None,
223 }
224 }
225
226 pub fn position(mut self, x: u32, y: u32) -> Self {
228 self.x = x;
229 self.y = y;
230 self
231 }
232
233 pub fn with_crop(mut self, left: f64, top: f64, right: f64, bottom: f64) -> Self {
235 self.crop = Some(Crop::new(left, top, right, bottom));
236 self
237 }
238
239 pub fn with_effect(mut self, effect: ImageEffect) -> Self {
241 self.effects.push(effect);
242 self
243 }
244
245 pub fn aspect_ratio(&self) -> f64 {
247 self.width as f64 / self.height as f64
248 }
249
250 pub fn scale_to_width(mut self, width: u32) -> Self {
252 let ratio = self.aspect_ratio();
253 self.width = width;
254 self.height = (width as f64 / ratio) as u32;
255 self
256 }
257
258 pub fn scale_to_height(mut self, height: u32) -> Self {
260 let ratio = self.aspect_ratio();
261 self.height = height;
262 self.width = (height as f64 * ratio) as u32;
263 self
264 }
265
266 pub fn extension(&self) -> String {
268 Path::new(&self.filename)
269 .extension()
270 .and_then(|ext| ext.to_str())
271 .map(|s| s.to_lowercase())
272 .unwrap_or_else(|| self.format.to_lowercase())
273 }
274
275 pub fn mime_type(&self) -> String {
277 match self.format.as_str() {
278 "PNG" => "image/png".to_string(),
279 "JPG" | "JPEG" => "image/jpeg".to_string(),
280 "GIF" => "image/gif".to_string(),
281 "BMP" => "image/bmp".to_string(),
282 "TIFF" => "image/tiff".to_string(),
283 "SVG" => "image/svg+xml".to_string(),
284 _ => "application/octet-stream".to_string(),
285 }
286 }
287
288 pub fn at(mut self, x: Dimension, y: Dimension) -> Self {
290 self.x = x.to_emu_x();
291 self.y = y.to_emu_y();
292 self
293 }
294
295 pub fn with_dimensions(mut self, width: Dimension, height: Dimension) -> Self {
297 self.width = width.to_emu_x();
298 self.height = height.to_emu_y();
299 self
300 }
301}
302
303impl Positioned for Image {
304 fn x(&self) -> u32 { self.x }
305 fn y(&self) -> u32 { self.y }
306 fn set_position(&mut self, x: u32, y: u32) {
307 self.x = x;
308 self.y = y;
309 }
310}
311
312impl ElementSized for Image {
313 fn width(&self) -> u32 { self.width }
314 fn height(&self) -> u32 { self.height }
315 fn set_size(&mut self, width: u32, height: u32) {
316 self.width = width;
317 self.height = height;
318 }
319}
320
321fn base64_decode(input: &str) -> Result<Vec<u8>, std::io::Error> {
323 const DECODE_TABLE: [i8; 128] = [
325 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
326 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
327 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
328 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
329 -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
330 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
331 -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
332 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
333 ];
334
335 let input = input.trim().replace(['\n', '\r', ' '], "");
336 let mut output = Vec::with_capacity(input.len() * 3 / 4);
337 let bytes: Vec<u8> = input.bytes().collect();
338
339 let mut i = 0;
340 while i < bytes.len() {
341 let mut buf = [0u8; 4];
342 let mut pad = 0;
343
344 for j in 0..4 {
345 if i + j >= bytes.len() || bytes[i + j] == b'=' {
346 buf[j] = 0;
347 pad += 1;
348 } else if bytes[i + j] < 128 && DECODE_TABLE[bytes[i + j] as usize] >= 0 {
349 buf[j] = DECODE_TABLE[bytes[i + j] as usize] as u8;
350 } else {
351 return Err(std::io::Error::new(
352 std::io::ErrorKind::InvalidData,
353 "Invalid base64 character",
354 ));
355 }
356 }
357
358 output.push((buf[0] << 2) | (buf[1] >> 4));
359 if pad < 2 {
360 output.push((buf[1] << 4) | (buf[2] >> 2));
361 }
362 if pad < 1 {
363 output.push((buf[2] << 6) | buf[3]);
364 }
365
366 i += 4;
367 }
368
369 Ok(output)
370}
371
372pub struct ImageBuilder {
374 filename: String,
375 placement: ElementPlacement,
376 format: String,
377 source: Option<ImageSource>,
378 effects: Vec<ImageEffect>,
379 crop: Option<Crop>,
380}
381
382impl ImageBuilder {
383 pub fn new(filename: &str, width: u32, height: u32) -> Self {
385 let format = Path::new(filename)
386 .extension()
387 .and_then(|ext| ext.to_str())
388 .map(|s| s.to_uppercase())
389 .unwrap_or_else(|| "PNG".to_string());
390
391 ImageBuilder {
392 filename: filename.to_string(),
393 placement: ElementPlacement::new().with_size(width, height),
394 format,
395 source: Some(ImageSource::File(filename.to_string())),
396 effects: Vec::new(),
397 crop: None,
398 }
399 }
400
401 pub fn from_file(filename: &str) -> Self {
410 let defaults = ElementPlacement::image_defaults();
411 Self::new(filename, defaults.width, defaults.height)
412 }
413
414 pub fn from_base64(data: &str, width: u32, height: u32, format: &str) -> Self {
416 let (upper, ext) = format_and_ext(format);
417 ImageBuilder {
418 filename: format!("image.{ext}"),
419 placement: ElementPlacement::new().with_size(width, height),
420 format: upper,
421 source: Some(ImageSource::Base64(data.to_string())),
422 effects: Vec::new(),
423 crop: None,
424 }
425 }
426
427 pub fn base64(data: &str, format: &str) -> Self {
436 let defaults = ElementPlacement::image_defaults();
437 Self::from_base64(data, defaults.width, defaults.height, format)
438 }
439
440 pub fn from_bytes(data: Vec<u8>, width: u32, height: u32, format: &str) -> Self {
442 let (upper, ext) = format_and_ext(format);
443 ImageBuilder {
444 filename: format!("image.{ext}"),
445 placement: ElementPlacement::new().with_size(width, height),
446 format: upper,
447 source: Some(ImageSource::Bytes(data)),
448 effects: Vec::new(),
449 crop: None,
450 }
451 }
452
453 pub fn bytes(data: Vec<u8>, format: &str) -> Self {
463 let defaults = ElementPlacement::image_defaults();
464 Self::from_bytes(data, defaults.width, defaults.height, format)
465 }
466
467 pub fn auto(data: Vec<u8>) -> Self {
477 let defaults = ElementPlacement::image_defaults();
478
479 let format = if data.len() >= 4 {
481 if &data[0..4] == b"\x89PNG" {
482 "PNG"
483 } else if data.len() >= 2 && &data[0..2] == b"\xFF\xD8" {
484 "JPEG"
485 } else if data.len() >= 6 && &data[0..6] == b"GIF89a" || &data[0..6] == b"GIF87a" {
486 "GIF"
487 } else {
488 "PNG" }
490 } else {
491 "PNG"
492 };
493
494 Self::from_bytes(data, defaults.width, defaults.height, format)
495 }
496
497 pub fn position(mut self, x: u32, y: u32) -> Self {
499 self.placement.set_position(x, y);
500 self
501 }
502
503 pub fn at(self, x: u32, y: u32) -> Self {
505 self.position(x, y)
506 }
507
508 pub fn size(mut self, width: u32, height: u32) -> Self {
510 self.placement.set_size(width, height);
511 self
512 }
513
514 pub fn format(mut self, format: &str) -> Self {
516 self.format = format.to_uppercase();
517 self
518 }
519
520 pub fn scale_to_width(mut self, width: u32) -> Self {
522 let ratio = self.placement.width as f64 / self.placement.height as f64;
523 self.placement.width = width;
524 self.placement.height = (width as f64 / ratio) as u32;
525 self
526 }
527
528 pub fn scale_to_height(mut self, height: u32) -> Self {
530 let ratio = self.placement.width as f64 / self.placement.height as f64;
531 self.placement.height = height;
532 self.placement.width = (height as f64 * ratio) as u32;
533 self
534 }
535
536 pub fn shadow(mut self) -> Self {
538 self.effects.push(ImageEffect::Shadow);
539 self
540 }
541
542 pub fn reflection(mut self) -> Self {
544 self.effects.push(ImageEffect::Reflection);
545 self
546 }
547
548 pub fn glow(mut self) -> Self {
550 self.effects.push(ImageEffect::Glow);
551 self
552 }
553
554 pub fn soft_edges(mut self) -> Self {
556 self.effects.push(ImageEffect::SoftEdges);
557 self
558 }
559
560 pub fn inner_shadow(mut self) -> Self {
562 self.effects.push(ImageEffect::InnerShadow);
563 self
564 }
565
566 pub fn blur(mut self) -> Self {
568 self.effects.push(ImageEffect::Blur);
569 self
570 }
571
572 pub fn crop(mut self, left: f64, top: f64, right: f64, bottom: f64) -> Self {
574 self.crop = Some(Crop::new(left, top, right, bottom));
575 self
576 }
577
578 pub fn build(self) -> Image {
580 Image {
581 filename: self.filename,
582 width: self.placement.width,
583 height: self.placement.height,
584 x: self.placement.x,
585 y: self.placement.y,
586 format: self.format,
587 source: self.source,
588 crop: self.crop,
589 effects: self.effects,
590 alt_text: None,
591 }
592 }
593
594 pub fn build_with_crop(self, left: f64, top: f64, right: f64, bottom: f64) -> Image {
596 Image {
597 filename: self.filename,
598 width: self.placement.width,
599 height: self.placement.height,
600 x: self.placement.x,
601 y: self.placement.y,
602 format: self.format,
603 source: self.source,
604 crop: Some(Crop::new(left, top, right, bottom)),
605 effects: Vec::new(),
606 alt_text: None,
607 }
608 }
609
610 pub fn build_with_shadow(self) -> Image {
612 Image {
613 filename: self.filename,
614 width: self.placement.width,
615 height: self.placement.height,
616 x: self.placement.x,
617 y: self.placement.y,
618 format: self.format,
619 source: self.source,
620 crop: None,
621 effects: vec![ImageEffect::Shadow],
622 alt_text: None,
623 }
624 }
625
626 pub fn build_with_reflection(self) -> Image {
628 Image {
629 filename: self.filename,
630 width: self.placement.width,
631 height: self.placement.height,
632 x: self.placement.x,
633 y: self.placement.y,
634 format: self.format,
635 source: self.source,
636 crop: None,
637 effects: vec![ImageEffect::Reflection],
638 alt_text: None,
639 }
640 }
641
642 pub fn build_with_effects(self) -> Image {
644 Image {
645 filename: self.filename,
646 width: self.placement.width,
647 height: self.placement.height,
648 x: self.placement.x,
649 y: self.placement.y,
650 format: self.format,
651 source: self.source,
652 crop: None,
653 effects: vec![ImageEffect::Shadow, ImageEffect::Reflection],
654 alt_text: None,
655 }
656 }
657
658 pub fn build_with_glow(self) -> Image {
660 Image {
661 filename: self.filename,
662 width: self.placement.width,
663 height: self.placement.height,
664 x: self.placement.x,
665 y: self.placement.y,
666 format: self.format,
667 source: self.source,
668 crop: None,
669 effects: vec![ImageEffect::Glow],
670 alt_text: None,
671 }
672 }
673
674 pub fn build_with_soft_edges(self) -> Image {
676 Image {
677 filename: self.filename,
678 width: self.placement.width,
679 height: self.placement.height,
680 x: self.placement.x,
681 y: self.placement.y,
682 format: self.format,
683 source: self.source,
684 crop: None,
685 effects: vec![ImageEffect::SoftEdges],
686 alt_text: None,
687 }
688 }
689
690 pub fn build_with_inner_shadow(self) -> Image {
692 Image {
693 filename: self.filename,
694 width: self.placement.width,
695 height: self.placement.height,
696 x: self.placement.x,
697 y: self.placement.y,
698 format: self.format,
699 source: self.source,
700 crop: None,
701 effects: vec![ImageEffect::InnerShadow],
702 alt_text: None,
703 }
704 }
705
706 pub fn build_with_blur(self) -> Image {
708 Image {
709 filename: self.filename,
710 width: self.placement.width,
711 height: self.placement.height,
712 x: self.placement.x,
713 y: self.placement.y,
714 format: self.format,
715 source: self.source,
716 crop: None,
717 effects: vec![ImageEffect::Blur],
718 alt_text: None,
719 }
720 }
721}
722
723fn read_image_dimensions(data: &[u8]) -> Option<(u32, u32, String)> {
726 if data.len() < 10 {
727 return None;
728 }
729 if data.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) && data.len() >= 24 {
731 let w = u32::from_be_bytes([data[16], data[17], data[18], data[19]]);
732 let h = u32::from_be_bytes([data[20], data[21], data[22], data[23]]);
733 return Some((w, h, "PNG".into()));
734 }
735 if data.starts_with(&[0xFF, 0xD8]) {
737 return read_jpeg_dimensions(data);
738 }
739 if data.starts_with(b"GIF8") && data.len() >= 10 {
741 let w = u16::from_le_bytes([data[6], data[7]]) as u32;
742 let h = u16::from_le_bytes([data[8], data[9]]) as u32;
743 return Some((w, h, "GIF".into()));
744 }
745 if data.starts_with(b"BM") && data.len() >= 26 {
747 let w = u32::from_le_bytes([data[18], data[19], data[20], data[21]]);
748 let h = u32::from_le_bytes([data[22], data[23], data[24], data[25]]);
749 return Some((w, h, "BMP".into()));
750 }
751 if data.len() >= 30 && &data[0..4] == b"RIFF" && &data[8..12] == b"WEBP" {
753 if &data[12..16] == b"VP8 " && data.len() >= 30 {
755 let w = u16::from_le_bytes([data[26], data[27]]) as u32 & 0x3FFF;
756 let h = u16::from_le_bytes([data[28], data[29]]) as u32 & 0x3FFF;
757 return Some((w, h, "WEBP".into()));
758 }
759 if &data[12..16] == b"VP8L" && data.len() >= 25 {
761 let b0 = data[21] as u32;
762 let b1 = data[22] as u32;
763 let b2 = data[23] as u32;
764 let b3 = data[24] as u32;
765 let bits = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
766 let w = (bits & 0x3FFF) + 1;
767 let h = ((bits >> 14) & 0x3FFF) + 1;
768 return Some((w, h, "WEBP".into()));
769 }
770 }
771 None
772}
773
774fn read_jpeg_dimensions(data: &[u8]) -> Option<(u32, u32, String)> {
776 let mut i = 2;
777 while i + 1 < data.len() {
778 if data[i] != 0xFF {
779 i += 1;
780 continue;
781 }
782 let marker = data[i + 1];
783 i += 2;
784 if (marker == 0xC0 || marker == 0xC2) && i + 7 < data.len() {
786 let h = u16::from_be_bytes([data[i + 3], data[i + 4]]) as u32;
787 let w = u16::from_be_bytes([data[i + 5], data[i + 6]]) as u32;
788 return Some((w, h, "JPEG".into()));
789 }
790 if marker >= 0xC0 && marker != 0xD9 && marker != 0xDA && i + 1 < data.len() {
792 let len = u16::from_be_bytes([data[i], data[i + 1]]) as usize;
793 i += len;
794 }
795 }
796 None
797}
798
799#[cfg(test)]
800mod tests {
801 use super::*;
802
803 #[test]
804 fn test_image_creation() {
805 let img = Image::new("test.png", 1920, 1080, "PNG");
806 assert_eq!(img.filename, "test.png");
807 assert_eq!(img.width, 1920);
808 assert_eq!(img.height, 1080);
809 }
810
811 #[test]
812 fn test_image_position() {
813 let img = Image::new("test.png", 1920, 1080, "PNG")
814 .position(500000, 1000000);
815 assert_eq!(img.x, 500000);
816 assert_eq!(img.y, 1000000);
817 }
818
819 #[test]
820 fn test_image_aspect_ratio() {
821 let img = Image::new("test.png", 1920, 1080, "PNG");
822 let ratio = img.aspect_ratio();
823 assert!((ratio - 1.777).abs() < 0.01);
824 }
825
826 #[test]
827 fn test_image_scale_to_width() {
828 let img = Image::new("test.png", 1920, 1080, "PNG")
829 .scale_to_width(960);
830 assert_eq!(img.width, 960);
831 assert_eq!(img.height, 540);
832 }
833
834 #[test]
835 fn test_image_scale_to_height() {
836 let img = Image::new("test.png", 1920, 1080, "PNG")
837 .scale_to_height(540);
838 assert_eq!(img.width, 960);
839 assert_eq!(img.height, 540);
840 }
841
842 #[test]
843 fn test_image_extension() {
844 let img = Image::new("photo.jpg", 1920, 1080, "JPEG");
845 assert_eq!(img.extension(), "jpg");
846 }
847
848 #[test]
849 fn test_image_mime_types() {
850 assert_eq!(
851 Image::new("test.png", 100, 100, "PNG").mime_type(),
852 "image/png"
853 );
854 assert_eq!(
855 Image::new("test.jpg", 100, 100, "JPG").mime_type(),
856 "image/jpeg"
857 );
858 assert_eq!(
859 Image::new("test.gif", 100, 100, "GIF").mime_type(),
860 "image/gif"
861 );
862 }
863
864 #[test]
865 fn test_image_builder() {
866 let img = ImageBuilder::new("photo.png", 1920, 1080)
867 .position(500000, 1000000)
868 .scale_to_width(960)
869 .build();
870
871 assert_eq!(img.filename, "photo.png");
872 assert_eq!(img.width, 960);
873 assert_eq!(img.height, 540);
874 assert_eq!(img.x, 500000);
875 assert_eq!(img.y, 1000000);
876 }
877
878 #[test]
879 fn test_image_builder_auto_format() {
880 let img = ImageBuilder::new("photo.jpg", 1920, 1080).build();
881 assert_eq!(img.format, "JPG");
882 }
883
884 #[test]
885 fn test_image_from_base64() {
886 let base64_png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
888 let img = Image::from_base64(base64_png, 100, 100, "PNG");
889
890 assert!(img.filename.ends_with(".png"));
891 assert_eq!(img.format, "PNG");
892 assert!(matches!(img.source, Some(ImageSource::Base64(_))));
893 }
894
895 #[test]
896 fn test_image_from_bytes() {
897 let data = vec![0x89, 0x50, 0x4E, 0x47]; let img = Image::from_bytes(data.clone(), 100, 100, "PNG");
899
900 assert_eq!(img.format, "PNG");
901 assert!(matches!(img.source, Some(ImageSource::Bytes(_))));
902 }
903
904 #[test]
905 fn test_base64_decode() {
906 let result = base64_decode("SGVsbG8=").unwrap();
908 assert_eq!(result, b"Hello");
909
910 let result = base64_decode("SGVsbG8gV29ybGQ=").unwrap();
912 assert_eq!(result, b"Hello World");
913 }
914
915 #[test]
916 fn test_image_get_bytes_base64() {
917 let base64_png = "SGVsbG8="; let img = Image::from_base64(base64_png, 100, 100, "PNG");
919
920 let bytes = img.get_bytes().unwrap();
921 assert_eq!(bytes, b"Hello");
922 }
923
924 #[test]
925 fn test_image_builder_from_base64() {
926 let base64_data = "SGVsbG8=";
927 let img = ImageBuilder::from_base64(base64_data, 200, 150, "JPEG")
928 .position(1000, 2000)
929 .build();
930
931 assert_eq!(img.width, 200);
932 assert_eq!(img.height, 150);
933 assert_eq!(img.x, 1000);
934 assert_eq!(img.y, 2000);
935 assert_eq!(img.format, "JPEG");
936 }
937
938 #[test]
939 fn test_read_png_dimensions() {
940 let png: Vec<u8> = vec![
942 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, ];
949 let (w, h, fmt) = read_image_dimensions(&png).unwrap();
950 assert_eq!((w, h), (1, 1));
951 assert_eq!(fmt, "PNG");
952 }
953
954 #[test]
955 fn test_read_gif_dimensions() {
956 let gif: Vec<u8> = vec![
957 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x0A, 0x00, 0x14, 0x00, ];
961 let (w, h, fmt) = read_image_dimensions(&gif).unwrap();
962 assert_eq!((w, h), (10, 20));
963 assert_eq!(fmt, "GIF");
964 }
965
966 #[test]
967 fn test_read_bmp_dimensions() {
968 let mut bmp = vec![0u8; 26];
969 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();
973 assert_eq!((w, h), (100, 200));
974 assert_eq!(fmt, "BMP");
975 }
976}