smriti/services/
image_io.rs1use std::path::Path;
16
17use image::DynamicImage;
18
19pub fn open_image(path: &Path) -> Result<DynamicImage, String> {
27 let ext = path
28 .extension()
29 .and_then(|e| e.to_str())
30 .map(|e| e.to_ascii_lowercase());
31
32 match ext.as_deref() {
33 Some("heic") | Some("heif") => decode_heic(path),
34 Some("raf") => Err(format!(
35 "RAF support is not available yet ({} is a Fujifilm RAW container, not TIFF-based)",
36 path.display()
37 )),
38 Some(e) if is_raw_extension(e) => decode_raw_via_preview(path),
39 _ => image::open(path).map_err(|e| e.to_string()),
40 }
41}
42
43fn is_raw_extension(e: &str) -> bool {
48 matches!(
49 e,
50 "nef" | "cr2" | "cr3" | "arw" | "dng" | "orf" | "rw2" | "pef" | "rwl" | "srw"
51 )
52}
53
54#[cfg(feature = "raw")]
55fn decode_raw_via_preview(path: &Path) -> Result<DynamicImage, String> {
56 let jpeg_bytes = crate::services::raw_preview::extract_largest_preview(path)
57 .map_err(|e| format!("RAW preview lookup failed for {}: {}", path.display(), e))?
58 .ok_or_else(|| {
59 format!(
60 "RAW has no embedded JPEG preview: {}. Full-RAW Bayer decode is not supported in this build.",
61 path.display()
62 )
63 })?;
64 image::load_from_memory(&jpeg_bytes).map_err(|e| {
65 format!(
66 "embedded JPEG preview in {} is invalid: {}",
67 path.display(),
68 e
69 )
70 })
71}
72
73#[cfg(not(feature = "raw"))]
74fn decode_raw_via_preview(path: &Path) -> Result<DynamicImage, String> {
75 Err(format!(
76 "RAW support not compiled in (rebuild with --features raw to decode {})",
77 path.display()
78 ))
79}
80
81#[cfg(feature = "heic")]
82fn decode_heic(path: &Path) -> Result<DynamicImage, String> {
83 use libheif_rs::{ColorSpace, HeifContext, LibHeif, RgbChroma};
84
85 let lib = LibHeif::new();
86 let ctx = HeifContext::read_from_file(
87 path.to_str()
88 .ok_or_else(|| format!("non-UTF8 HEIC path: {}", path.display()))?,
89 )
90 .map_err(|e| format!("HeifContext::read_from_file failed: {}", e))?;
91 let handle = ctx
92 .primary_image_handle()
93 .map_err(|e| format!("HEIC primary_image_handle: {}", e))?;
94 let img = lib
95 .decode(&handle, ColorSpace::Rgb(RgbChroma::Rgb), None)
96 .map_err(|e| format!("HEIC decode: {}", e))?;
97
98 let planes = img.planes();
99 let plane = planes
100 .interleaved
101 .ok_or_else(|| "HEIC: missing interleaved RGB plane".to_string())?;
102 let (w, h) = (plane.width as u32, plane.height as u32);
103 let stride = plane.stride;
104 let mut buf = Vec::with_capacity((w as usize) * (h as usize) * 3);
105 for y in 0..(h as usize) {
106 let row_start = y * stride;
107 let row_end = row_start + (w as usize) * 3;
108 if row_end > plane.data.len() {
109 return Err(format!(
110 "HEIC: RGB plane too short at row {} (need {}, have {})",
111 y,
112 row_end,
113 plane.data.len()
114 ));
115 }
116 buf.extend_from_slice(&plane.data[row_start..row_end]);
117 }
118 image::RgbImage::from_raw(w, h, buf)
119 .map(DynamicImage::ImageRgb8)
120 .ok_or_else(|| "HEIC: RGB buffer size mismatch".to_string())
121}
122
123#[cfg(not(feature = "heic"))]
124fn decode_heic(path: &Path) -> Result<DynamicImage, String> {
125 Err(format!(
126 "HEIC support not compiled in (rebuild with --features heic to decode {})",
127 path.display()
128 ))
129}