1use 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)]
56pub struct Image {
57 pub path: String,
59 pub width: HwpUnit,
61 pub height: HwpUnit,
63 pub format: ImageFormat,
65 pub caption: Option<Caption>,
67}
68
69impl Image {
70 pub fn new(
87 path: impl Into<String>,
88 width: HwpUnit,
89 height: HwpUnit,
90 format: ImageFormat,
91 ) -> Self {
92 Self { path: path.into(), width, height, format, caption: None }
93 }
94
95 pub fn from_path(path: impl Into<String>, width: HwpUnit, height: HwpUnit) -> Self {
119 let path: String = path.into();
120 let format = ImageFormat::from_extension(&path);
121 Self { path, width, height, format, caption: None }
122 }
123}
124
125impl std::fmt::Display for Image {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 write!(
128 f,
129 "Image({}, {:.1}mm x {:.1}mm)",
130 self.format,
131 self.width.to_mm(),
132 self.height.to_mm()
133 )
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
154#[non_exhaustive]
155pub enum ImageFormat {
156 Png,
158 Jpeg,
160 Gif,
162 Bmp,
164 Wmf,
166 Emf,
168 Unknown(String),
170}
171
172impl ImageFormat {
173 pub fn from_extension(path: &str) -> Self {
203 let ext_lower = path.rfind('.').map(|i| path[i + 1..].to_ascii_lowercase());
205 match ext_lower.as_deref() {
206 Some("png") => Self::Png,
207 Some("jpg" | "jpeg") => Self::Jpeg,
208 Some("gif") => Self::Gif,
209 Some("bmp") => Self::Bmp,
210 Some("wmf") => Self::Wmf,
211 Some("emf") => Self::Emf,
212 Some(ext) => Self::Unknown(ext.to_string()),
213 None => Self::Unknown(String::new()),
214 }
215 }
216}
217
218impl std::fmt::Display for ImageFormat {
219 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220 match self {
221 Self::Png => write!(f, "PNG"),
222 Self::Jpeg => write!(f, "JPEG"),
223 Self::Gif => write!(f, "GIF"),
224 Self::Bmp => write!(f, "BMP"),
225 Self::Wmf => write!(f, "WMF"),
226 Self::Emf => write!(f, "EMF"),
227 Self::Unknown(s) => {
228 let lower = s.to_ascii_lowercase();
229 write!(f, "{lower}")
230 }
231 }
232 }
233}
234
235#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
256pub struct ImageStore {
257 images: HashMap<String, Vec<u8>>,
258}
259
260impl ImageStore {
261 pub fn new() -> Self {
263 Self { images: HashMap::new() }
264 }
265
266 pub fn insert(&mut self, key: impl Into<String>, data: Vec<u8>) {
270 self.images.insert(key.into(), data);
271 }
272
273 pub fn get(&self, key: &str) -> Option<&[u8]> {
275 self.images.get(key).map(|v| v.as_slice())
276 }
277
278 pub fn len(&self) -> usize {
280 self.images.len()
281 }
282
283 pub fn is_empty(&self) -> bool {
285 self.images.is_empty()
286 }
287
288 pub fn iter(&self) -> impl Iterator<Item = (&str, &[u8])> {
290 self.images.iter().map(|(k, v)| (k.as_str(), v.as_slice()))
291 }
292}
293
294impl FromIterator<(String, Vec<u8>)> for ImageStore {
295 fn from_iter<I: IntoIterator<Item = (String, Vec<u8>)>>(iter: I) -> Self {
296 Self { images: iter.into_iter().collect() }
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 fn sample_image() -> Image {
305 Image::new(
306 "BinData/image1.png",
307 HwpUnit::from_mm(50.0).unwrap(),
308 HwpUnit::from_mm(30.0).unwrap(),
309 ImageFormat::Png,
310 )
311 }
312
313 #[test]
314 fn new_constructor() {
315 let img = sample_image();
316 assert_eq!(img.path, "BinData/image1.png");
317 assert_eq!(img.format, ImageFormat::Png);
318 }
319
320 #[test]
321 fn struct_literal_construction() {
322 let img = Image {
323 path: "test.jpeg".to_string(),
324 width: HwpUnit::from_mm(10.0).unwrap(),
325 height: HwpUnit::from_mm(10.0).unwrap(),
326 format: ImageFormat::Jpeg,
327 caption: None,
328 };
329 assert_eq!(img.format, ImageFormat::Jpeg);
330 }
331
332 #[test]
333 fn display_format() {
334 let img = sample_image();
335 let s = img.to_string();
336 assert!(s.contains("PNG"), "display: {s}");
337 assert!(s.contains("50.0"), "display: {s}");
338 assert!(s.contains("30.0"), "display: {s}");
339 }
340
341 #[test]
342 fn image_format_display() {
343 assert_eq!(ImageFormat::Png.to_string(), "PNG");
344 assert_eq!(ImageFormat::Jpeg.to_string(), "JPEG");
345 assert_eq!(ImageFormat::Gif.to_string(), "GIF");
346 assert_eq!(ImageFormat::Bmp.to_string(), "BMP");
347 assert_eq!(ImageFormat::Wmf.to_string(), "WMF");
348 assert_eq!(ImageFormat::Emf.to_string(), "EMF");
349 assert_eq!(ImageFormat::Unknown("TIFF".to_string()).to_string(), "tiff");
350 }
351
352 #[test]
353 fn equality() {
354 let a = sample_image();
355 let b = sample_image();
356 assert_eq!(a, b);
357 }
358
359 #[test]
360 fn inequality_on_different_paths() {
361 let a = sample_image();
362 let mut b = sample_image();
363 b.path = "other.png".to_string();
364 assert_ne!(a, b);
365 }
366
367 #[test]
368 fn clone_independence() {
369 let img = sample_image();
370 let mut cloned = img.clone();
371 cloned.path = "modified.png".to_string();
372 assert_eq!(img.path, "BinData/image1.png");
373 }
374
375 #[test]
376 fn serde_roundtrip() {
377 let img = sample_image();
378 let json = serde_json::to_string(&img).unwrap();
379 let back: Image = serde_json::from_str(&json).unwrap();
380 assert_eq!(img, back);
381 }
382
383 #[test]
384 fn serde_unknown_format_roundtrip() {
385 let img = Image::new(
386 "test.svg",
387 HwpUnit::from_mm(10.0).unwrap(),
388 HwpUnit::from_mm(10.0).unwrap(),
389 ImageFormat::Unknown("SVG".to_string()),
390 );
391 let json = serde_json::to_string(&img).unwrap();
392 let back: Image = serde_json::from_str(&json).unwrap();
393 assert_eq!(img, back);
394 }
395
396 #[test]
397 fn image_format_hash() {
398 use std::collections::HashSet;
399 let mut set = HashSet::new();
400 set.insert(ImageFormat::Png);
401 set.insert(ImageFormat::Jpeg);
402 set.insert(ImageFormat::Png);
403 assert_eq!(set.len(), 2);
404 }
405
406 #[test]
407 fn from_string_path() {
408 let path = String::from("dynamic/path.bmp");
409 let img = Image::new(path, HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Bmp);
410 assert_eq!(img.path, "dynamic/path.bmp");
411 }
412
413 #[test]
418 fn image_store_new_is_empty() {
419 let store = ImageStore::new();
420 assert!(store.is_empty());
421 assert_eq!(store.len(), 0);
422 }
423
424 #[test]
425 fn image_store_insert_and_get() {
426 let mut store = ImageStore::new();
427 store.insert("logo.png", vec![0x89, 0x50, 0x4E, 0x47]);
428 assert_eq!(store.len(), 1);
429 assert!(!store.is_empty());
430 assert_eq!(store.get("logo.png"), Some(&[0x89, 0x50, 0x4E, 0x47][..]));
431 }
432
433 #[test]
434 fn image_store_get_missing() {
435 let store = ImageStore::new();
436 assert!(store.get("nonexistent.png").is_none());
437 }
438
439 #[test]
440 fn image_store_insert_replaces() {
441 let mut store = ImageStore::new();
442 store.insert("img.png", vec![1, 2, 3]);
443 store.insert("img.png", vec![4, 5, 6]);
444 assert_eq!(store.len(), 1);
445 assert_eq!(store.get("img.png"), Some(&[4, 5, 6][..]));
446 }
447
448 #[test]
449 fn image_store_multiple_images() {
450 let mut store = ImageStore::new();
451 store.insert("a.png", vec![1]);
452 store.insert("b.jpg", vec![2]);
453 store.insert("c.gif", vec![3]);
454 assert_eq!(store.len(), 3);
455 }
456
457 #[test]
458 fn image_store_iter() {
459 let mut store = ImageStore::new();
460 store.insert("a.png", vec![1]);
461 store.insert("b.jpg", vec![2]);
462 let pairs: Vec<_> = store.iter().collect();
463 assert_eq!(pairs.len(), 2);
464 }
465
466 #[test]
467 fn image_store_from_iterator() {
468 let items = vec![("a.png".to_string(), vec![1, 2]), ("b.jpg".to_string(), vec![3, 4])];
469 let store: ImageStore = items.into_iter().collect();
470 assert_eq!(store.len(), 2);
471 assert_eq!(store.get("a.png"), Some(&[1, 2][..]));
472 }
473
474 #[test]
475 fn image_store_default() {
476 let store = ImageStore::default();
477 assert!(store.is_empty());
478 }
479
480 #[test]
481 fn image_store_clone_independence() {
482 let mut store = ImageStore::new();
483 store.insert("img.png", vec![1, 2, 3]);
484 let mut cloned = store.clone();
485 cloned.insert("other.png", vec![4, 5]);
486 assert_eq!(store.len(), 1);
487 assert_eq!(cloned.len(), 2);
488 }
489
490 #[test]
491 fn image_store_equality() {
492 let mut a = ImageStore::new();
493 a.insert("img.png", vec![1, 2, 3]);
494 let mut b = ImageStore::new();
495 b.insert("img.png", vec![1, 2, 3]);
496 assert_eq!(a, b);
497 }
498
499 #[test]
500 fn image_store_serde_roundtrip() {
501 let mut store = ImageStore::new();
502 store.insert("logo.png", vec![0x89, 0x50]);
503 let json = serde_json::to_string(&store).unwrap();
504 let back: ImageStore = serde_json::from_str(&json).unwrap();
505 assert_eq!(store, back);
506 }
507
508 #[test]
509 fn image_store_string_key() {
510 let mut store = ImageStore::new();
511 let key = String::from("dynamic/path.png");
512 store.insert(key, vec![42]);
513 assert!(store.get("dynamic/path.png").is_some());
514 }
515
516 #[test]
521 fn from_extension_png() {
522 assert_eq!(ImageFormat::from_extension("photo.png"), ImageFormat::Png);
523 }
524
525 #[test]
526 fn from_extension_jpg_uppercase() {
527 assert_eq!(ImageFormat::from_extension("image.JPG"), ImageFormat::Jpeg);
528 }
529
530 #[test]
531 fn from_extension_jpeg() {
532 assert_eq!(ImageFormat::from_extension("file.jpeg"), ImageFormat::Jpeg);
533 }
534
535 #[test]
536 fn from_extension_gif() {
537 assert_eq!(ImageFormat::from_extension("doc.gif"), ImageFormat::Gif);
538 }
539
540 #[test]
541 fn from_extension_bmp() {
542 assert_eq!(ImageFormat::from_extension("img.bmp"), ImageFormat::Bmp);
543 }
544
545 #[test]
546 fn from_extension_wmf() {
547 assert_eq!(ImageFormat::from_extension("chart.wmf"), ImageFormat::Wmf);
548 }
549
550 #[test]
551 fn from_extension_emf() {
552 assert_eq!(ImageFormat::from_extension("dia.emf"), ImageFormat::Emf);
553 }
554
555 #[test]
556 fn from_extension_unknown() {
557 assert_eq!(
558 ImageFormat::from_extension("file.xyz"),
559 ImageFormat::Unknown("xyz".to_string()),
560 );
561 }
562
563 #[test]
564 fn from_extension_no_extension() {
565 assert_eq!(ImageFormat::from_extension("noext"), ImageFormat::Unknown(String::new()));
566 }
567
568 #[test]
569 fn from_extension_multi_dot() {
570 assert_eq!(ImageFormat::from_extension("multi.dot.png"), ImageFormat::Png);
571 }
572
573 #[test]
578 fn from_path_infers_format() {
579 let w = HwpUnit::from_mm(100.0).unwrap();
580 let h = HwpUnit::from_mm(75.0).unwrap();
581
582 let img = Image::from_path("photos/hero.png", w, h);
583 assert_eq!(img.format, ImageFormat::Png);
584 assert_eq!(img.path, "photos/hero.png");
585 assert_eq!(img.width, w);
586 assert_eq!(img.height, h);
587 assert!(img.caption.is_none());
588 }
589
590 #[test]
591 fn from_path_jpeg_uppercase() {
592 let w = HwpUnit::ZERO;
593 let h = HwpUnit::ZERO;
594 let img = Image::from_path("scan.JPG", w, h);
595 assert_eq!(img.format, ImageFormat::Jpeg);
596 }
597
598 #[test]
599 fn from_path_unknown_extension() {
600 let w = HwpUnit::ZERO;
601 let h = HwpUnit::ZERO;
602 let img = Image::from_path("diagram.svg", w, h);
603 assert_eq!(img.format, ImageFormat::Unknown("svg".to_string()));
604 }
605
606 #[test]
607 fn from_path_string_owned() {
608 let w = HwpUnit::ZERO;
609 let h = HwpUnit::ZERO;
610 let path = String::from("owned/path.bmp");
611 let img = Image::from_path(path, w, h);
612 assert_eq!(img.format, ImageFormat::Bmp);
613 assert_eq!(img.path, "owned/path.bmp");
614 }
615
616 #[test]
617 fn unknown_format_display_normalizes_to_lowercase() {
618 assert_eq!(ImageFormat::Unknown("SVG".to_string()).to_string(), "svg");
619 assert_eq!(ImageFormat::Unknown("Tiff".to_string()).to_string(), "tiff");
620 assert_eq!(ImageFormat::Unknown("webp".to_string()).to_string(), "webp");
621 }
622
623 #[test]
624 fn unknown_format_casing_inequality() {
625 let upper = ImageFormat::Unknown("SVG".to_string());
627 let lower = ImageFormat::Unknown("svg".to_string());
628 assert_ne!(upper, lower, "Different casing in Unknown produces inequality");
629 assert_eq!(upper.to_string(), lower.to_string());
631 }
632}