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
253impl std::fmt::Display for ImageFormat {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 match self {
256 Self::Png => write!(f, "PNG"),
257 Self::Jpeg => write!(f, "JPEG"),
258 Self::Gif => write!(f, "GIF"),
259 Self::Bmp => write!(f, "BMP"),
260 Self::Wmf => write!(f, "WMF"),
261 Self::Emf => write!(f, "EMF"),
262 Self::Unknown(s) => {
263 let lower = s.to_ascii_lowercase();
264 write!(f, "{lower}")
265 }
266 }
267 }
268}
269
270#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
291pub struct ImageStore {
292 images: HashMap<String, Vec<u8>>,
293}
294
295impl ImageStore {
296 pub fn new() -> Self {
298 Self { images: HashMap::new() }
299 }
300
301 pub fn insert(&mut self, key: impl Into<String>, data: Vec<u8>) {
305 self.images.insert(key.into(), data);
306 }
307
308 pub fn get(&self, key: &str) -> Option<&[u8]> {
310 self.images.get(key).map(|v| v.as_slice())
311 }
312
313 pub fn len(&self) -> usize {
315 self.images.len()
316 }
317
318 pub fn is_empty(&self) -> bool {
320 self.images.is_empty()
321 }
322
323 pub fn iter(&self) -> impl Iterator<Item = (&str, &[u8])> {
325 self.images.iter().map(|(k, v)| (k.as_str(), v.as_slice()))
326 }
327}
328
329impl FromIterator<(String, Vec<u8>)> for ImageStore {
330 fn from_iter<I: IntoIterator<Item = (String, Vec<u8>)>>(iter: I) -> Self {
331 Self { images: iter.into_iter().collect() }
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 fn sample_image() -> Image {
340 Image::new(
341 "BinData/image1.png",
342 HwpUnit::from_mm(50.0).unwrap(),
343 HwpUnit::from_mm(30.0).unwrap(),
344 ImageFormat::Png,
345 )
346 }
347
348 #[test]
349 fn new_constructor() {
350 let img = sample_image();
351 assert_eq!(img.path, "BinData/image1.png");
352 assert_eq!(img.format, ImageFormat::Png);
353 }
354
355 #[test]
356 fn from_path_constructor() {
357 let img = Image::from_path(
358 "test.jpeg",
359 HwpUnit::from_mm(10.0).unwrap(),
360 HwpUnit::from_mm(10.0).unwrap(),
361 );
362 assert_eq!(img.format, ImageFormat::Jpeg);
363 }
364
365 #[test]
366 fn builder_attaches_caption() {
367 let img = sample_image().with_caption(Caption::default());
368 assert!(img.caption.is_some());
369 }
370
371 #[test]
372 fn display_format() {
373 let img = sample_image();
374 let s = img.to_string();
375 assert!(s.contains("PNG"), "display: {s}");
376 assert!(s.contains("50.0"), "display: {s}");
377 assert!(s.contains("30.0"), "display: {s}");
378 }
379
380 #[test]
381 fn image_format_display() {
382 assert_eq!(ImageFormat::Png.to_string(), "PNG");
383 assert_eq!(ImageFormat::Jpeg.to_string(), "JPEG");
384 assert_eq!(ImageFormat::Gif.to_string(), "GIF");
385 assert_eq!(ImageFormat::Bmp.to_string(), "BMP");
386 assert_eq!(ImageFormat::Wmf.to_string(), "WMF");
387 assert_eq!(ImageFormat::Emf.to_string(), "EMF");
388 assert_eq!(ImageFormat::Unknown("TIFF".to_string()).to_string(), "tiff");
389 }
390
391 #[test]
392 fn equality() {
393 let a = sample_image();
394 let b = sample_image();
395 assert_eq!(a, b);
396 }
397
398 #[test]
399 fn inequality_on_different_paths() {
400 let a = sample_image();
401 let mut b = sample_image();
402 b.path = "other.png".to_string();
403 assert_ne!(a, b);
404 }
405
406 #[test]
407 fn clone_independence() {
408 let img = sample_image();
409 let mut cloned = img.clone();
410 cloned.path = "modified.png".to_string();
411 assert_eq!(img.path, "BinData/image1.png");
412 }
413
414 #[test]
415 fn serde_roundtrip() {
416 let img = sample_image();
417 let json = serde_json::to_string(&img).unwrap();
418 let back: Image = serde_json::from_str(&json).unwrap();
419 assert_eq!(img, back);
420 }
421
422 #[test]
423 fn placement_roundtrip() {
424 use crate::placement::{ObjectPlacement, ObjectRelativeTo, ObjectTextFlow, ObjectTextWrap};
425 let img = sample_image().with_placement(ObjectPlacement {
426 text_wrap: ObjectTextWrap::Square,
427 text_flow: ObjectTextFlow::RightOnly,
428 treat_as_char: false,
429 flow_with_text: true,
430 allow_overlap: true,
431 vert_rel_to: ObjectRelativeTo::Paper,
432 horz_rel_to: ObjectRelativeTo::Page,
433 vert_offset: HwpUnit::new(1200).unwrap(),
434 horz_offset: HwpUnit::new(3400).unwrap(),
435 });
436 let json = serde_json::to_string(&img).unwrap();
437 let back: Image = serde_json::from_str(&json).unwrap();
438 assert_eq!(img, back);
439 }
440
441 #[test]
442 fn serde_unknown_format_roundtrip() {
443 let img = Image::new(
444 "test.svg",
445 HwpUnit::from_mm(10.0).unwrap(),
446 HwpUnit::from_mm(10.0).unwrap(),
447 ImageFormat::Unknown("SVG".to_string()),
448 );
449 let json = serde_json::to_string(&img).unwrap();
450 let back: Image = serde_json::from_str(&json).unwrap();
451 assert_eq!(img, back);
452 }
453
454 #[test]
455 fn image_format_hash() {
456 use std::collections::HashSet;
457 let mut set = HashSet::new();
458 set.insert(ImageFormat::Png);
459 set.insert(ImageFormat::Jpeg);
460 set.insert(ImageFormat::Png);
461 assert_eq!(set.len(), 2);
462 }
463
464 #[test]
465 fn from_string_path() {
466 let path = String::from("dynamic/path.bmp");
467 let img = Image::new(path, HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Bmp);
468 assert_eq!(img.path, "dynamic/path.bmp");
469 }
470
471 #[test]
476 fn image_store_new_is_empty() {
477 let store = ImageStore::new();
478 assert!(store.is_empty());
479 assert_eq!(store.len(), 0);
480 }
481
482 #[test]
483 fn image_store_insert_and_get() {
484 let mut store = ImageStore::new();
485 store.insert("logo.png", vec![0x89, 0x50, 0x4E, 0x47]);
486 assert_eq!(store.len(), 1);
487 assert!(!store.is_empty());
488 assert_eq!(store.get("logo.png"), Some(&[0x89, 0x50, 0x4E, 0x47][..]));
489 }
490
491 #[test]
492 fn image_store_get_missing() {
493 let store = ImageStore::new();
494 assert!(store.get("nonexistent.png").is_none());
495 }
496
497 #[test]
498 fn image_store_insert_replaces() {
499 let mut store = ImageStore::new();
500 store.insert("img.png", vec![1, 2, 3]);
501 store.insert("img.png", vec![4, 5, 6]);
502 assert_eq!(store.len(), 1);
503 assert_eq!(store.get("img.png"), Some(&[4, 5, 6][..]));
504 }
505
506 #[test]
507 fn image_store_multiple_images() {
508 let mut store = ImageStore::new();
509 store.insert("a.png", vec![1]);
510 store.insert("b.jpg", vec![2]);
511 store.insert("c.gif", vec![3]);
512 assert_eq!(store.len(), 3);
513 }
514
515 #[test]
516 fn image_store_iter() {
517 let mut store = ImageStore::new();
518 store.insert("a.png", vec![1]);
519 store.insert("b.jpg", vec![2]);
520 let pairs: Vec<_> = store.iter().collect();
521 assert_eq!(pairs.len(), 2);
522 }
523
524 #[test]
525 fn image_store_from_iterator() {
526 let items = vec![("a.png".to_string(), vec![1, 2]), ("b.jpg".to_string(), vec![3, 4])];
527 let store: ImageStore = items.into_iter().collect();
528 assert_eq!(store.len(), 2);
529 assert_eq!(store.get("a.png"), Some(&[1, 2][..]));
530 }
531
532 #[test]
533 fn image_store_default() {
534 let store = ImageStore::default();
535 assert!(store.is_empty());
536 }
537
538 #[test]
539 fn image_store_clone_independence() {
540 let mut store = ImageStore::new();
541 store.insert("img.png", vec![1, 2, 3]);
542 let mut cloned = store.clone();
543 cloned.insert("other.png", vec![4, 5]);
544 assert_eq!(store.len(), 1);
545 assert_eq!(cloned.len(), 2);
546 }
547
548 #[test]
549 fn image_store_equality() {
550 let mut a = ImageStore::new();
551 a.insert("img.png", vec![1, 2, 3]);
552 let mut b = ImageStore::new();
553 b.insert("img.png", vec![1, 2, 3]);
554 assert_eq!(a, b);
555 }
556
557 #[test]
558 fn image_store_serde_roundtrip() {
559 let mut store = ImageStore::new();
560 store.insert("logo.png", vec![0x89, 0x50]);
561 let json = serde_json::to_string(&store).unwrap();
562 let back: ImageStore = serde_json::from_str(&json).unwrap();
563 assert_eq!(store, back);
564 }
565
566 #[test]
567 fn image_store_string_key() {
568 let mut store = ImageStore::new();
569 let key = String::from("dynamic/path.png");
570 store.insert(key, vec![42]);
571 assert!(store.get("dynamic/path.png").is_some());
572 }
573
574 #[test]
579 fn from_extension_png() {
580 assert_eq!(ImageFormat::from_extension("photo.png"), ImageFormat::Png);
581 }
582
583 #[test]
584 fn from_extension_jpg_uppercase() {
585 assert_eq!(ImageFormat::from_extension("image.JPG"), ImageFormat::Jpeg);
586 }
587
588 #[test]
589 fn from_extension_jpeg() {
590 assert_eq!(ImageFormat::from_extension("file.jpeg"), ImageFormat::Jpeg);
591 }
592
593 #[test]
594 fn from_extension_gif() {
595 assert_eq!(ImageFormat::from_extension("doc.gif"), ImageFormat::Gif);
596 }
597
598 #[test]
599 fn from_extension_bmp() {
600 assert_eq!(ImageFormat::from_extension("img.bmp"), ImageFormat::Bmp);
601 }
602
603 #[test]
604 fn from_extension_wmf() {
605 assert_eq!(ImageFormat::from_extension("chart.wmf"), ImageFormat::Wmf);
606 }
607
608 #[test]
609 fn from_extension_emf() {
610 assert_eq!(ImageFormat::from_extension("dia.emf"), ImageFormat::Emf);
611 }
612
613 #[test]
614 fn from_extension_unknown() {
615 assert_eq!(
616 ImageFormat::from_extension("file.xyz"),
617 ImageFormat::Unknown("xyz".to_string()),
618 );
619 }
620
621 #[test]
622 fn from_extension_no_extension() {
623 assert_eq!(ImageFormat::from_extension("noext"), ImageFormat::Unknown(String::new()));
624 }
625
626 #[test]
627 fn from_extension_multi_dot() {
628 assert_eq!(ImageFormat::from_extension("multi.dot.png"), ImageFormat::Png);
629 }
630
631 #[test]
636 fn from_path_infers_format() {
637 let w = HwpUnit::from_mm(100.0).unwrap();
638 let h = HwpUnit::from_mm(75.0).unwrap();
639
640 let img = Image::from_path("photos/hero.png", w, h);
641 assert_eq!(img.format, ImageFormat::Png);
642 assert_eq!(img.path, "photos/hero.png");
643 assert_eq!(img.width, w);
644 assert_eq!(img.height, h);
645 assert!(img.caption.is_none());
646 }
647
648 #[test]
649 fn from_path_jpeg_uppercase() {
650 let w = HwpUnit::ZERO;
651 let h = HwpUnit::ZERO;
652 let img = Image::from_path("scan.JPG", w, h);
653 assert_eq!(img.format, ImageFormat::Jpeg);
654 }
655
656 #[test]
657 fn from_path_unknown_extension() {
658 let w = HwpUnit::ZERO;
659 let h = HwpUnit::ZERO;
660 let img = Image::from_path("diagram.svg", w, h);
661 assert_eq!(img.format, ImageFormat::Unknown("svg".to_string()));
662 }
663
664 #[test]
665 fn from_path_string_owned() {
666 let w = HwpUnit::ZERO;
667 let h = HwpUnit::ZERO;
668 let path = String::from("owned/path.bmp");
669 let img = Image::from_path(path, w, h);
670 assert_eq!(img.format, ImageFormat::Bmp);
671 assert_eq!(img.path, "owned/path.bmp");
672 }
673
674 #[test]
675 fn unknown_format_display_normalizes_to_lowercase() {
676 assert_eq!(ImageFormat::Unknown("SVG".to_string()).to_string(), "svg");
677 assert_eq!(ImageFormat::Unknown("Tiff".to_string()).to_string(), "tiff");
678 assert_eq!(ImageFormat::Unknown("webp".to_string()).to_string(), "webp");
679 }
680
681 #[test]
682 fn unknown_format_casing_inequality() {
683 let upper = ImageFormat::Unknown("SVG".to_string());
685 let lower = ImageFormat::Unknown("svg".to_string());
686 assert_ne!(upper, lower, "Different casing in Unknown produces inequality");
687 assert_eq!(upper.to_string(), lower.to_string());
689 }
690}