Skip to main content

smriti/services/
image_io.rs

1//! Format-aware image opening.
2//!
3//! Wraps `image::open` so:
4//!   - HEIC/HEIF (every modern iPhone photo) decodes through libheif
5//!     when the `heic` Cargo feature is enabled, fails clearly
6//!     otherwise.
7//!   - TIFF-based RAW (NEF / CR2 / CR3 / ARW / DNG / ORF / RW2 /
8//!     PEF / RWL / SRW) decodes via the camera's embedded full-res
9//!     JPEG preview when the `raw` feature is on (default).
10//!   - Everything else falls through to the `image` crate.
11//!
12//! Call this from any code path that previously did `image::open(...)`
13//! — thumbnail generation, face detection, brightness sampling.
14
15use std::path::Path;
16
17use image::DynamicImage;
18
19/// Open and decode an image. Returns a fully-decoded `DynamicImage`
20/// regardless of the on-disk format. HEIC/HEIF route through libheif
21/// when compiled in; RAW files route through `raw_preview` to extract
22/// the embedded full-res JPEG preview (the camera's intended
23/// rendering, much better than software-debayered output without
24/// proprietary colour profiles); everything else uses the `image`
25/// crate's native decoders.
26pub 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
43/// Recognises every TIFF-based RAW we route through the embedded-JPEG
44/// path. Kept in sync with `scanner::SUPPORTED_EXTENSIONS` and
45/// `reindexer`'s extension allowlist — adding a new RAW extension
46/// means touching this list too.
47fn 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}