1use std::collections::HashMap;
23
24use hwpforge_foundation::HwpUnit;
25use schemars::JsonSchema;
26use serde::{Deserialize, Serialize};
27
28use crate::caption::Caption;
29use crate::object_id::ObjectId;
30use crate::placement::ObjectPlacement;
31
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
57#[non_exhaustive]
58pub struct Image {
59 pub path: String,
61 pub width: HwpUnit,
63 pub height: HwpUnit,
65 pub format: ImageFormat,
67 pub caption: Option<Caption>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub placement: Option<ObjectPlacement>,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub inst_id: Option<ObjectId>,
78}
79
80impl Image {
81 #[must_use]
98 pub fn new(
99 path: impl Into<String>,
100 width: HwpUnit,
101 height: HwpUnit,
102 format: ImageFormat,
103 ) -> Self {
104 Self {
105 path: path.into(),
106 width,
107 height,
108 format,
109 caption: None,
110 placement: None,
111 inst_id: None,
112 }
113 }
114
115 #[must_use]
139 pub fn from_path(path: impl Into<String>, width: HwpUnit, height: HwpUnit) -> Self {
140 let path: String = path.into();
141 let format = ImageFormat::from_extension(&path);
142 Self { path, width, height, format, caption: None, placement: None, inst_id: None }
143 }
144
145 #[must_use]
147 pub fn with_caption(mut self, caption: Caption) -> Self {
148 self.caption = Some(caption);
149 self
150 }
151
152 #[must_use]
154 pub fn with_placement(mut self, placement: ObjectPlacement) -> Self {
155 self.placement = Some(placement);
156 self
157 }
158}
159
160impl std::fmt::Display for Image {
161 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 write!(
163 f,
164 "Image({}, {:.1}mm x {:.1}mm)",
165 self.format,
166 self.width.to_mm(),
167 self.height.to_mm()
168 )
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
189#[non_exhaustive]
190pub enum ImageFormat {
191 Png,
193 Jpeg,
195 Gif,
197 Bmp,
199 Wmf,
201 Emf,
203 Unknown(String),
205}
206
207impl ImageFormat {
208 pub fn from_extension(path: &str) -> Self {
238 let ext_lower = path.rfind('.').map(|i| path[i + 1..].to_ascii_lowercase());
240 match ext_lower.as_deref() {
241 Some("png") => Self::Png,
242 Some("jpg" | "jpeg") => Self::Jpeg,
243 Some("gif") => Self::Gif,
244 Some("bmp") => Self::Bmp,
245 Some("wmf") => Self::Wmf,
246 Some("emf") => Self::Emf,
247 Some(ext) => Self::Unknown(ext.to_string()),
248 None => Self::Unknown(String::new()),
249 }
250 }
251
252 #[must_use]
291 pub fn sniff(bytes: &[u8]) -> Option<Self> {
292 if bytes.len() >= 8 && bytes[..8] == [0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A] {
293 return Some(Self::Png);
294 }
295 if bytes.len() >= 3 && bytes[..3] == [0xFF, 0xD8, 0xFF] {
296 return Some(Self::Jpeg);
297 }
298 if bytes.len() >= 6 && (&bytes[..6] == b"GIF87a" || &bytes[..6] == b"GIF89a") {
299 return Some(Self::Gif);
300 }
301 if bytes.len() >= 18 && &bytes[..2] == b"BM" {
302 let off_bits = u32::from_le_bytes([bytes[10], bytes[11], bytes[12], bytes[13]]);
303 let dib_size = u32::from_le_bytes([bytes[14], bytes[15], bytes[16], bytes[17]]);
304 let off_ok = off_bits >= 14 && (off_bits as usize) <= bytes.len();
305 let dib_ok = matches!(dib_size, 12 | 40 | 52 | 56 | 64 | 108 | 124);
306 if off_ok && dib_ok {
307 return Some(Self::Bmp);
308 }
309 return None;
310 }
311 if bytes.len() >= 44 && bytes[..4] == [0x01, 0, 0, 0] && &bytes[40..44] == b" EMF" {
312 return Some(Self::Emf);
313 }
314 if bytes.len() >= 4 && bytes[..4] == [0xD7, 0xCD, 0xC6, 0x9A] {
315 return Some(Self::Wmf);
316 }
317 None
318 }
319
320 #[must_use]
335 pub fn canonical_extension(&self) -> Option<&'static str> {
336 match self {
337 Self::Png => Some("png"),
338 Self::Jpeg => Some("jpg"),
339 Self::Gif => Some("gif"),
340 Self::Bmp => Some("bmp"),
341 Self::Wmf => Some("wmf"),
342 Self::Emf => Some("emf"),
343 Self::Unknown(_) => None,
344 }
345 }
346}
347
348impl std::fmt::Display for ImageFormat {
349 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
350 match self {
351 Self::Png => write!(f, "PNG"),
352 Self::Jpeg => write!(f, "JPEG"),
353 Self::Gif => write!(f, "GIF"),
354 Self::Bmp => write!(f, "BMP"),
355 Self::Wmf => write!(f, "WMF"),
356 Self::Emf => write!(f, "EMF"),
357 Self::Unknown(s) => {
358 let lower = s.to_ascii_lowercase();
359 write!(f, "{lower}")
360 }
361 }
362 }
363}
364
365#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
386pub struct ImageStore {
387 images: HashMap<String, Vec<u8>>,
388}
389
390impl ImageStore {
391 pub fn new() -> Self {
393 Self { images: HashMap::new() }
394 }
395
396 pub fn insert(&mut self, key: impl Into<String>, data: Vec<u8>) {
400 self.images.insert(key.into(), data);
401 }
402
403 pub fn get(&self, key: &str) -> Option<&[u8]> {
405 self.images.get(key).map(|v| v.as_slice())
406 }
407
408 pub fn len(&self) -> usize {
410 self.images.len()
411 }
412
413 pub fn is_empty(&self) -> bool {
415 self.images.is_empty()
416 }
417
418 pub fn iter(&self) -> impl Iterator<Item = (&str, &[u8])> {
420 self.images.iter().map(|(k, v)| (k.as_str(), v.as_slice()))
421 }
422}
423
424impl FromIterator<(String, Vec<u8>)> for ImageStore {
425 fn from_iter<I: IntoIterator<Item = (String, Vec<u8>)>>(iter: I) -> Self {
426 Self { images: iter.into_iter().collect() }
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 fn sample_image() -> Image {
435 Image::new(
436 "BinData/image1.png",
437 HwpUnit::from_mm(50.0).unwrap(),
438 HwpUnit::from_mm(30.0).unwrap(),
439 ImageFormat::Png,
440 )
441 }
442
443 #[test]
444 fn new_constructor() {
445 let img = sample_image();
446 assert_eq!(img.path, "BinData/image1.png");
447 assert_eq!(img.format, ImageFormat::Png);
448 }
449
450 #[test]
451 fn from_path_constructor() {
452 let img = Image::from_path(
453 "test.jpeg",
454 HwpUnit::from_mm(10.0).unwrap(),
455 HwpUnit::from_mm(10.0).unwrap(),
456 );
457 assert_eq!(img.format, ImageFormat::Jpeg);
458 }
459
460 #[test]
461 fn builder_attaches_caption() {
462 let img = sample_image().with_caption(Caption::default());
463 assert!(img.caption.is_some());
464 }
465
466 #[test]
467 fn display_format() {
468 let img = sample_image();
469 let s = img.to_string();
470 assert!(s.contains("PNG"), "display: {s}");
471 assert!(s.contains("50.0"), "display: {s}");
472 assert!(s.contains("30.0"), "display: {s}");
473 }
474
475 #[test]
476 fn image_format_display() {
477 assert_eq!(ImageFormat::Png.to_string(), "PNG");
478 assert_eq!(ImageFormat::Jpeg.to_string(), "JPEG");
479 assert_eq!(ImageFormat::Gif.to_string(), "GIF");
480 assert_eq!(ImageFormat::Bmp.to_string(), "BMP");
481 assert_eq!(ImageFormat::Wmf.to_string(), "WMF");
482 assert_eq!(ImageFormat::Emf.to_string(), "EMF");
483 assert_eq!(ImageFormat::Unknown("TIFF".to_string()).to_string(), "tiff");
484 }
485
486 #[test]
487 fn equality() {
488 let a = sample_image();
489 let b = sample_image();
490 assert_eq!(a, b);
491 }
492
493 #[test]
494 fn inequality_on_different_paths() {
495 let a = sample_image();
496 let mut b = sample_image();
497 b.path = "other.png".to_string();
498 assert_ne!(a, b);
499 }
500
501 #[test]
502 fn clone_independence() {
503 let img = sample_image();
504 let mut cloned = img.clone();
505 cloned.path = "modified.png".to_string();
506 assert_eq!(img.path, "BinData/image1.png");
507 }
508
509 #[test]
510 fn serde_roundtrip() {
511 let img = sample_image();
512 let json = serde_json::to_string(&img).unwrap();
513 let back: Image = serde_json::from_str(&json).unwrap();
514 assert_eq!(img, back);
515 }
516
517 #[test]
518 fn placement_roundtrip() {
519 use crate::placement::{ObjectPlacement, ObjectRelativeTo, ObjectTextFlow, ObjectTextWrap};
520 let img = sample_image().with_placement(ObjectPlacement {
521 text_wrap: ObjectTextWrap::Square,
522 text_flow: ObjectTextFlow::RightOnly,
523 treat_as_char: false,
524 flow_with_text: true,
525 allow_overlap: true,
526 vert_rel_to: ObjectRelativeTo::Paper,
527 horz_rel_to: ObjectRelativeTo::Page,
528 vert_offset: HwpUnit::new(1200).unwrap(),
529 horz_offset: HwpUnit::new(3400).unwrap(),
530 });
531 let json = serde_json::to_string(&img).unwrap();
532 let back: Image = serde_json::from_str(&json).unwrap();
533 assert_eq!(img, back);
534 }
535
536 #[test]
537 fn serde_unknown_format_roundtrip() {
538 let img = Image::new(
539 "test.svg",
540 HwpUnit::from_mm(10.0).unwrap(),
541 HwpUnit::from_mm(10.0).unwrap(),
542 ImageFormat::Unknown("SVG".to_string()),
543 );
544 let json = serde_json::to_string(&img).unwrap();
545 let back: Image = serde_json::from_str(&json).unwrap();
546 assert_eq!(img, back);
547 }
548
549 #[test]
550 fn image_format_hash() {
551 use std::collections::HashSet;
552 let mut set = HashSet::new();
553 set.insert(ImageFormat::Png);
554 set.insert(ImageFormat::Jpeg);
555 set.insert(ImageFormat::Png);
556 assert_eq!(set.len(), 2);
557 }
558
559 #[test]
560 fn from_string_path() {
561 let path = String::from("dynamic/path.bmp");
562 let img = Image::new(path, HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Bmp);
563 assert_eq!(img.path, "dynamic/path.bmp");
564 }
565
566 #[test]
571 fn image_store_new_is_empty() {
572 let store = ImageStore::new();
573 assert!(store.is_empty());
574 assert_eq!(store.len(), 0);
575 }
576
577 #[test]
578 fn image_store_insert_and_get() {
579 let mut store = ImageStore::new();
580 store.insert("logo.png", vec![0x89, 0x50, 0x4E, 0x47]);
581 assert_eq!(store.len(), 1);
582 assert!(!store.is_empty());
583 assert_eq!(store.get("logo.png"), Some(&[0x89, 0x50, 0x4E, 0x47][..]));
584 }
585
586 #[test]
587 fn image_store_get_missing() {
588 let store = ImageStore::new();
589 assert!(store.get("nonexistent.png").is_none());
590 }
591
592 #[test]
593 fn image_store_insert_replaces() {
594 let mut store = ImageStore::new();
595 store.insert("img.png", vec![1, 2, 3]);
596 store.insert("img.png", vec![4, 5, 6]);
597 assert_eq!(store.len(), 1);
598 assert_eq!(store.get("img.png"), Some(&[4, 5, 6][..]));
599 }
600
601 #[test]
602 fn image_store_multiple_images() {
603 let mut store = ImageStore::new();
604 store.insert("a.png", vec![1]);
605 store.insert("b.jpg", vec![2]);
606 store.insert("c.gif", vec![3]);
607 assert_eq!(store.len(), 3);
608 }
609
610 #[test]
611 fn image_store_iter() {
612 let mut store = ImageStore::new();
613 store.insert("a.png", vec![1]);
614 store.insert("b.jpg", vec![2]);
615 let pairs: Vec<_> = store.iter().collect();
616 assert_eq!(pairs.len(), 2);
617 }
618
619 #[test]
620 fn image_store_from_iterator() {
621 let items = vec![("a.png".to_string(), vec![1, 2]), ("b.jpg".to_string(), vec![3, 4])];
622 let store: ImageStore = items.into_iter().collect();
623 assert_eq!(store.len(), 2);
624 assert_eq!(store.get("a.png"), Some(&[1, 2][..]));
625 }
626
627 #[test]
628 fn image_store_default() {
629 let store = ImageStore::default();
630 assert!(store.is_empty());
631 }
632
633 #[test]
634 fn image_store_clone_independence() {
635 let mut store = ImageStore::new();
636 store.insert("img.png", vec![1, 2, 3]);
637 let mut cloned = store.clone();
638 cloned.insert("other.png", vec![4, 5]);
639 assert_eq!(store.len(), 1);
640 assert_eq!(cloned.len(), 2);
641 }
642
643 #[test]
644 fn image_store_equality() {
645 let mut a = ImageStore::new();
646 a.insert("img.png", vec![1, 2, 3]);
647 let mut b = ImageStore::new();
648 b.insert("img.png", vec![1, 2, 3]);
649 assert_eq!(a, b);
650 }
651
652 #[test]
653 fn image_store_serde_roundtrip() {
654 let mut store = ImageStore::new();
655 store.insert("logo.png", vec![0x89, 0x50]);
656 let json = serde_json::to_string(&store).unwrap();
657 let back: ImageStore = serde_json::from_str(&json).unwrap();
658 assert_eq!(store, back);
659 }
660
661 #[test]
662 fn image_store_string_key() {
663 let mut store = ImageStore::new();
664 let key = String::from("dynamic/path.png");
665 store.insert(key, vec![42]);
666 assert!(store.get("dynamic/path.png").is_some());
667 }
668
669 #[test]
674 fn from_extension_png() {
675 assert_eq!(ImageFormat::from_extension("photo.png"), ImageFormat::Png);
676 }
677
678 #[test]
679 fn from_extension_jpg_uppercase() {
680 assert_eq!(ImageFormat::from_extension("image.JPG"), ImageFormat::Jpeg);
681 }
682
683 #[test]
684 fn from_extension_jpeg() {
685 assert_eq!(ImageFormat::from_extension("file.jpeg"), ImageFormat::Jpeg);
686 }
687
688 #[test]
689 fn from_extension_gif() {
690 assert_eq!(ImageFormat::from_extension("doc.gif"), ImageFormat::Gif);
691 }
692
693 #[test]
694 fn from_extension_bmp() {
695 assert_eq!(ImageFormat::from_extension("img.bmp"), ImageFormat::Bmp);
696 }
697
698 #[test]
699 fn from_extension_wmf() {
700 assert_eq!(ImageFormat::from_extension("chart.wmf"), ImageFormat::Wmf);
701 }
702
703 #[test]
704 fn from_extension_emf() {
705 assert_eq!(ImageFormat::from_extension("dia.emf"), ImageFormat::Emf);
706 }
707
708 #[test]
709 fn from_extension_unknown() {
710 assert_eq!(
711 ImageFormat::from_extension("file.xyz"),
712 ImageFormat::Unknown("xyz".to_string()),
713 );
714 }
715
716 #[test]
717 fn from_extension_no_extension() {
718 assert_eq!(ImageFormat::from_extension("noext"), ImageFormat::Unknown(String::new()));
719 }
720
721 #[test]
722 fn from_extension_multi_dot() {
723 assert_eq!(ImageFormat::from_extension("multi.dot.png"), ImageFormat::Png);
724 }
725
726 #[test]
731 fn sniff_known_magics() {
732 let png = [0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A, 0, 0];
733 assert_eq!(ImageFormat::sniff(&png), Some(ImageFormat::Png));
734 assert_eq!(ImageFormat::sniff(&[0xFF, 0xD8, 0xFF, 0xE0]), Some(ImageFormat::Jpeg));
735 assert_eq!(ImageFormat::sniff(b"GIF87a\x00"), Some(ImageFormat::Gif));
736 assert_eq!(ImageFormat::sniff(b"GIF89a\x00"), Some(ImageFormat::Gif));
737 let mut bmp = Vec::from(&b"BM"[..]);
739 bmp.extend_from_slice(&[0u8; 8]); bmp.extend_from_slice(&26u32.to_le_bytes()); bmp.extend_from_slice(&12u32.to_le_bytes()); bmp.extend_from_slice(&[0u8; 8]); assert_eq!(ImageFormat::sniff(&bmp), Some(ImageFormat::Bmp));
744 let mut emf = vec![0x01, 0, 0, 0];
745 emf.extend_from_slice(&[0u8; 36]);
746 emf.extend_from_slice(b" EMF");
747 assert_eq!(ImageFormat::sniff(&emf), Some(ImageFormat::Emf));
748 assert_eq!(ImageFormat::sniff(&[0xD7, 0xCD, 0xC6, 0x9A, 0, 0]), Some(ImageFormat::Wmf));
749 }
750
751 #[test]
752 fn sniff_truncated_and_weak_magics_are_none() {
753 assert_eq!(ImageFormat::sniff(&[]), None);
754 assert_eq!(ImageFormat::sniff(b"BM"), None);
756 assert_eq!(ImageFormat::sniff(&[0x89, b'P']), None);
758 assert_eq!(ImageFormat::sniff(&[0xFF, 0xD8]), None);
759 assert_eq!(ImageFormat::sniff(b"GIF8"), None);
760 assert_eq!(ImageFormat::sniff(&[0x01, 0, 0, 0, 0, 0, 0, 0]), None);
762 }
763
764 #[test]
765 fn sniff_bmp_requires_structural_header() {
766 assert_eq!(ImageFormat::sniff(b"BMsecret-credential-material-here-0123456789"), None);
769 let mut zeros = Vec::from(&b"BM"[..]);
771 zeros.extend_from_slice(&[0u8; 20]);
772 assert_eq!(ImageFormat::sniff(&zeros), None);
773 let mut oob = Vec::from(&b"BM"[..]);
775 oob.extend_from_slice(&[0u8; 8]);
776 oob.extend_from_slice(&999u32.to_le_bytes());
777 oob.extend_from_slice(&40u32.to_le_bytes());
778 assert_eq!(ImageFormat::sniff(&oob), None);
779 }
780
781 #[test]
782 fn sniff_never_guesses_from_content() {
783 assert_eq!(ImageFormat::sniff(b"<svg xmlns=\"http\""), None);
784 assert_eq!(ImageFormat::sniff(&[0u8; 64]), None);
785 let mut wav = Vec::from(&b"RIFF"[..]);
788 wav.extend_from_slice(&[0x10, 0, 0, 0]);
789 wav.extend_from_slice(b"WAVEfmt ");
790 assert_eq!(ImageFormat::sniff(&wav), None);
791 }
792
793 #[test]
794 fn canonical_extension_covers_all_known() {
795 assert_eq!(ImageFormat::Png.canonical_extension(), Some("png"));
796 assert_eq!(ImageFormat::Jpeg.canonical_extension(), Some("jpg"));
797 assert_eq!(ImageFormat::Gif.canonical_extension(), Some("gif"));
798 assert_eq!(ImageFormat::Bmp.canonical_extension(), Some("bmp"));
799 assert_eq!(ImageFormat::Wmf.canonical_extension(), Some("wmf"));
800 assert_eq!(ImageFormat::Emf.canonical_extension(), Some("emf"));
801 assert_eq!(ImageFormat::Unknown("svg".into()).canonical_extension(), None);
802 }
803
804 #[test]
809 fn from_path_infers_format() {
810 let w = HwpUnit::from_mm(100.0).unwrap();
811 let h = HwpUnit::from_mm(75.0).unwrap();
812
813 let img = Image::from_path("photos/hero.png", w, h);
814 assert_eq!(img.format, ImageFormat::Png);
815 assert_eq!(img.path, "photos/hero.png");
816 assert_eq!(img.width, w);
817 assert_eq!(img.height, h);
818 assert!(img.caption.is_none());
819 }
820
821 #[test]
822 fn from_path_jpeg_uppercase() {
823 let w = HwpUnit::ZERO;
824 let h = HwpUnit::ZERO;
825 let img = Image::from_path("scan.JPG", w, h);
826 assert_eq!(img.format, ImageFormat::Jpeg);
827 }
828
829 #[test]
830 fn from_path_unknown_extension() {
831 let w = HwpUnit::ZERO;
832 let h = HwpUnit::ZERO;
833 let img = Image::from_path("diagram.svg", w, h);
834 assert_eq!(img.format, ImageFormat::Unknown("svg".to_string()));
835 }
836
837 #[test]
838 fn from_path_string_owned() {
839 let w = HwpUnit::ZERO;
840 let h = HwpUnit::ZERO;
841 let path = String::from("owned/path.bmp");
842 let img = Image::from_path(path, w, h);
843 assert_eq!(img.format, ImageFormat::Bmp);
844 assert_eq!(img.path, "owned/path.bmp");
845 }
846
847 #[test]
848 fn unknown_format_display_normalizes_to_lowercase() {
849 assert_eq!(ImageFormat::Unknown("SVG".to_string()).to_string(), "svg");
850 assert_eq!(ImageFormat::Unknown("Tiff".to_string()).to_string(), "tiff");
851 assert_eq!(ImageFormat::Unknown("webp".to_string()).to_string(), "webp");
852 }
853
854 #[test]
855 fn unknown_format_casing_inequality() {
856 let upper = ImageFormat::Unknown("SVG".to_string());
858 let lower = ImageFormat::Unknown("svg".to_string());
859 assert_ne!(upper, lower, "Different casing in Unknown produces inequality");
860 assert_eq!(upper.to_string(), lower.to_string());
862 }
863}