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