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;
30
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
56#[non_exhaustive]
57pub struct Image {
58 pub path: String,
60 pub width: HwpUnit,
62 pub height: HwpUnit,
64 pub format: ImageFormat,
66 pub caption: Option<Caption>,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub placement: Option<ImagePlacement>,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub inst_id: Option<u64>,
77}
78
79impl Image {
80 #[must_use]
97 pub fn new(
98 path: impl Into<String>,
99 width: HwpUnit,
100 height: HwpUnit,
101 format: ImageFormat,
102 ) -> Self {
103 Self {
104 path: path.into(),
105 width,
106 height,
107 format,
108 caption: None,
109 placement: None,
110 inst_id: None,
111 }
112 }
113
114 #[must_use]
138 pub fn from_path(path: impl Into<String>, width: HwpUnit, height: HwpUnit) -> Self {
139 let path: String = path.into();
140 let format = ImageFormat::from_extension(&path);
141 Self { path, width, height, format, caption: None, placement: None, inst_id: None }
142 }
143
144 #[must_use]
146 pub fn with_caption(mut self, caption: Caption) -> Self {
147 self.caption = Some(caption);
148 self
149 }
150
151 #[must_use]
153 pub fn with_placement(mut self, placement: ImagePlacement) -> Self {
154 self.placement = Some(placement);
155 self
156 }
157}
158
159impl std::fmt::Display for Image {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 write!(
162 f,
163 "Image({}, {:.1}mm x {:.1}mm)",
164 self.format,
165 self.width.to_mm(),
166 self.height.to_mm()
167 )
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
173pub struct ImagePlacement {
174 pub text_wrap: ImageTextWrap,
176 pub text_flow: ImageTextFlow,
178 pub treat_as_char: bool,
180 pub flow_with_text: bool,
182 pub allow_overlap: bool,
184 pub vert_rel_to: ImageRelativeTo,
186 pub horz_rel_to: ImageRelativeTo,
188 pub vert_offset: HwpUnit,
190 pub horz_offset: HwpUnit,
192}
193
194impl ImagePlacement {
195 pub fn legacy_inline_defaults() -> Self {
197 Self {
198 text_wrap: ImageTextWrap::TopAndBottom,
199 text_flow: ImageTextFlow::BothSides,
200 treat_as_char: true,
201 flow_with_text: false,
202 allow_overlap: false,
203 vert_rel_to: ImageRelativeTo::Para,
204 horz_rel_to: ImageRelativeTo::Para,
205 vert_offset: HwpUnit::ZERO,
206 horz_offset: HwpUnit::ZERO,
207 }
208 }
209}
210
211#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
213#[non_exhaustive]
214pub enum ImageTextWrap {
215 TopAndBottom,
217 Square,
219 BehindText,
221 InFrontOfText,
223 Tight,
225 Through,
227 Other(String),
229}
230
231impl ImageTextWrap {
232 pub fn from_hwpx(value: &str) -> Self {
234 match value {
235 "TOP_AND_BOTTOM" => Self::TopAndBottom,
236 "SQUARE" => Self::Square,
237 "BEHIND_TEXT" => Self::BehindText,
238 "IN_FRONT_OF_TEXT" => Self::InFrontOfText,
239 "TIGHT" => Self::Tight,
240 "THROUGH" => Self::Through,
241 other => Self::Other(other.to_string()),
242 }
243 }
244
245 pub fn as_hwpx_str(&self) -> Cow<'_, str> {
247 match self {
248 Self::TopAndBottom => Cow::Borrowed("TOP_AND_BOTTOM"),
249 Self::Square => Cow::Borrowed("SQUARE"),
250 Self::BehindText => Cow::Borrowed("BEHIND_TEXT"),
251 Self::InFrontOfText => Cow::Borrowed("IN_FRONT_OF_TEXT"),
252 Self::Tight => Cow::Borrowed("TIGHT"),
253 Self::Through => Cow::Borrowed("THROUGH"),
254 Self::Other(value) => Cow::Borrowed(value.as_str()),
255 }
256 }
257}
258
259#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
261#[non_exhaustive]
262pub enum ImageTextFlow {
263 BothSides,
265 LeftOnly,
267 RightOnly,
269 LargestOnly,
271 Other(String),
273}
274
275impl ImageTextFlow {
276 pub fn from_hwpx(value: &str) -> Self {
278 match value {
279 "BOTH_SIDES" => Self::BothSides,
280 "LEFT_ONLY" => Self::LeftOnly,
281 "RIGHT_ONLY" => Self::RightOnly,
282 "LARGEST_ONLY" => Self::LargestOnly,
283 other => Self::Other(other.to_string()),
284 }
285 }
286
287 pub fn as_hwpx_str(&self) -> Cow<'_, str> {
289 match self {
290 Self::BothSides => Cow::Borrowed("BOTH_SIDES"),
291 Self::LeftOnly => Cow::Borrowed("LEFT_ONLY"),
292 Self::RightOnly => Cow::Borrowed("RIGHT_ONLY"),
293 Self::LargestOnly => Cow::Borrowed("LARGEST_ONLY"),
294 Self::Other(value) => Cow::Borrowed(value.as_str()),
295 }
296 }
297}
298
299#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
301#[non_exhaustive]
302pub enum ImageRelativeTo {
303 Paper,
305 Page,
307 Para,
309 Column,
311 Character,
313 Line,
315 Other(String),
317}
318
319impl ImageRelativeTo {
320 pub fn from_hwpx(value: &str) -> Self {
322 match value {
323 "PAPER" => Self::Paper,
324 "PAGE" => Self::Page,
325 "PARA" => Self::Para,
326 "COLUMN" => Self::Column,
327 "CHAR" => Self::Character,
328 "LINE" => Self::Line,
329 other => Self::Other(other.to_string()),
330 }
331 }
332
333 pub fn as_hwpx_str(&self) -> Cow<'_, str> {
335 match self {
336 Self::Paper => Cow::Borrowed("PAPER"),
337 Self::Page => Cow::Borrowed("PAGE"),
338 Self::Para => Cow::Borrowed("PARA"),
339 Self::Column => Cow::Borrowed("COLUMN"),
340 Self::Character => Cow::Borrowed("CHAR"),
341 Self::Line => Cow::Borrowed("LINE"),
342 Self::Other(value) => Cow::Borrowed(value.as_str()),
343 }
344 }
345}
346
347#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
364#[non_exhaustive]
365pub enum ImageFormat {
366 Png,
368 Jpeg,
370 Gif,
372 Bmp,
374 Wmf,
376 Emf,
378 Unknown(String),
380}
381
382impl ImageFormat {
383 pub fn from_extension(path: &str) -> Self {
413 let ext_lower = path.rfind('.').map(|i| path[i + 1..].to_ascii_lowercase());
415 match ext_lower.as_deref() {
416 Some("png") => Self::Png,
417 Some("jpg" | "jpeg") => Self::Jpeg,
418 Some("gif") => Self::Gif,
419 Some("bmp") => Self::Bmp,
420 Some("wmf") => Self::Wmf,
421 Some("emf") => Self::Emf,
422 Some(ext) => Self::Unknown(ext.to_string()),
423 None => Self::Unknown(String::new()),
424 }
425 }
426}
427
428impl std::fmt::Display for ImageFormat {
429 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
430 match self {
431 Self::Png => write!(f, "PNG"),
432 Self::Jpeg => write!(f, "JPEG"),
433 Self::Gif => write!(f, "GIF"),
434 Self::Bmp => write!(f, "BMP"),
435 Self::Wmf => write!(f, "WMF"),
436 Self::Emf => write!(f, "EMF"),
437 Self::Unknown(s) => {
438 let lower = s.to_ascii_lowercase();
439 write!(f, "{lower}")
440 }
441 }
442 }
443}
444
445#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
466pub struct ImageStore {
467 images: HashMap<String, Vec<u8>>,
468}
469
470impl ImageStore {
471 pub fn new() -> Self {
473 Self { images: HashMap::new() }
474 }
475
476 pub fn insert(&mut self, key: impl Into<String>, data: Vec<u8>) {
480 self.images.insert(key.into(), data);
481 }
482
483 pub fn get(&self, key: &str) -> Option<&[u8]> {
485 self.images.get(key).map(|v| v.as_slice())
486 }
487
488 pub fn len(&self) -> usize {
490 self.images.len()
491 }
492
493 pub fn is_empty(&self) -> bool {
495 self.images.is_empty()
496 }
497
498 pub fn iter(&self) -> impl Iterator<Item = (&str, &[u8])> {
500 self.images.iter().map(|(k, v)| (k.as_str(), v.as_slice()))
501 }
502}
503
504impl FromIterator<(String, Vec<u8>)> for ImageStore {
505 fn from_iter<I: IntoIterator<Item = (String, Vec<u8>)>>(iter: I) -> Self {
506 Self { images: iter.into_iter().collect() }
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513
514 fn sample_image() -> Image {
515 Image::new(
516 "BinData/image1.png",
517 HwpUnit::from_mm(50.0).unwrap(),
518 HwpUnit::from_mm(30.0).unwrap(),
519 ImageFormat::Png,
520 )
521 }
522
523 #[test]
524 fn new_constructor() {
525 let img = sample_image();
526 assert_eq!(img.path, "BinData/image1.png");
527 assert_eq!(img.format, ImageFormat::Png);
528 }
529
530 #[test]
531 fn from_path_constructor() {
532 let img = Image::from_path(
533 "test.jpeg",
534 HwpUnit::from_mm(10.0).unwrap(),
535 HwpUnit::from_mm(10.0).unwrap(),
536 );
537 assert_eq!(img.format, ImageFormat::Jpeg);
538 }
539
540 #[test]
541 fn builder_attaches_caption() {
542 let img = sample_image().with_caption(Caption::default());
543 assert!(img.caption.is_some());
544 }
545
546 #[test]
547 fn display_format() {
548 let img = sample_image();
549 let s = img.to_string();
550 assert!(s.contains("PNG"), "display: {s}");
551 assert!(s.contains("50.0"), "display: {s}");
552 assert!(s.contains("30.0"), "display: {s}");
553 }
554
555 #[test]
556 fn image_format_display() {
557 assert_eq!(ImageFormat::Png.to_string(), "PNG");
558 assert_eq!(ImageFormat::Jpeg.to_string(), "JPEG");
559 assert_eq!(ImageFormat::Gif.to_string(), "GIF");
560 assert_eq!(ImageFormat::Bmp.to_string(), "BMP");
561 assert_eq!(ImageFormat::Wmf.to_string(), "WMF");
562 assert_eq!(ImageFormat::Emf.to_string(), "EMF");
563 assert_eq!(ImageFormat::Unknown("TIFF".to_string()).to_string(), "tiff");
564 }
565
566 #[test]
567 fn equality() {
568 let a = sample_image();
569 let b = sample_image();
570 assert_eq!(a, b);
571 }
572
573 #[test]
574 fn inequality_on_different_paths() {
575 let a = sample_image();
576 let mut b = sample_image();
577 b.path = "other.png".to_string();
578 assert_ne!(a, b);
579 }
580
581 #[test]
582 fn clone_independence() {
583 let img = sample_image();
584 let mut cloned = img.clone();
585 cloned.path = "modified.png".to_string();
586 assert_eq!(img.path, "BinData/image1.png");
587 }
588
589 #[test]
590 fn serde_roundtrip() {
591 let img = sample_image();
592 let json = serde_json::to_string(&img).unwrap();
593 let back: Image = serde_json::from_str(&json).unwrap();
594 assert_eq!(img, back);
595 }
596
597 #[test]
598 fn placement_roundtrip() {
599 let img = sample_image().with_placement(ImagePlacement {
600 text_wrap: ImageTextWrap::Square,
601 text_flow: ImageTextFlow::RightOnly,
602 treat_as_char: false,
603 flow_with_text: true,
604 allow_overlap: true,
605 vert_rel_to: ImageRelativeTo::Paper,
606 horz_rel_to: ImageRelativeTo::Page,
607 vert_offset: HwpUnit::new(1200).unwrap(),
608 horz_offset: HwpUnit::new(3400).unwrap(),
609 });
610 let json = serde_json::to_string(&img).unwrap();
611 let back: Image = serde_json::from_str(&json).unwrap();
612 assert_eq!(img, back);
613 }
614
615 #[test]
616 fn serde_unknown_format_roundtrip() {
617 let img = Image::new(
618 "test.svg",
619 HwpUnit::from_mm(10.0).unwrap(),
620 HwpUnit::from_mm(10.0).unwrap(),
621 ImageFormat::Unknown("SVG".to_string()),
622 );
623 let json = serde_json::to_string(&img).unwrap();
624 let back: Image = serde_json::from_str(&json).unwrap();
625 assert_eq!(img, back);
626 }
627
628 #[test]
629 fn image_format_hash() {
630 use std::collections::HashSet;
631 let mut set = HashSet::new();
632 set.insert(ImageFormat::Png);
633 set.insert(ImageFormat::Jpeg);
634 set.insert(ImageFormat::Png);
635 assert_eq!(set.len(), 2);
636 }
637
638 #[test]
639 fn from_string_path() {
640 let path = String::from("dynamic/path.bmp");
641 let img = Image::new(path, HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Bmp);
642 assert_eq!(img.path, "dynamic/path.bmp");
643 }
644
645 #[test]
650 fn image_store_new_is_empty() {
651 let store = ImageStore::new();
652 assert!(store.is_empty());
653 assert_eq!(store.len(), 0);
654 }
655
656 #[test]
657 fn image_store_insert_and_get() {
658 let mut store = ImageStore::new();
659 store.insert("logo.png", vec![0x89, 0x50, 0x4E, 0x47]);
660 assert_eq!(store.len(), 1);
661 assert!(!store.is_empty());
662 assert_eq!(store.get("logo.png"), Some(&[0x89, 0x50, 0x4E, 0x47][..]));
663 }
664
665 #[test]
666 fn image_store_get_missing() {
667 let store = ImageStore::new();
668 assert!(store.get("nonexistent.png").is_none());
669 }
670
671 #[test]
672 fn image_store_insert_replaces() {
673 let mut store = ImageStore::new();
674 store.insert("img.png", vec![1, 2, 3]);
675 store.insert("img.png", vec![4, 5, 6]);
676 assert_eq!(store.len(), 1);
677 assert_eq!(store.get("img.png"), Some(&[4, 5, 6][..]));
678 }
679
680 #[test]
681 fn image_store_multiple_images() {
682 let mut store = ImageStore::new();
683 store.insert("a.png", vec![1]);
684 store.insert("b.jpg", vec![2]);
685 store.insert("c.gif", vec![3]);
686 assert_eq!(store.len(), 3);
687 }
688
689 #[test]
690 fn image_store_iter() {
691 let mut store = ImageStore::new();
692 store.insert("a.png", vec![1]);
693 store.insert("b.jpg", vec![2]);
694 let pairs: Vec<_> = store.iter().collect();
695 assert_eq!(pairs.len(), 2);
696 }
697
698 #[test]
699 fn image_store_from_iterator() {
700 let items = vec![("a.png".to_string(), vec![1, 2]), ("b.jpg".to_string(), vec![3, 4])];
701 let store: ImageStore = items.into_iter().collect();
702 assert_eq!(store.len(), 2);
703 assert_eq!(store.get("a.png"), Some(&[1, 2][..]));
704 }
705
706 #[test]
707 fn image_store_default() {
708 let store = ImageStore::default();
709 assert!(store.is_empty());
710 }
711
712 #[test]
713 fn image_store_clone_independence() {
714 let mut store = ImageStore::new();
715 store.insert("img.png", vec![1, 2, 3]);
716 let mut cloned = store.clone();
717 cloned.insert("other.png", vec![4, 5]);
718 assert_eq!(store.len(), 1);
719 assert_eq!(cloned.len(), 2);
720 }
721
722 #[test]
723 fn image_store_equality() {
724 let mut a = ImageStore::new();
725 a.insert("img.png", vec![1, 2, 3]);
726 let mut b = ImageStore::new();
727 b.insert("img.png", vec![1, 2, 3]);
728 assert_eq!(a, b);
729 }
730
731 #[test]
732 fn image_store_serde_roundtrip() {
733 let mut store = ImageStore::new();
734 store.insert("logo.png", vec![0x89, 0x50]);
735 let json = serde_json::to_string(&store).unwrap();
736 let back: ImageStore = serde_json::from_str(&json).unwrap();
737 assert_eq!(store, back);
738 }
739
740 #[test]
741 fn image_store_string_key() {
742 let mut store = ImageStore::new();
743 let key = String::from("dynamic/path.png");
744 store.insert(key, vec![42]);
745 assert!(store.get("dynamic/path.png").is_some());
746 }
747
748 #[test]
753 fn from_extension_png() {
754 assert_eq!(ImageFormat::from_extension("photo.png"), ImageFormat::Png);
755 }
756
757 #[test]
758 fn from_extension_jpg_uppercase() {
759 assert_eq!(ImageFormat::from_extension("image.JPG"), ImageFormat::Jpeg);
760 }
761
762 #[test]
763 fn from_extension_jpeg() {
764 assert_eq!(ImageFormat::from_extension("file.jpeg"), ImageFormat::Jpeg);
765 }
766
767 #[test]
768 fn from_extension_gif() {
769 assert_eq!(ImageFormat::from_extension("doc.gif"), ImageFormat::Gif);
770 }
771
772 #[test]
773 fn from_extension_bmp() {
774 assert_eq!(ImageFormat::from_extension("img.bmp"), ImageFormat::Bmp);
775 }
776
777 #[test]
778 fn from_extension_wmf() {
779 assert_eq!(ImageFormat::from_extension("chart.wmf"), ImageFormat::Wmf);
780 }
781
782 #[test]
783 fn from_extension_emf() {
784 assert_eq!(ImageFormat::from_extension("dia.emf"), ImageFormat::Emf);
785 }
786
787 #[test]
788 fn from_extension_unknown() {
789 assert_eq!(
790 ImageFormat::from_extension("file.xyz"),
791 ImageFormat::Unknown("xyz".to_string()),
792 );
793 }
794
795 #[test]
796 fn from_extension_no_extension() {
797 assert_eq!(ImageFormat::from_extension("noext"), ImageFormat::Unknown(String::new()));
798 }
799
800 #[test]
801 fn from_extension_multi_dot() {
802 assert_eq!(ImageFormat::from_extension("multi.dot.png"), ImageFormat::Png);
803 }
804
805 #[test]
810 fn from_path_infers_format() {
811 let w = HwpUnit::from_mm(100.0).unwrap();
812 let h = HwpUnit::from_mm(75.0).unwrap();
813
814 let img = Image::from_path("photos/hero.png", w, h);
815 assert_eq!(img.format, ImageFormat::Png);
816 assert_eq!(img.path, "photos/hero.png");
817 assert_eq!(img.width, w);
818 assert_eq!(img.height, h);
819 assert!(img.caption.is_none());
820 }
821
822 #[test]
823 fn from_path_jpeg_uppercase() {
824 let w = HwpUnit::ZERO;
825 let h = HwpUnit::ZERO;
826 let img = Image::from_path("scan.JPG", w, h);
827 assert_eq!(img.format, ImageFormat::Jpeg);
828 }
829
830 #[test]
831 fn from_path_unknown_extension() {
832 let w = HwpUnit::ZERO;
833 let h = HwpUnit::ZERO;
834 let img = Image::from_path("diagram.svg", w, h);
835 assert_eq!(img.format, ImageFormat::Unknown("svg".to_string()));
836 }
837
838 #[test]
839 fn from_path_string_owned() {
840 let w = HwpUnit::ZERO;
841 let h = HwpUnit::ZERO;
842 let path = String::from("owned/path.bmp");
843 let img = Image::from_path(path, w, h);
844 assert_eq!(img.format, ImageFormat::Bmp);
845 assert_eq!(img.path, "owned/path.bmp");
846 }
847
848 #[test]
849 fn unknown_format_display_normalizes_to_lowercase() {
850 assert_eq!(ImageFormat::Unknown("SVG".to_string()).to_string(), "svg");
851 assert_eq!(ImageFormat::Unknown("Tiff".to_string()).to_string(), "tiff");
852 assert_eq!(ImageFormat::Unknown("webp".to_string()).to_string(), "webp");
853 }
854
855 #[test]
856 fn unknown_format_casing_inequality() {
857 let upper = ImageFormat::Unknown("SVG".to_string());
859 let lower = ImageFormat::Unknown("svg".to_string());
860 assert_ne!(upper, lower, "Different casing in Unknown produces inequality");
861 assert_eq!(upper.to_string(), lower.to_string());
863 }
864}