rawshift_image/formats/
heic.rs1use 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
19fn heic_err(message: String) -> RawError {
21 RawError::Format(FormatError::ImageDecode {
22 format: "HEIC",
23 message,
24 })
25}
26
27#[derive(Debug, Clone)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub struct HeicAuxImage {
33 pub kind: HeicAuxKind,
35 pub width: u32,
37 pub height: u32,
39 pub aux_type: Option<String>,
41 pub(crate) item_id: u32,
43}
44
45pub 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 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 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 pub fn metadata(&self) -> ImageMetadata {
99 read_heic_metadata(&self.data)
100 }
101
102 pub fn aux_images(&self) -> &[HeicAuxImage] {
104 &self.aux
105 }
106
107 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 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
128pub 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 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 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 #[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 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 let _ = file.metadata();
222 }
223}