1use std::borrow::Cow;
23use std::collections::HashMap;
24
25use hwpforge_foundation::HwpUnit;
26use schemars::JsonSchema;
27use serde::{Deserialize, Serialize};
28
29use crate::caption::Caption;
30use crate::object_id::ObjectId;
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<ImagePlacement>,
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: ImagePlacement) -> 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, Serialize, Deserialize, JsonSchema)]
174pub struct ImagePlacement {
175 pub text_wrap: ImageTextWrap,
177 pub text_flow: ImageTextFlow,
179 pub treat_as_char: bool,
181 pub flow_with_text: bool,
183 pub allow_overlap: bool,
185 pub vert_rel_to: ImageRelativeTo,
187 pub horz_rel_to: ImageRelativeTo,
189 pub vert_offset: HwpUnit,
191 pub horz_offset: HwpUnit,
193}
194
195impl ImagePlacement {
196 pub fn legacy_inline_defaults() -> Self {
198 Self {
199 text_wrap: ImageTextWrap::TopAndBottom,
200 text_flow: ImageTextFlow::BothSides,
201 treat_as_char: true,
202 flow_with_text: false,
203 allow_overlap: false,
204 vert_rel_to: ImageRelativeTo::Para,
205 horz_rel_to: ImageRelativeTo::Para,
206 vert_offset: HwpUnit::ZERO,
207 horz_offset: HwpUnit::ZERO,
208 }
209 }
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
214#[non_exhaustive]
215pub enum ImageTextWrap {
216 TopAndBottom,
218 Square,
220 BehindText,
222 InFrontOfText,
224 Tight,
226 Through,
228 Other(String),
230}
231
232impl ImageTextWrap {
233 pub fn from_hwpx(value: &str) -> Self {
235 match value {
236 "TOP_AND_BOTTOM" => Self::TopAndBottom,
237 "SQUARE" => Self::Square,
238 "BEHIND_TEXT" => Self::BehindText,
239 "IN_FRONT_OF_TEXT" => Self::InFrontOfText,
240 "TIGHT" => Self::Tight,
241 "THROUGH" => Self::Through,
242 other => Self::Other(other.to_string()),
243 }
244 }
245
246 pub fn as_hwpx_str(&self) -> Cow<'_, str> {
248 match self {
249 Self::TopAndBottom => Cow::Borrowed("TOP_AND_BOTTOM"),
250 Self::Square => Cow::Borrowed("SQUARE"),
251 Self::BehindText => Cow::Borrowed("BEHIND_TEXT"),
252 Self::InFrontOfText => Cow::Borrowed("IN_FRONT_OF_TEXT"),
253 Self::Tight => Cow::Borrowed("TIGHT"),
254 Self::Through => Cow::Borrowed("THROUGH"),
255 Self::Other(value) => Cow::Borrowed(value.as_str()),
256 }
257 }
258}
259
260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
262#[non_exhaustive]
263pub enum ImageTextFlow {
264 BothSides,
266 LeftOnly,
268 RightOnly,
270 LargestOnly,
272 Other(String),
274}
275
276impl ImageTextFlow {
277 pub fn from_hwpx(value: &str) -> Self {
279 match value {
280 "BOTH_SIDES" => Self::BothSides,
281 "LEFT_ONLY" => Self::LeftOnly,
282 "RIGHT_ONLY" => Self::RightOnly,
283 "LARGEST_ONLY" => Self::LargestOnly,
284 other => Self::Other(other.to_string()),
285 }
286 }
287
288 pub fn as_hwpx_str(&self) -> Cow<'_, str> {
290 match self {
291 Self::BothSides => Cow::Borrowed("BOTH_SIDES"),
292 Self::LeftOnly => Cow::Borrowed("LEFT_ONLY"),
293 Self::RightOnly => Cow::Borrowed("RIGHT_ONLY"),
294 Self::LargestOnly => Cow::Borrowed("LARGEST_ONLY"),
295 Self::Other(value) => Cow::Borrowed(value.as_str()),
296 }
297 }
298}
299
300#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
302#[non_exhaustive]
303pub enum ImageRelativeTo {
304 Paper,
306 Page,
308 Para,
310 Column,
312 Character,
314 Line,
316 Other(String),
318}
319
320impl ImageRelativeTo {
321 pub fn from_hwpx(value: &str) -> Self {
323 match value {
324 "PAPER" => Self::Paper,
325 "PAGE" => Self::Page,
326 "PARA" => Self::Para,
327 "COLUMN" => Self::Column,
328 "CHAR" => Self::Character,
329 "LINE" => Self::Line,
330 other => Self::Other(other.to_string()),
331 }
332 }
333
334 pub fn as_hwpx_str(&self) -> Cow<'_, str> {
336 match self {
337 Self::Paper => Cow::Borrowed("PAPER"),
338 Self::Page => Cow::Borrowed("PAGE"),
339 Self::Para => Cow::Borrowed("PARA"),
340 Self::Column => Cow::Borrowed("COLUMN"),
341 Self::Character => Cow::Borrowed("CHAR"),
342 Self::Line => Cow::Borrowed("LINE"),
343 Self::Other(value) => Cow::Borrowed(value.as_str()),
344 }
345 }
346}
347
348#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
365#[non_exhaustive]
366pub enum ImageFormat {
367 Png,
369 Jpeg,
371 Gif,
373 Bmp,
375 Wmf,
377 Emf,
379 Unknown(String),
381}
382
383impl ImageFormat {
384 pub fn from_extension(path: &str) -> Self {
414 let ext_lower = path.rfind('.').map(|i| path[i + 1..].to_ascii_lowercase());
416 match ext_lower.as_deref() {
417 Some("png") => Self::Png,
418 Some("jpg" | "jpeg") => Self::Jpeg,
419 Some("gif") => Self::Gif,
420 Some("bmp") => Self::Bmp,
421 Some("wmf") => Self::Wmf,
422 Some("emf") => Self::Emf,
423 Some(ext) => Self::Unknown(ext.to_string()),
424 None => Self::Unknown(String::new()),
425 }
426 }
427}
428
429impl std::fmt::Display for ImageFormat {
430 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431 match self {
432 Self::Png => write!(f, "PNG"),
433 Self::Jpeg => write!(f, "JPEG"),
434 Self::Gif => write!(f, "GIF"),
435 Self::Bmp => write!(f, "BMP"),
436 Self::Wmf => write!(f, "WMF"),
437 Self::Emf => write!(f, "EMF"),
438 Self::Unknown(s) => {
439 let lower = s.to_ascii_lowercase();
440 write!(f, "{lower}")
441 }
442 }
443 }
444}
445
446#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
467pub struct ImageStore {
468 images: HashMap<String, Vec<u8>>,
469}
470
471impl ImageStore {
472 pub fn new() -> Self {
474 Self { images: HashMap::new() }
475 }
476
477 pub fn insert(&mut self, key: impl Into<String>, data: Vec<u8>) {
481 self.images.insert(key.into(), data);
482 }
483
484 pub fn get(&self, key: &str) -> Option<&[u8]> {
486 self.images.get(key).map(|v| v.as_slice())
487 }
488
489 pub fn len(&self) -> usize {
491 self.images.len()
492 }
493
494 pub fn is_empty(&self) -> bool {
496 self.images.is_empty()
497 }
498
499 pub fn iter(&self) -> impl Iterator<Item = (&str, &[u8])> {
501 self.images.iter().map(|(k, v)| (k.as_str(), v.as_slice()))
502 }
503}
504
505impl FromIterator<(String, Vec<u8>)> for ImageStore {
506 fn from_iter<I: IntoIterator<Item = (String, Vec<u8>)>>(iter: I) -> Self {
507 Self { images: iter.into_iter().collect() }
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514
515 fn sample_image() -> Image {
516 Image::new(
517 "BinData/image1.png",
518 HwpUnit::from_mm(50.0).unwrap(),
519 HwpUnit::from_mm(30.0).unwrap(),
520 ImageFormat::Png,
521 )
522 }
523
524 #[test]
525 fn new_constructor() {
526 let img = sample_image();
527 assert_eq!(img.path, "BinData/image1.png");
528 assert_eq!(img.format, ImageFormat::Png);
529 }
530
531 #[test]
532 fn from_path_constructor() {
533 let img = Image::from_path(
534 "test.jpeg",
535 HwpUnit::from_mm(10.0).unwrap(),
536 HwpUnit::from_mm(10.0).unwrap(),
537 );
538 assert_eq!(img.format, ImageFormat::Jpeg);
539 }
540
541 #[test]
542 fn builder_attaches_caption() {
543 let img = sample_image().with_caption(Caption::default());
544 assert!(img.caption.is_some());
545 }
546
547 #[test]
548 fn display_format() {
549 let img = sample_image();
550 let s = img.to_string();
551 assert!(s.contains("PNG"), "display: {s}");
552 assert!(s.contains("50.0"), "display: {s}");
553 assert!(s.contains("30.0"), "display: {s}");
554 }
555
556 #[test]
557 fn image_format_display() {
558 assert_eq!(ImageFormat::Png.to_string(), "PNG");
559 assert_eq!(ImageFormat::Jpeg.to_string(), "JPEG");
560 assert_eq!(ImageFormat::Gif.to_string(), "GIF");
561 assert_eq!(ImageFormat::Bmp.to_string(), "BMP");
562 assert_eq!(ImageFormat::Wmf.to_string(), "WMF");
563 assert_eq!(ImageFormat::Emf.to_string(), "EMF");
564 assert_eq!(ImageFormat::Unknown("TIFF".to_string()).to_string(), "tiff");
565 }
566
567 #[test]
568 fn equality() {
569 let a = sample_image();
570 let b = sample_image();
571 assert_eq!(a, b);
572 }
573
574 #[test]
575 fn inequality_on_different_paths() {
576 let a = sample_image();
577 let mut b = sample_image();
578 b.path = "other.png".to_string();
579 assert_ne!(a, b);
580 }
581
582 #[test]
583 fn clone_independence() {
584 let img = sample_image();
585 let mut cloned = img.clone();
586 cloned.path = "modified.png".to_string();
587 assert_eq!(img.path, "BinData/image1.png");
588 }
589
590 #[test]
591 fn serde_roundtrip() {
592 let img = sample_image();
593 let json = serde_json::to_string(&img).unwrap();
594 let back: Image = serde_json::from_str(&json).unwrap();
595 assert_eq!(img, back);
596 }
597
598 #[test]
599 fn placement_roundtrip() {
600 let img = sample_image().with_placement(ImagePlacement {
601 text_wrap: ImageTextWrap::Square,
602 text_flow: ImageTextFlow::RightOnly,
603 treat_as_char: false,
604 flow_with_text: true,
605 allow_overlap: true,
606 vert_rel_to: ImageRelativeTo::Paper,
607 horz_rel_to: ImageRelativeTo::Page,
608 vert_offset: HwpUnit::new(1200).unwrap(),
609 horz_offset: HwpUnit::new(3400).unwrap(),
610 });
611 let json = serde_json::to_string(&img).unwrap();
612 let back: Image = serde_json::from_str(&json).unwrap();
613 assert_eq!(img, back);
614 }
615
616 #[test]
617 fn serde_unknown_format_roundtrip() {
618 let img = Image::new(
619 "test.svg",
620 HwpUnit::from_mm(10.0).unwrap(),
621 HwpUnit::from_mm(10.0).unwrap(),
622 ImageFormat::Unknown("SVG".to_string()),
623 );
624 let json = serde_json::to_string(&img).unwrap();
625 let back: Image = serde_json::from_str(&json).unwrap();
626 assert_eq!(img, back);
627 }
628
629 #[test]
630 fn image_format_hash() {
631 use std::collections::HashSet;
632 let mut set = HashSet::new();
633 set.insert(ImageFormat::Png);
634 set.insert(ImageFormat::Jpeg);
635 set.insert(ImageFormat::Png);
636 assert_eq!(set.len(), 2);
637 }
638
639 #[test]
640 fn from_string_path() {
641 let path = String::from("dynamic/path.bmp");
642 let img = Image::new(path, HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Bmp);
643 assert_eq!(img.path, "dynamic/path.bmp");
644 }
645
646 #[test]
651 fn image_store_new_is_empty() {
652 let store = ImageStore::new();
653 assert!(store.is_empty());
654 assert_eq!(store.len(), 0);
655 }
656
657 #[test]
658 fn image_store_insert_and_get() {
659 let mut store = ImageStore::new();
660 store.insert("logo.png", vec![0x89, 0x50, 0x4E, 0x47]);
661 assert_eq!(store.len(), 1);
662 assert!(!store.is_empty());
663 assert_eq!(store.get("logo.png"), Some(&[0x89, 0x50, 0x4E, 0x47][..]));
664 }
665
666 #[test]
667 fn image_store_get_missing() {
668 let store = ImageStore::new();
669 assert!(store.get("nonexistent.png").is_none());
670 }
671
672 #[test]
673 fn image_store_insert_replaces() {
674 let mut store = ImageStore::new();
675 store.insert("img.png", vec![1, 2, 3]);
676 store.insert("img.png", vec![4, 5, 6]);
677 assert_eq!(store.len(), 1);
678 assert_eq!(store.get("img.png"), Some(&[4, 5, 6][..]));
679 }
680
681 #[test]
682 fn image_store_multiple_images() {
683 let mut store = ImageStore::new();
684 store.insert("a.png", vec![1]);
685 store.insert("b.jpg", vec![2]);
686 store.insert("c.gif", vec![3]);
687 assert_eq!(store.len(), 3);
688 }
689
690 #[test]
691 fn image_store_iter() {
692 let mut store = ImageStore::new();
693 store.insert("a.png", vec![1]);
694 store.insert("b.jpg", vec![2]);
695 let pairs: Vec<_> = store.iter().collect();
696 assert_eq!(pairs.len(), 2);
697 }
698
699 #[test]
700 fn image_store_from_iterator() {
701 let items = vec![("a.png".to_string(), vec![1, 2]), ("b.jpg".to_string(), vec![3, 4])];
702 let store: ImageStore = items.into_iter().collect();
703 assert_eq!(store.len(), 2);
704 assert_eq!(store.get("a.png"), Some(&[1, 2][..]));
705 }
706
707 #[test]
708 fn image_store_default() {
709 let store = ImageStore::default();
710 assert!(store.is_empty());
711 }
712
713 #[test]
714 fn image_store_clone_independence() {
715 let mut store = ImageStore::new();
716 store.insert("img.png", vec![1, 2, 3]);
717 let mut cloned = store.clone();
718 cloned.insert("other.png", vec![4, 5]);
719 assert_eq!(store.len(), 1);
720 assert_eq!(cloned.len(), 2);
721 }
722
723 #[test]
724 fn image_store_equality() {
725 let mut a = ImageStore::new();
726 a.insert("img.png", vec![1, 2, 3]);
727 let mut b = ImageStore::new();
728 b.insert("img.png", vec![1, 2, 3]);
729 assert_eq!(a, b);
730 }
731
732 #[test]
733 fn image_store_serde_roundtrip() {
734 let mut store = ImageStore::new();
735 store.insert("logo.png", vec![0x89, 0x50]);
736 let json = serde_json::to_string(&store).unwrap();
737 let back: ImageStore = serde_json::from_str(&json).unwrap();
738 assert_eq!(store, back);
739 }
740
741 #[test]
742 fn image_store_string_key() {
743 let mut store = ImageStore::new();
744 let key = String::from("dynamic/path.png");
745 store.insert(key, vec![42]);
746 assert!(store.get("dynamic/path.png").is_some());
747 }
748
749 #[test]
754 fn from_extension_png() {
755 assert_eq!(ImageFormat::from_extension("photo.png"), ImageFormat::Png);
756 }
757
758 #[test]
759 fn from_extension_jpg_uppercase() {
760 assert_eq!(ImageFormat::from_extension("image.JPG"), ImageFormat::Jpeg);
761 }
762
763 #[test]
764 fn from_extension_jpeg() {
765 assert_eq!(ImageFormat::from_extension("file.jpeg"), ImageFormat::Jpeg);
766 }
767
768 #[test]
769 fn from_extension_gif() {
770 assert_eq!(ImageFormat::from_extension("doc.gif"), ImageFormat::Gif);
771 }
772
773 #[test]
774 fn from_extension_bmp() {
775 assert_eq!(ImageFormat::from_extension("img.bmp"), ImageFormat::Bmp);
776 }
777
778 #[test]
779 fn from_extension_wmf() {
780 assert_eq!(ImageFormat::from_extension("chart.wmf"), ImageFormat::Wmf);
781 }
782
783 #[test]
784 fn from_extension_emf() {
785 assert_eq!(ImageFormat::from_extension("dia.emf"), ImageFormat::Emf);
786 }
787
788 #[test]
789 fn from_extension_unknown() {
790 assert_eq!(
791 ImageFormat::from_extension("file.xyz"),
792 ImageFormat::Unknown("xyz".to_string()),
793 );
794 }
795
796 #[test]
797 fn from_extension_no_extension() {
798 assert_eq!(ImageFormat::from_extension("noext"), ImageFormat::Unknown(String::new()));
799 }
800
801 #[test]
802 fn from_extension_multi_dot() {
803 assert_eq!(ImageFormat::from_extension("multi.dot.png"), ImageFormat::Png);
804 }
805
806 #[test]
811 fn from_path_infers_format() {
812 let w = HwpUnit::from_mm(100.0).unwrap();
813 let h = HwpUnit::from_mm(75.0).unwrap();
814
815 let img = Image::from_path("photos/hero.png", w, h);
816 assert_eq!(img.format, ImageFormat::Png);
817 assert_eq!(img.path, "photos/hero.png");
818 assert_eq!(img.width, w);
819 assert_eq!(img.height, h);
820 assert!(img.caption.is_none());
821 }
822
823 #[test]
824 fn from_path_jpeg_uppercase() {
825 let w = HwpUnit::ZERO;
826 let h = HwpUnit::ZERO;
827 let img = Image::from_path("scan.JPG", w, h);
828 assert_eq!(img.format, ImageFormat::Jpeg);
829 }
830
831 #[test]
832 fn from_path_unknown_extension() {
833 let w = HwpUnit::ZERO;
834 let h = HwpUnit::ZERO;
835 let img = Image::from_path("diagram.svg", w, h);
836 assert_eq!(img.format, ImageFormat::Unknown("svg".to_string()));
837 }
838
839 #[test]
840 fn from_path_string_owned() {
841 let w = HwpUnit::ZERO;
842 let h = HwpUnit::ZERO;
843 let path = String::from("owned/path.bmp");
844 let img = Image::from_path(path, w, h);
845 assert_eq!(img.format, ImageFormat::Bmp);
846 assert_eq!(img.path, "owned/path.bmp");
847 }
848
849 #[test]
850 fn unknown_format_display_normalizes_to_lowercase() {
851 assert_eq!(ImageFormat::Unknown("SVG".to_string()).to_string(), "svg");
852 assert_eq!(ImageFormat::Unknown("Tiff".to_string()).to_string(), "tiff");
853 assert_eq!(ImageFormat::Unknown("webp".to_string()).to_string(), "webp");
854 }
855
856 #[test]
857 fn unknown_format_casing_inequality() {
858 let upper = ImageFormat::Unknown("SVG".to_string());
860 let lower = ImageFormat::Unknown("svg".to_string());
861 assert_ne!(upper, lower, "Different casing in Unknown produces inequality");
862 assert_eq!(upper.to_string(), lower.to_string());
864 }
865}