use crate::codecs::heic;
use crate::core::image::RgbImage;
use crate::core::metadata::{ImageMetadata, MetadataKey, MetadataNamespace, MetadataValue};
use crate::error::{FormatError, RawError, RawResult};
use crate::metadata::exif::ExifParser;
pub use crate::codecs::heic::HeicAuxKind;
fn heic_err(message: String) -> RawError {
RawError::Format(FormatError::ImageDecode {
format: "HEIC",
message,
})
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct HeicAuxImage {
pub kind: HeicAuxKind,
pub width: u32,
pub height: u32,
pub aux_type: Option<String>,
pub(crate) item_id: u32,
}
pub struct HeicFile {
data: Vec<u8>,
aux: Vec<HeicAuxImage>,
}
impl std::fmt::Debug for HeicFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HeicFile")
.field("data_len", &self.data.len())
.field("aux", &self.aux)
.finish()
}
}
impl HeicFile {
pub fn open(data: Vec<u8>) -> RawResult<Self> {
let infos = heic::list_aux_images(&data).map_err(heic_err)?;
let aux = infos
.into_iter()
.map(|i| HeicAuxImage {
kind: i.kind,
width: i.width,
height: i.height,
aux_type: i.aux_type,
item_id: i.item_id,
})
.collect();
Ok(Self { data, aux })
}
pub fn decode_primary(&self) -> RawResult<RgbImage> {
let decoded = heic::decode_primary(&self.data).map_err(heic_err)?;
Ok(RgbImage::new(decoded.width, decoded.height, decoded.rgb))
}
pub fn metadata(&self) -> ImageMetadata {
read_heic_metadata(&self.data)
}
pub fn aux_images(&self) -> &[HeicAuxImage] {
&self.aux
}
pub fn decode_aux(&self, aux: &HeicAuxImage) -> RawResult<RgbImage> {
let decoded = heic::decode_aux(&self.data, aux.item_id).map_err(heic_err)?;
Ok(RgbImage::new(decoded.width, decoded.height, decoded.rgb))
}
pub fn thumbnail(&self) -> RawResult<Option<RgbImage>> {
match self.aux.iter().find(|a| a.kind == HeicAuxKind::Thumbnail) {
Some(thumb) => Ok(Some(self.decode_aux(thumb)?)),
None => Ok(None),
}
}
}
pub fn read_heic_metadata(data: &[u8]) -> ImageMetadata {
use little_exif::filetype::FileExtension;
let blobs = match heic::extract_metadata_blobs(data) {
Ok(b) => b,
Err(_) => return ImageMetadata::default(),
};
let mut md = match blobs.exif {
Some(ref exif) => ExifParser::parse_from_bytes(exif, FileExtension::TIFF),
None => ImageMetadata::default(),
};
md.exif_raw = blobs.exif;
md.xmp = blobs.xmp;
md.icc_profile = blobs.icc;
if md.image.bit_depth == 0 {
md.image.bit_depth = blobs.bit_depth;
}
md.insert(
MetadataKey::new(MetadataNamespace::Heic, "bit_depth"),
MetadataValue::U64(blobs.bit_depth as u64),
);
md.insert(
MetadataKey::new(MetadataNamespace::Heic, "has_alpha"),
MetadataValue::U64(blobs.has_alpha as u64),
);
md.insert(
MetadataKey::new(MetadataNamespace::Heic, "width"),
MetadataValue::U64(blobs.width as u64),
);
md.insert(
MetadataKey::new(MetadataNamespace::Heic, "height"),
MetadataValue::U64(blobs.height as u64),
);
md
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn open_rejects_junk() {
let junk = vec![0u8; 64];
assert!(HeicFile::open(junk).is_err());
}
#[test]
fn read_metadata_junk_returns_default() {
let junk = vec![0u8; 64];
assert_eq!(read_heic_metadata(&junk), ImageMetadata::default());
}
#[test]
fn open_and_decode_homebrew_sample() {
let candidates = [
"/opt/homebrew/share/libheif/example.heic",
"/usr/local/share/libheif/example.heic",
];
let Some(path) = candidates.iter().find(|p| std::path::Path::new(p).exists()) else {
eprintln!("skipping: no libheif sample HEIC found");
return;
};
let data = std::fs::read(path).expect("read sample heic");
let file = HeicFile::open(data).expect("open sample heic");
let primary = file.decode_primary().expect("decode primary");
assert!(primary.width() > 0 && primary.height() > 0);
assert_eq!(
primary.data.len(),
primary.width() as usize * primary.height() as usize * 3
);
for aux in file.aux_images() {
let img = file.decode_aux(aux).expect("decode aux image");
assert!(img.width() > 0 && img.height() > 0);
}
let _ = file.metadata();
}
}