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}
72
73impl Image {
74 #[must_use]
91 pub fn new(
92 path: impl Into<String>,
93 width: HwpUnit,
94 height: HwpUnit,
95 format: ImageFormat,
96 ) -> Self {
97 Self { path: path.into(), width, height, format, caption: None, placement: None }
98 }
99
100 #[must_use]
124 pub fn from_path(path: impl Into<String>, width: HwpUnit, height: HwpUnit) -> Self {
125 let path: String = path.into();
126 let format = ImageFormat::from_extension(&path);
127 Self { path, width, height, format, caption: None, placement: None }
128 }
129
130 #[must_use]
132 pub fn with_caption(mut self, caption: Caption) -> Self {
133 self.caption = Some(caption);
134 self
135 }
136
137 #[must_use]
139 pub fn with_placement(mut self, placement: ImagePlacement) -> Self {
140 self.placement = Some(placement);
141 self
142 }
143}
144
145impl std::fmt::Display for Image {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 write!(
148 f,
149 "Image({}, {:.1}mm x {:.1}mm)",
150 self.format,
151 self.width.to_mm(),
152 self.height.to_mm()
153 )
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
159pub struct ImagePlacement {
160 pub text_wrap: ImageTextWrap,
162 pub text_flow: ImageTextFlow,
164 pub treat_as_char: bool,
166 pub flow_with_text: bool,
168 pub allow_overlap: bool,
170 pub vert_rel_to: ImageRelativeTo,
172 pub horz_rel_to: ImageRelativeTo,
174 pub vert_offset: HwpUnit,
176 pub horz_offset: HwpUnit,
178}
179
180impl ImagePlacement {
181 pub fn legacy_inline_defaults() -> Self {
183 Self {
184 text_wrap: ImageTextWrap::TopAndBottom,
185 text_flow: ImageTextFlow::BothSides,
186 treat_as_char: true,
187 flow_with_text: false,
188 allow_overlap: false,
189 vert_rel_to: ImageRelativeTo::Para,
190 horz_rel_to: ImageRelativeTo::Para,
191 vert_offset: HwpUnit::ZERO,
192 horz_offset: HwpUnit::ZERO,
193 }
194 }
195}
196
197#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
199#[non_exhaustive]
200pub enum ImageTextWrap {
201 TopAndBottom,
203 Square,
205 BehindText,
207 InFrontOfText,
209 Tight,
211 Through,
213 Other(String),
215}
216
217impl ImageTextWrap {
218 pub fn from_hwpx(value: &str) -> Self {
220 match value {
221 "TOP_AND_BOTTOM" => Self::TopAndBottom,
222 "SQUARE" => Self::Square,
223 "BEHIND_TEXT" => Self::BehindText,
224 "IN_FRONT_OF_TEXT" => Self::InFrontOfText,
225 "TIGHT" => Self::Tight,
226 "THROUGH" => Self::Through,
227 other => Self::Other(other.to_string()),
228 }
229 }
230
231 pub fn as_hwpx_str(&self) -> Cow<'_, str> {
233 match self {
234 Self::TopAndBottom => Cow::Borrowed("TOP_AND_BOTTOM"),
235 Self::Square => Cow::Borrowed("SQUARE"),
236 Self::BehindText => Cow::Borrowed("BEHIND_TEXT"),
237 Self::InFrontOfText => Cow::Borrowed("IN_FRONT_OF_TEXT"),
238 Self::Tight => Cow::Borrowed("TIGHT"),
239 Self::Through => Cow::Borrowed("THROUGH"),
240 Self::Other(value) => Cow::Borrowed(value.as_str()),
241 }
242 }
243}
244
245#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
247#[non_exhaustive]
248pub enum ImageTextFlow {
249 BothSides,
251 LeftOnly,
253 RightOnly,
255 LargestOnly,
257 Other(String),
259}
260
261impl ImageTextFlow {
262 pub fn from_hwpx(value: &str) -> Self {
264 match value {
265 "BOTH_SIDES" => Self::BothSides,
266 "LEFT_ONLY" => Self::LeftOnly,
267 "RIGHT_ONLY" => Self::RightOnly,
268 "LARGEST_ONLY" => Self::LargestOnly,
269 other => Self::Other(other.to_string()),
270 }
271 }
272
273 pub fn as_hwpx_str(&self) -> Cow<'_, str> {
275 match self {
276 Self::BothSides => Cow::Borrowed("BOTH_SIDES"),
277 Self::LeftOnly => Cow::Borrowed("LEFT_ONLY"),
278 Self::RightOnly => Cow::Borrowed("RIGHT_ONLY"),
279 Self::LargestOnly => Cow::Borrowed("LARGEST_ONLY"),
280 Self::Other(value) => Cow::Borrowed(value.as_str()),
281 }
282 }
283}
284
285#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
287#[non_exhaustive]
288pub enum ImageRelativeTo {
289 Paper,
291 Page,
293 Para,
295 Column,
297 Character,
299 Line,
301 Other(String),
303}
304
305impl ImageRelativeTo {
306 pub fn from_hwpx(value: &str) -> Self {
308 match value {
309 "PAPER" => Self::Paper,
310 "PAGE" => Self::Page,
311 "PARA" => Self::Para,
312 "COLUMN" => Self::Column,
313 "CHAR" => Self::Character,
314 "LINE" => Self::Line,
315 other => Self::Other(other.to_string()),
316 }
317 }
318
319 pub fn as_hwpx_str(&self) -> Cow<'_, str> {
321 match self {
322 Self::Paper => Cow::Borrowed("PAPER"),
323 Self::Page => Cow::Borrowed("PAGE"),
324 Self::Para => Cow::Borrowed("PARA"),
325 Self::Column => Cow::Borrowed("COLUMN"),
326 Self::Character => Cow::Borrowed("CHAR"),
327 Self::Line => Cow::Borrowed("LINE"),
328 Self::Other(value) => Cow::Borrowed(value.as_str()),
329 }
330 }
331}
332
333#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
350#[non_exhaustive]
351pub enum ImageFormat {
352 Png,
354 Jpeg,
356 Gif,
358 Bmp,
360 Wmf,
362 Emf,
364 Unknown(String),
366}
367
368impl ImageFormat {
369 pub fn from_extension(path: &str) -> Self {
399 let ext_lower = path.rfind('.').map(|i| path[i + 1..].to_ascii_lowercase());
401 match ext_lower.as_deref() {
402 Some("png") => Self::Png,
403 Some("jpg" | "jpeg") => Self::Jpeg,
404 Some("gif") => Self::Gif,
405 Some("bmp") => Self::Bmp,
406 Some("wmf") => Self::Wmf,
407 Some("emf") => Self::Emf,
408 Some(ext) => Self::Unknown(ext.to_string()),
409 None => Self::Unknown(String::new()),
410 }
411 }
412}
413
414impl std::fmt::Display for ImageFormat {
415 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416 match self {
417 Self::Png => write!(f, "PNG"),
418 Self::Jpeg => write!(f, "JPEG"),
419 Self::Gif => write!(f, "GIF"),
420 Self::Bmp => write!(f, "BMP"),
421 Self::Wmf => write!(f, "WMF"),
422 Self::Emf => write!(f, "EMF"),
423 Self::Unknown(s) => {
424 let lower = s.to_ascii_lowercase();
425 write!(f, "{lower}")
426 }
427 }
428 }
429}
430
431#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
452pub struct ImageStore {
453 images: HashMap<String, Vec<u8>>,
454}
455
456impl ImageStore {
457 pub fn new() -> Self {
459 Self { images: HashMap::new() }
460 }
461
462 pub fn insert(&mut self, key: impl Into<String>, data: Vec<u8>) {
466 self.images.insert(key.into(), data);
467 }
468
469 pub fn get(&self, key: &str) -> Option<&[u8]> {
471 self.images.get(key).map(|v| v.as_slice())
472 }
473
474 pub fn len(&self) -> usize {
476 self.images.len()
477 }
478
479 pub fn is_empty(&self) -> bool {
481 self.images.is_empty()
482 }
483
484 pub fn iter(&self) -> impl Iterator<Item = (&str, &[u8])> {
486 self.images.iter().map(|(k, v)| (k.as_str(), v.as_slice()))
487 }
488}
489
490impl FromIterator<(String, Vec<u8>)> for ImageStore {
491 fn from_iter<I: IntoIterator<Item = (String, Vec<u8>)>>(iter: I) -> Self {
492 Self { images: iter.into_iter().collect() }
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 fn sample_image() -> Image {
501 Image::new(
502 "BinData/image1.png",
503 HwpUnit::from_mm(50.0).unwrap(),
504 HwpUnit::from_mm(30.0).unwrap(),
505 ImageFormat::Png,
506 )
507 }
508
509 #[test]
510 fn new_constructor() {
511 let img = sample_image();
512 assert_eq!(img.path, "BinData/image1.png");
513 assert_eq!(img.format, ImageFormat::Png);
514 }
515
516 #[test]
517 fn from_path_constructor() {
518 let img = Image::from_path(
519 "test.jpeg",
520 HwpUnit::from_mm(10.0).unwrap(),
521 HwpUnit::from_mm(10.0).unwrap(),
522 );
523 assert_eq!(img.format, ImageFormat::Jpeg);
524 }
525
526 #[test]
527 fn builder_attaches_caption() {
528 let img = sample_image().with_caption(Caption::default());
529 assert!(img.caption.is_some());
530 }
531
532 #[test]
533 fn display_format() {
534 let img = sample_image();
535 let s = img.to_string();
536 assert!(s.contains("PNG"), "display: {s}");
537 assert!(s.contains("50.0"), "display: {s}");
538 assert!(s.contains("30.0"), "display: {s}");
539 }
540
541 #[test]
542 fn image_format_display() {
543 assert_eq!(ImageFormat::Png.to_string(), "PNG");
544 assert_eq!(ImageFormat::Jpeg.to_string(), "JPEG");
545 assert_eq!(ImageFormat::Gif.to_string(), "GIF");
546 assert_eq!(ImageFormat::Bmp.to_string(), "BMP");
547 assert_eq!(ImageFormat::Wmf.to_string(), "WMF");
548 assert_eq!(ImageFormat::Emf.to_string(), "EMF");
549 assert_eq!(ImageFormat::Unknown("TIFF".to_string()).to_string(), "tiff");
550 }
551
552 #[test]
553 fn equality() {
554 let a = sample_image();
555 let b = sample_image();
556 assert_eq!(a, b);
557 }
558
559 #[test]
560 fn inequality_on_different_paths() {
561 let a = sample_image();
562 let mut b = sample_image();
563 b.path = "other.png".to_string();
564 assert_ne!(a, b);
565 }
566
567 #[test]
568 fn clone_independence() {
569 let img = sample_image();
570 let mut cloned = img.clone();
571 cloned.path = "modified.png".to_string();
572 assert_eq!(img.path, "BinData/image1.png");
573 }
574
575 #[test]
576 fn serde_roundtrip() {
577 let img = sample_image();
578 let json = serde_json::to_string(&img).unwrap();
579 let back: Image = serde_json::from_str(&json).unwrap();
580 assert_eq!(img, back);
581 }
582
583 #[test]
584 fn placement_roundtrip() {
585 let img = sample_image().with_placement(ImagePlacement {
586 text_wrap: ImageTextWrap::Square,
587 text_flow: ImageTextFlow::RightOnly,
588 treat_as_char: false,
589 flow_with_text: true,
590 allow_overlap: true,
591 vert_rel_to: ImageRelativeTo::Paper,
592 horz_rel_to: ImageRelativeTo::Page,
593 vert_offset: HwpUnit::new(1200).unwrap(),
594 horz_offset: HwpUnit::new(3400).unwrap(),
595 });
596 let json = serde_json::to_string(&img).unwrap();
597 let back: Image = serde_json::from_str(&json).unwrap();
598 assert_eq!(img, back);
599 }
600
601 #[test]
602 fn serde_unknown_format_roundtrip() {
603 let img = Image::new(
604 "test.svg",
605 HwpUnit::from_mm(10.0).unwrap(),
606 HwpUnit::from_mm(10.0).unwrap(),
607 ImageFormat::Unknown("SVG".to_string()),
608 );
609 let json = serde_json::to_string(&img).unwrap();
610 let back: Image = serde_json::from_str(&json).unwrap();
611 assert_eq!(img, back);
612 }
613
614 #[test]
615 fn image_format_hash() {
616 use std::collections::HashSet;
617 let mut set = HashSet::new();
618 set.insert(ImageFormat::Png);
619 set.insert(ImageFormat::Jpeg);
620 set.insert(ImageFormat::Png);
621 assert_eq!(set.len(), 2);
622 }
623
624 #[test]
625 fn from_string_path() {
626 let path = String::from("dynamic/path.bmp");
627 let img = Image::new(path, HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Bmp);
628 assert_eq!(img.path, "dynamic/path.bmp");
629 }
630
631 #[test]
636 fn image_store_new_is_empty() {
637 let store = ImageStore::new();
638 assert!(store.is_empty());
639 assert_eq!(store.len(), 0);
640 }
641
642 #[test]
643 fn image_store_insert_and_get() {
644 let mut store = ImageStore::new();
645 store.insert("logo.png", vec![0x89, 0x50, 0x4E, 0x47]);
646 assert_eq!(store.len(), 1);
647 assert!(!store.is_empty());
648 assert_eq!(store.get("logo.png"), Some(&[0x89, 0x50, 0x4E, 0x47][..]));
649 }
650
651 #[test]
652 fn image_store_get_missing() {
653 let store = ImageStore::new();
654 assert!(store.get("nonexistent.png").is_none());
655 }
656
657 #[test]
658 fn image_store_insert_replaces() {
659 let mut store = ImageStore::new();
660 store.insert("img.png", vec![1, 2, 3]);
661 store.insert("img.png", vec![4, 5, 6]);
662 assert_eq!(store.len(), 1);
663 assert_eq!(store.get("img.png"), Some(&[4, 5, 6][..]));
664 }
665
666 #[test]
667 fn image_store_multiple_images() {
668 let mut store = ImageStore::new();
669 store.insert("a.png", vec![1]);
670 store.insert("b.jpg", vec![2]);
671 store.insert("c.gif", vec![3]);
672 assert_eq!(store.len(), 3);
673 }
674
675 #[test]
676 fn image_store_iter() {
677 let mut store = ImageStore::new();
678 store.insert("a.png", vec![1]);
679 store.insert("b.jpg", vec![2]);
680 let pairs: Vec<_> = store.iter().collect();
681 assert_eq!(pairs.len(), 2);
682 }
683
684 #[test]
685 fn image_store_from_iterator() {
686 let items = vec![("a.png".to_string(), vec![1, 2]), ("b.jpg".to_string(), vec![3, 4])];
687 let store: ImageStore = items.into_iter().collect();
688 assert_eq!(store.len(), 2);
689 assert_eq!(store.get("a.png"), Some(&[1, 2][..]));
690 }
691
692 #[test]
693 fn image_store_default() {
694 let store = ImageStore::default();
695 assert!(store.is_empty());
696 }
697
698 #[test]
699 fn image_store_clone_independence() {
700 let mut store = ImageStore::new();
701 store.insert("img.png", vec![1, 2, 3]);
702 let mut cloned = store.clone();
703 cloned.insert("other.png", vec![4, 5]);
704 assert_eq!(store.len(), 1);
705 assert_eq!(cloned.len(), 2);
706 }
707
708 #[test]
709 fn image_store_equality() {
710 let mut a = ImageStore::new();
711 a.insert("img.png", vec![1, 2, 3]);
712 let mut b = ImageStore::new();
713 b.insert("img.png", vec![1, 2, 3]);
714 assert_eq!(a, b);
715 }
716
717 #[test]
718 fn image_store_serde_roundtrip() {
719 let mut store = ImageStore::new();
720 store.insert("logo.png", vec![0x89, 0x50]);
721 let json = serde_json::to_string(&store).unwrap();
722 let back: ImageStore = serde_json::from_str(&json).unwrap();
723 assert_eq!(store, back);
724 }
725
726 #[test]
727 fn image_store_string_key() {
728 let mut store = ImageStore::new();
729 let key = String::from("dynamic/path.png");
730 store.insert(key, vec![42]);
731 assert!(store.get("dynamic/path.png").is_some());
732 }
733
734 #[test]
739 fn from_extension_png() {
740 assert_eq!(ImageFormat::from_extension("photo.png"), ImageFormat::Png);
741 }
742
743 #[test]
744 fn from_extension_jpg_uppercase() {
745 assert_eq!(ImageFormat::from_extension("image.JPG"), ImageFormat::Jpeg);
746 }
747
748 #[test]
749 fn from_extension_jpeg() {
750 assert_eq!(ImageFormat::from_extension("file.jpeg"), ImageFormat::Jpeg);
751 }
752
753 #[test]
754 fn from_extension_gif() {
755 assert_eq!(ImageFormat::from_extension("doc.gif"), ImageFormat::Gif);
756 }
757
758 #[test]
759 fn from_extension_bmp() {
760 assert_eq!(ImageFormat::from_extension("img.bmp"), ImageFormat::Bmp);
761 }
762
763 #[test]
764 fn from_extension_wmf() {
765 assert_eq!(ImageFormat::from_extension("chart.wmf"), ImageFormat::Wmf);
766 }
767
768 #[test]
769 fn from_extension_emf() {
770 assert_eq!(ImageFormat::from_extension("dia.emf"), ImageFormat::Emf);
771 }
772
773 #[test]
774 fn from_extension_unknown() {
775 assert_eq!(
776 ImageFormat::from_extension("file.xyz"),
777 ImageFormat::Unknown("xyz".to_string()),
778 );
779 }
780
781 #[test]
782 fn from_extension_no_extension() {
783 assert_eq!(ImageFormat::from_extension("noext"), ImageFormat::Unknown(String::new()));
784 }
785
786 #[test]
787 fn from_extension_multi_dot() {
788 assert_eq!(ImageFormat::from_extension("multi.dot.png"), ImageFormat::Png);
789 }
790
791 #[test]
796 fn from_path_infers_format() {
797 let w = HwpUnit::from_mm(100.0).unwrap();
798 let h = HwpUnit::from_mm(75.0).unwrap();
799
800 let img = Image::from_path("photos/hero.png", w, h);
801 assert_eq!(img.format, ImageFormat::Png);
802 assert_eq!(img.path, "photos/hero.png");
803 assert_eq!(img.width, w);
804 assert_eq!(img.height, h);
805 assert!(img.caption.is_none());
806 }
807
808 #[test]
809 fn from_path_jpeg_uppercase() {
810 let w = HwpUnit::ZERO;
811 let h = HwpUnit::ZERO;
812 let img = Image::from_path("scan.JPG", w, h);
813 assert_eq!(img.format, ImageFormat::Jpeg);
814 }
815
816 #[test]
817 fn from_path_unknown_extension() {
818 let w = HwpUnit::ZERO;
819 let h = HwpUnit::ZERO;
820 let img = Image::from_path("diagram.svg", w, h);
821 assert_eq!(img.format, ImageFormat::Unknown("svg".to_string()));
822 }
823
824 #[test]
825 fn from_path_string_owned() {
826 let w = HwpUnit::ZERO;
827 let h = HwpUnit::ZERO;
828 let path = String::from("owned/path.bmp");
829 let img = Image::from_path(path, w, h);
830 assert_eq!(img.format, ImageFormat::Bmp);
831 assert_eq!(img.path, "owned/path.bmp");
832 }
833
834 #[test]
835 fn unknown_format_display_normalizes_to_lowercase() {
836 assert_eq!(ImageFormat::Unknown("SVG".to_string()).to_string(), "svg");
837 assert_eq!(ImageFormat::Unknown("Tiff".to_string()).to_string(), "tiff");
838 assert_eq!(ImageFormat::Unknown("webp".to_string()).to_string(), "webp");
839 }
840
841 #[test]
842 fn unknown_format_casing_inequality() {
843 let upper = ImageFormat::Unknown("SVG".to_string());
845 let lower = ImageFormat::Unknown("svg".to_string());
846 assert_ne!(upper, lower, "Different casing in Unknown produces inequality");
847 assert_eq!(upper.to_string(), lower.to_string());
849 }
850}