Skip to main content

rawshift_image/formats/
heic.rs

1//! HEIC/HEIF format support — public API.
2//!
3//! [`HeicFile`] is the entry point for HEIC files. It decodes the primary image
4//! and gives full access to auxiliary content (thumbnails, depth maps, HDR gain
5//! maps, alpha/auxiliary images) and to embedded EXIF/XMP/ICC metadata.
6//!
7//! HEIC is also reachable through the generic standard-format API
8//! ([`decode_standard_image`](crate::formats::decode_standard_image)); use
9//! [`HeicFile`] when you need the auxiliary images or richer metadata.
10
11use crate::codecs::heic;
12use crate::core::image::RgbImage;
13use crate::core::metadata::{ImageMetadata, MetadataKey, MetadataNamespace, MetadataValue};
14use crate::error::{FormatError, RawError, RawResult};
15use crate::metadata::exif::ExifParser;
16
17pub use crate::codecs::heic::HeicAuxKind;
18
19/// Map a libheif wrapper error string into a [`RawError`].
20fn heic_err(message: String) -> RawError {
21    RawError::Format(FormatError::ImageDecode {
22        format: "HEIC",
23        message,
24    })
25}
26
27/// Descriptor of one auxiliary image inside a [`HeicFile`].
28///
29/// Decode it with [`HeicFile::decode_aux`].
30#[derive(Debug, Clone)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub struct HeicAuxImage {
33    /// What kind of auxiliary image this is.
34    pub kind: HeicAuxKind,
35    /// Width in pixels.
36    pub width: u32,
37    /// Height in pixels.
38    pub height: u32,
39    /// Auxiliary type URN string, when the container provides one.
40    pub aux_type: Option<String>,
41    /// Opaque libheif item id used to decode this image.
42    pub(crate) item_id: u32,
43}
44
45/// A parsed HEIC/HEIF file.
46///
47/// Construct with [`HeicFile::open`]. Opening only parses the container (cheap);
48/// the CPU-heavy HEVC decode happens in [`decode_primary`](Self::decode_primary)
49/// and [`decode_aux`](Self::decode_aux).
50pub struct HeicFile {
51    data: Vec<u8>,
52    aux: Vec<HeicAuxImage>,
53}
54
55impl std::fmt::Debug for HeicFile {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        f.debug_struct("HeicFile")
58            .field("data_len", &self.data.len())
59            .field("aux", &self.aux)
60            .finish()
61    }
62}
63
64impl HeicFile {
65    /// Parse a HEIC file from its raw bytes.
66    ///
67    /// Only the container is parsed here, so this is cheap. The auxiliary-image
68    /// table is populated up front; pixel decoding is deferred.
69    ///
70    /// # Errors
71    /// Returns [`RawError::Format`] if `data` is not a readable HEIC file.
72    pub fn open(data: Vec<u8>) -> RawResult<Self> {
73        let infos = heic::list_aux_images(&data).map_err(heic_err)?;
74        let aux = infos
75            .into_iter()
76            .map(|i| HeicAuxImage {
77                kind: i.kind,
78                width: i.width,
79                height: i.height,
80                aux_type: i.aux_type,
81                item_id: i.item_id,
82            })
83            .collect();
84        Ok(Self { data, aux })
85    }
86
87    /// Decode the primary image to a 16-bit RGB image.
88    ///
89    /// Geometric transforms (`irot`/`imir`) and grid (tiled) stitching are
90    /// already applied; HDR (10/12-bit) sources are scaled to the full 16-bit
91    /// range.
92    pub fn decode_primary(&self) -> RawResult<RgbImage> {
93        let decoded = heic::decode_primary(&self.data).map_err(heic_err)?;
94        Ok(RgbImage::new(decoded.width, decoded.height, decoded.rgb))
95    }
96
97    /// Extract embedded EXIF/XMP/ICC and full typed metadata.
98    pub fn metadata(&self) -> ImageMetadata {
99        read_heic_metadata(&self.data)
100    }
101
102    /// All auxiliary images (thumbnails, depth maps, HDR gain maps, auxiliary).
103    pub fn aux_images(&self) -> &[HeicAuxImage] {
104        &self.aux
105    }
106
107    /// Decode a specific auxiliary image to a 16-bit RGB image.
108    ///
109    /// Single-channel sources (depth maps, gain maps) are returned as
110    /// grayscale-expanded RGB.
111    pub fn decode_aux(&self, aux: &HeicAuxImage) -> RawResult<RgbImage> {
112        let decoded = heic::decode_aux(&self.data, aux.item_id).map_err(heic_err)?;
113        Ok(RgbImage::new(decoded.width, decoded.height, decoded.rgb))
114    }
115
116    /// Decode the embedded thumbnail, if the file carries one.
117    ///
118    /// Returns `Ok(None)` when there is no thumbnail. (libheif does not expose
119    /// the thumbnail's coded bitstream, so a decoded image is returned.)
120    pub fn thumbnail(&self) -> RawResult<Option<RgbImage>> {
121        match self.aux.iter().find(|a| a.kind == HeicAuxKind::Thumbnail) {
122            Some(thumb) => Ok(Some(self.decode_aux(thumb)?)),
123            None => Ok(None),
124        }
125    }
126}
127
128/// Extract unified metadata from HEIC file bytes.
129///
130/// Reads the embedded EXIF, XMP, and ICC profile and maps them onto
131/// [`ImageMetadata`]. Returns a default (empty) value when the file carries no
132/// metadata or cannot be parsed. Used by both [`HeicFile::metadata`] and
133/// [`read_standard_image_metadata`](crate::formats::read_standard_image_metadata).
134pub fn read_heic_metadata(data: &[u8]) -> ImageMetadata {
135    use little_exif::filetype::FileExtension;
136
137    let blobs = match heic::extract_metadata_blobs(data) {
138        Ok(b) => b,
139        Err(_) => return ImageMetadata::default(),
140    };
141
142    // Parse the EXIF block (a raw TIFF stream) into typed + generic fields.
143    let mut md = match blobs.exif {
144        Some(ref exif) => ExifParser::parse_from_bytes(exif, FileExtension::TIFF),
145        None => ImageMetadata::default(),
146    };
147
148    md.exif_raw = blobs.exif;
149    md.xmp = blobs.xmp;
150    md.icc_profile = blobs.icc;
151    if md.image.bit_depth == 0 {
152        md.image.bit_depth = blobs.bit_depth;
153    }
154
155    // Record HEIC container facts in the generic table.
156    md.insert(
157        MetadataKey::new(MetadataNamespace::Heic, "bit_depth"),
158        MetadataValue::U64(blobs.bit_depth as u64),
159    );
160    md.insert(
161        MetadataKey::new(MetadataNamespace::Heic, "has_alpha"),
162        MetadataValue::U64(blobs.has_alpha as u64),
163    );
164    md.insert(
165        MetadataKey::new(MetadataNamespace::Heic, "width"),
166        MetadataValue::U64(blobs.width as u64),
167    );
168    md.insert(
169        MetadataKey::new(MetadataNamespace::Heic, "height"),
170        MetadataValue::U64(blobs.height as u64),
171    );
172
173    md
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn open_rejects_junk() {
182        let junk = vec![0u8; 64];
183        assert!(HeicFile::open(junk).is_err());
184    }
185
186    #[test]
187    fn read_metadata_junk_returns_default() {
188        let junk = vec![0u8; 64];
189        assert_eq!(read_heic_metadata(&junk), ImageMetadata::default());
190    }
191
192    /// Exercise the full public API against libheif's bundled sample file when
193    /// it is available on this machine. Skips gracefully otherwise.
194    #[test]
195    fn open_and_decode_homebrew_sample() {
196        let candidates = [
197            "/opt/homebrew/share/libheif/example.heic",
198            "/usr/local/share/libheif/example.heic",
199        ];
200        let Some(path) = candidates.iter().find(|p| std::path::Path::new(p).exists()) else {
201            eprintln!("skipping: no libheif sample HEIC found");
202            return;
203        };
204        let data = std::fs::read(path).expect("read sample heic");
205
206        let file = HeicFile::open(data).expect("open sample heic");
207        let primary = file.decode_primary().expect("decode primary");
208        assert!(primary.width() > 0 && primary.height() > 0);
209        assert_eq!(
210            primary.data.len(),
211            primary.width() as usize * primary.height() as usize * 3
212        );
213
214        // Every enumerated auxiliary image must decode.
215        for aux in file.aux_images() {
216            let img = file.decode_aux(aux).expect("decode aux image");
217            assert!(img.width() > 0 && img.height() > 0);
218        }
219
220        // Metadata extraction must not panic.
221        let _ = file.metadata();
222    }
223}