diff --git a/Cargo.lock b/Cargo.lock index b8fa3de7..8d520e61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "anes" version = "0.1.6" @@ -146,6 +161,16 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "brotli-decompressor" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -833,6 +858,7 @@ dependencies = [ name = "jxl-oxide" version = "0.11.0" dependencies = [ + "brotli-decompressor", "bytemuck", "image", "jxl-bitstream", diff --git a/crates/jxl-bitstream/src/container.rs b/crates/jxl-bitstream/src/container.rs index 3180aee4..c0527b79 100644 --- a/crates/jxl-bitstream/src/container.rs +++ b/crates/jxl-bitstream/src/container.rs @@ -20,13 +20,14 @@ enum DetectState { WaitingBoxHeader, WaitingJxlpIndex(ContainerBoxHeader), InAuxBox { - #[allow(unused)] header: ContainerBoxHeader, + brotli_box_type: Option, bytes_left: Option, }, InCodestream { kind: BitstreamKind, bytes_left: Option, + pending_no_more_aux_box: bool, }, } diff --git a/crates/jxl-bitstream/src/container/box_header.rs b/crates/jxl-bitstream/src/container/box_header.rs index c8f64f2f..493e2c8b 100644 --- a/crates/jxl-bitstream/src/container/box_header.rs +++ b/crates/jxl-bitstream/src/container/box_header.rs @@ -3,7 +3,7 @@ use crate::Error; /// Box header used in JPEG XL containers. #[derive(Debug, Clone)] pub struct ContainerBoxHeader { - ty: ContainerBoxType, + pub(super) ty: ContainerBoxType, box_size: Option, is_last: bool, } diff --git a/crates/jxl-bitstream/src/container/parse.rs b/crates/jxl-bitstream/src/container/parse.rs index 6bb201f1..e0670845 100644 --- a/crates/jxl-bitstream/src/container/parse.rs +++ b/crates/jxl-bitstream/src/container/parse.rs @@ -40,6 +40,7 @@ impl<'inner, 'buf> ParseEvents<'inner, 'buf> { *state = DetectState::InCodestream { kind: BitstreamKind::BareCodestream, bytes_left: None, + pending_no_more_aux_box: true, }; return Ok(Some(ParseEvent::BitstreamKind( BitstreamKind::BareCodestream, @@ -56,12 +57,14 @@ impl<'inner, 'buf> ParseEvents<'inner, 'buf> { *state = DetectState::InCodestream { kind: BitstreamKind::Invalid, bytes_left: None, + pending_no_more_aux_box: true, }; return Ok(Some(ParseEvent::BitstreamKind(BitstreamKind::Invalid))); } else { return Ok(None); } } + DetectState::WaitingBoxHeader => match ContainerBoxHeader::parse(buf)? { HeaderParseResult::Done { header, @@ -84,9 +87,11 @@ impl<'inner, 'buf> ParseEvents<'inner, 'buf> { } } + let bytes_left = header.box_size().map(|x| x as usize); *state = DetectState::InCodestream { kind: BitstreamKind::Container, - bytes_left: header.box_size().map(|x| x as usize), + bytes_left, + pending_no_more_aux_box: bytes_left.is_none(), }; } else if tbox == ContainerBoxType::PARTIAL_CODESTREAM { if let Some(box_size) = header.box_size() { @@ -115,11 +120,37 @@ impl<'inner, 'buf> ParseEvents<'inner, 'buf> { *state = DetectState::WaitingJxlpIndex(header); } else { let bytes_left = header.box_size().map(|x| x as usize); - *state = DetectState::InAuxBox { header, bytes_left }; + let ty = header.box_type(); + let mut brotli_compressed = ty == ContainerBoxType::BROTLI_COMPRESSED; + if brotli_compressed { + if let Some(0..=3) = bytes_left { + tracing::error!( + bytes_left = bytes_left.unwrap(), + "Brotli-compressed box is too small" + ); + return Err(Error::InvalidBox); + } + brotli_compressed = true; + } + + *state = DetectState::InAuxBox { + header, + brotli_box_type: None, + bytes_left, + }; + + if !brotli_compressed { + return Ok(Some(ParseEvent::AuxBoxStart { + ty, + brotli_compressed: false, + last_box: bytes_left.is_none(), + })); + } } } HeaderParseResult::NeedMoreData => return Ok(None), }, + DetectState::WaitingJxlpIndex(header) => { let &[b0, b1, b2, b3, ..] = &**buf else { return Ok(None); @@ -149,11 +180,23 @@ impl<'inner, 'buf> ParseEvents<'inner, 'buf> { } } + let bytes_left = header.box_size().map(|x| x as usize - 4); *state = DetectState::InCodestream { kind: BitstreamKind::Container, - bytes_left: header.box_size().map(|x| x as usize - 4), + bytes_left, + pending_no_more_aux_box: bytes_left.is_none(), }; } + + // JXL codestream box is the last box; emit "no more aux box" event. + DetectState::InCodestream { + pending_no_more_aux_box: pending @ true, + .. + } => { + *pending = false; + return Ok(Some(ParseEvent::NoMoreAuxBox)); + } + DetectState::InCodestream { bytes_left: None, .. } => { @@ -178,30 +221,78 @@ impl<'inner, 'buf> ParseEvents<'inner, 'buf> { }; return Ok(Some(ParseEvent::Codestream(payload))); } + + // Read brob payload box type. DetectState::InAuxBox { - header: _, - bytes_left: None, + header: + ContainerBoxHeader { + ty: ContainerBoxType::BROTLI_COMPRESSED, + .. + }, + brotli_box_type: brotli_box_type @ None, + bytes_left, } => { - let _payload = *buf; - *buf = &[]; - // FIXME: emit auxiliary box event + if buf.len() < 4 { + return Ok(None); + } + + let (ty_slice, remaining) = buf.split_at(4); + *buf = remaining; + if let Some(bytes_left) = bytes_left { + *bytes_left -= 4; + } + + let mut ty = [0u8; 4]; + ty.copy_from_slice(ty_slice); + let is_reserved_box_type = + &ty[..3] == b"jxl" || &ty == b"brob" || &ty == b"jbrd"; + if is_reserved_box_type { + return Err(Error::ValidationFailed( + "brob box, jxl boxes and jbrd box cannot be Brotli-compressed", + )); + } + + let ty = ContainerBoxType(ty); + *brotli_box_type = Some(ty); + + return Ok(Some(ParseEvent::AuxBoxStart { + ty, + brotli_compressed: true, + last_box: bytes_left.is_none(), + })); } + DetectState::InAuxBox { - header: _, - bytes_left: Some(bytes_left), + header: ContainerBoxHeader { ty, .. }, + brotli_box_type, + bytes_left, } => { - let _payload = if buf.len() >= *bytes_left { - let (payload, remaining) = buf.split_at(*bytes_left); - *state = DetectState::WaitingBoxHeader; - *buf = remaining; - payload + let ty = if let Some(ty) = brotli_box_type { + *ty } else { - let payload = *buf; - *bytes_left -= buf.len(); - *buf = &[]; - payload + *ty }; - // FIXME: emit auxiliary box event + + let payload = match bytes_left { + Some(0) => { + *state = DetectState::WaitingBoxHeader; + return Ok(Some(ParseEvent::AuxBoxEnd(ty))); + } + Some(bytes_left) => { + let num_bytes_to_read = (*bytes_left).min(buf.len()); + let (payload, remaining) = buf.split_at(num_bytes_to_read); + *bytes_left -= num_bytes_to_read; + *buf = remaining; + payload + } + None => { + let payload = *buf; + *buf = &[]; + payload + } + }; + + return Ok(Some(ParseEvent::AuxBoxData(ty, payload))); } } } @@ -250,6 +341,14 @@ pub enum ParseEvent<'buf> { /// Returned data may be partial. Complete codestream can be obtained by concatenating all data /// of `Codestream` events. Codestream(&'buf [u8]), + NoMoreAuxBox, + AuxBoxStart { + ty: ContainerBoxType, + brotli_compressed: bool, + last_box: bool, + }, + AuxBoxData(ContainerBoxType, &'buf [u8]), + AuxBoxEnd(ContainerBoxType), } impl std::fmt::Debug for ParseEvent<'_> { @@ -260,6 +359,23 @@ impl std::fmt::Debug for ParseEvent<'_> { .debug_tuple("Codestream") .field(&format_args!("{} byte(s)", buf.len())) .finish(), + Self::NoMoreAuxBox => write!(f, "NoMoreAuxBox"), + Self::AuxBoxStart { + ty, + brotli_compressed, + last_box, + } => f + .debug_struct("AuxBoxStart") + .field("ty", ty) + .field("brotli_compressed", brotli_compressed) + .field("last_box", last_box) + .finish(), + Self::AuxBoxData(ty, buf) => f + .debug_tuple("AuxBoxData") + .field(ty) + .field(&format_args!("{} byte(s)", buf.len())) + .finish(), + Self::AuxBoxEnd(ty) => f.debug_tuple("AuxBoxEnd").field(&ty).finish(), } } } diff --git a/crates/jxl-oxide-cli/src/info.rs b/crates/jxl-oxide-cli/src/info.rs index cdc82d9d..39c95550 100644 --- a/crates/jxl-oxide-cli/src/info.rs +++ b/crates/jxl-oxide-cli/src/info.rs @@ -63,6 +63,19 @@ pub fn handle_info(args: InfoArgs) -> Result<()> { } } + match image.raw_exif_data() { + Ok(None) => {} + Ok(Some(exif)) => { + if let Some(data) = exif.payload() { + let size = data.len(); + println!("Exif metadata: {size} byte(s)"); + } + } + Err(e) => { + println!("Invalid Exif metadata: {e}"); + } + } + if let Some(animation) = &image_meta.animation { println!( " Animated ({}/{} ticks per second)", diff --git a/crates/jxl-oxide/Cargo.toml b/crates/jxl-oxide/Cargo.toml index ec6a8f2f..4cee1c1f 100644 --- a/crates/jxl-oxide/Cargo.toml +++ b/crates/jxl-oxide/Cargo.toml @@ -12,6 +12,7 @@ version = "0.11.0" edition = "2021" [dependencies] +brotli-decompressor = "4.0.1" tracing.workspace = true [dependencies.bytemuck] diff --git a/crates/jxl-oxide/examples/image-integration.rs b/crates/jxl-oxide/examples/image-integration.rs index 6f16b30b..eb22058e 100644 --- a/crates/jxl-oxide/examples/image-integration.rs +++ b/crates/jxl-oxide/examples/image-integration.rs @@ -16,6 +16,13 @@ fn main() { #[allow(unused)] let icc = decoder.icc_profile().unwrap(); + let exif = decoder + .exif_metadata() + .expect("cannot decode Exif metadata"); + if let Some(exif) = exif { + println!("Exif metadata found ({} byte(s))", exif.len()); + } + let image = DynamicImage::from_decoder(decoder).expect("cannot decode image"); let output_file = std::fs::File::create(output_path).expect("cannot open output file"); diff --git a/crates/jxl-oxide/src/aux_box.rs b/crates/jxl-oxide/src/aux_box.rs new file mode 100644 index 00000000..2ef588f0 --- /dev/null +++ b/crates/jxl-oxide/src/aux_box.rs @@ -0,0 +1,153 @@ +use std::io::Write; + +use brotli_decompressor::DecompressorWriter; + +use crate::Result; + +mod exif; + +pub use exif::*; + +#[derive(Debug, Default)] +pub struct AuxBoxReader { + data: DataKind, + done: bool, +} + +#[derive(Default)] +enum DataKind { + #[default] + Init, + NoData, + Raw(Vec), + Brotli(Box>>), +} + +impl std::fmt::Debug for DataKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Init => write!(f, "Init"), + Self::NoData => write!(f, "NoData"), + Self::Raw(buf) => f + .debug_tuple("Raw") + .field(&format_args!("{} byte(s)", buf.len())) + .finish(), + Self::Brotli(_) => f.debug_tuple("Brotli").finish(), + } + } +} + +impl AuxBoxReader { + pub(super) fn new() -> Self { + Self::default() + } + + pub(super) fn ensure_brotli(&mut self) -> Result<()> { + if self.done { + return Ok(()); + } + + match self.data { + DataKind::Init => { + let writer = DecompressorWriter::new(Vec::::new(), 4096); + self.data = DataKind::Brotli(Box::new(writer)); + } + DataKind::NoData | DataKind::Raw(_) => { + panic!(); + } + DataKind::Brotli(_) => {} + } + Ok(()) + } +} + +impl AuxBoxReader { + pub fn feed_data(&mut self, data: &[u8]) -> Result<()> { + if self.done { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "cannot feed into finalized box", + ) + .into()); + } + + match self.data { + DataKind::Init => { + self.data = DataKind::Raw(data.to_vec()); + } + DataKind::NoData => { + unreachable!(); + } + DataKind::Raw(ref mut buf) => { + buf.extend_from_slice(data); + } + DataKind::Brotli(ref mut writer) => { + writer.write_all(data)?; + } + } + Ok(()) + } + + pub fn finalize(&mut self) -> Result<()> { + if self.done { + return Ok(()); + } + + if let DataKind::Brotli(ref mut writer) = self.data { + writer.flush()?; + writer.close()?; + } + + match std::mem::replace(&mut self.data, DataKind::NoData) { + DataKind::Init | DataKind::NoData => {} + DataKind::Raw(buf) => self.data = DataKind::Raw(buf), + DataKind::Brotli(writer) => { + let inner = writer.into_inner().inspect_err(|_| { + tracing::warn!("Brotli decompressor reported an error"); + }); + let buf = inner.unwrap_or_else(|buf| buf); + self.data = DataKind::Raw(buf); + } + } + + self.done = true; + Ok(()) + } +} + +impl AuxBoxReader { + pub fn is_done(&self) -> bool { + self.done + } + + pub fn data(&self) -> AuxBoxData { + if !self.is_done() { + return AuxBoxData::Decoding; + } + + match &self.data { + DataKind::Init | DataKind::Brotli(_) => AuxBoxData::Decoding, + DataKind::NoData => AuxBoxData::NotFound, + DataKind::Raw(buf) => AuxBoxData::Data(buf), + } + } +} + +pub enum AuxBoxData<'data> { + Data(&'data [u8]), + Decoding, + NotFound, +} + +impl std::fmt::Debug for AuxBoxData<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Data(buf) => f + .debug_tuple("Data") + .field(&format_args!("{} byte(s)", buf.len())) + .finish(), + Self::Decoding => write!(f, "Decoding"), + Self::NotFound => write!(f, "NotFound"), + } + } +} diff --git a/crates/jxl-oxide/src/aux_box/exif.rs b/crates/jxl-oxide/src/aux_box/exif.rs new file mode 100644 index 00000000..2bfb43a3 --- /dev/null +++ b/crates/jxl-oxide/src/aux_box/exif.rs @@ -0,0 +1,64 @@ +use crate::Result; + +/// Raw Exif metadata. +pub struct RawExif<'image> { + tiff_header_offset: u32, + payload: Option<&'image [u8]>, +} + +impl std::fmt::Debug for RawExif<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RawExif") + .field("tiff_header_offset", &self.tiff_header_offset) + .finish_non_exhaustive() + } +} + +impl<'image> RawExif<'image> { + pub(crate) fn empty() -> Self { + Self { + tiff_header_offset: 0, + payload: None, + } + } + + pub(crate) fn new(box_data: &'image [u8]) -> Result { + if box_data.len() < 4 { + tracing::error!(len = box_data.len(), "Exif box is too short"); + return Err(jxl_bitstream::Error::ValidationFailed("Exif box is too short").into()); + } + + let (tiff, payload) = box_data.split_at(4); + let tiff_header_offset = u32::from_be_bytes([tiff[0], tiff[1], tiff[2], tiff[3]]); + if tiff_header_offset as usize >= payload.len() { + tracing::error!( + payload_len = payload.len(), + tiff_header_offset, + "tiff_header_offset of Exif box is too large" + ); + return Err(jxl_bitstream::Error::ValidationFailed( + "tiff_header_offset of Exif box is too large", + ) + .into()); + } + + Ok(Self { + tiff_header_offset, + payload: Some(payload), + }) + } +} + +impl<'image> RawExif<'image> { + /// Returns the offset of TIFF header within the payload. + pub fn tiff_header_offset(&self) -> u32 { + self.tiff_header_offset + } + + /// Returns the payload, if exists. + /// + /// Returns `None` if Exif metadata wasn't found in the image. + pub fn payload(&self) -> Option<&'image [u8]> { + self.payload + } +} diff --git a/crates/jxl-oxide/src/integration/image.rs b/crates/jxl-oxide/src/integration/image.rs index bb737f47..d9c7c50d 100644 --- a/crates/jxl-oxide/src/integration/image.rs +++ b/crates/jxl-oxide/src/integration/image.rs @@ -14,6 +14,7 @@ use crate::{CropInfo, InitializeResult, JxlImage}; /// - Returning images of 8-bit, 16-bit integer and 32-bit float samples /// - RGB or luma-only images, with or without alpha /// - Returning ICC profiles via `icc_profile` +/// - Returning Exif metadata via `exif_metadata` /// - Setting decoder limits (caveat: memory limits are not strict) /// - Cropped decoding with [`ImageDecoderRect`][image::ImageDecoderRect] /// - (When `lcms2` feature is enabled) Converting CMYK images to sRGB color space @@ -154,8 +155,11 @@ impl JxlDecoder { Ok(image) } - fn load_until_first_keyframe(&mut self) -> crate::Result<()> { - while self.image.ctx.loaded_keyframes() == 0 { + fn load_until_condition( + &mut self, + mut predicate: impl FnMut(&JxlImage) -> crate::Result, + ) -> crate::Result<()> { + while !predicate(&self.image)? { let count = self.reader.read(&mut self.buf[self.buf_valid..])?; if count == 0 { break; @@ -166,6 +170,12 @@ impl JxlDecoder { self.buf_valid -= consumed; } + Ok(()) + } + + fn load_until_first_keyframe(&mut self) -> crate::Result<()> { + self.load_until_condition(|image| Ok(image.ctx.loaded_frames() > 0))?; + if self.image.frame_by_keyframe(0).is_none() { return Err(std::io::Error::new( std::io::ErrorKind::UnexpectedEof, @@ -177,6 +187,10 @@ impl JxlDecoder { Ok(()) } + fn load_until_exif(&mut self) -> crate::Result<()> { + self.load_until_condition(|image| Ok(image.raw_exif_data()?.is_some())) + } + #[inline] fn is_float(&self) -> bool { use crate::BitDepth; @@ -305,6 +319,20 @@ impl image::ImageDecoder for JxlDecoder { Ok(Some(self.image.rendered_icc())) } + fn exif_metadata(&mut self) -> ImageResult>> { + self.load_until_exif().map_err(|e| { + ImageError::Decoding(DecodingError::new( + ImageFormatHint::PathExtension("jxl".into()), + e, + )) + })?; + + let Some(exif) = self.image.raw_exif_data().unwrap() else { + return Ok(None); + }; + Ok(exif.payload().map(|v| v.to_vec())) + } + fn set_limits(&mut self, limits: image::Limits) -> ImageResult<()> { use image::error::{LimitError, LimitErrorKind}; diff --git a/crates/jxl-oxide/src/lib.rs b/crates/jxl-oxide/src/lib.rs index 2e2ad179..5056c4da 100644 --- a/crates/jxl-oxide/src/lib.rs +++ b/crates/jxl-oxide/src/lib.rs @@ -148,6 +148,7 @@ use std::sync::Arc; +use jxl_bitstream::container::box_header::ContainerBoxType; use jxl_bitstream::{Bitstream, ContainerDetectingReader, ParseEvent}; use jxl_frame::FrameContext; use jxl_image::BitDepth; @@ -168,13 +169,17 @@ pub use jxl_image as image; pub use jxl_image::{ExtraChannelType, ImageHeader}; pub use jxl_threadpool::JxlThreadPool; +mod aux_box; mod fb; pub mod integration; #[cfg(feature = "lcms2")] mod lcms2; +use aux_box::AuxBoxReader; + #[cfg(feature = "lcms2")] pub use self::lcms2::Lcms2; +pub use aux_box::RawExif; pub use fb::{FrameBuffer, FrameBufferSample, ImageStream}; pub type Result = std::result::Result>; @@ -216,6 +221,7 @@ impl JxlImageBuilder { tracker: self.tracker, reader: ContainerDetectingReader::new(), buffer: Vec::new(), + exif: AuxBoxReader::new(), } } @@ -299,6 +305,7 @@ pub struct UninitializedJxlImage { tracker: Option, reader: ContainerDetectingReader, buffer: Vec, + exif: AuxBoxReader, } impl UninitializedJxlImage { @@ -312,6 +319,28 @@ impl UninitializedJxlImage { ParseEvent::Codestream(buf) => { self.buffer.extend_from_slice(buf); } + ParseEvent::NoMoreAuxBox => { + self.exif.finalize()?; + } + ParseEvent::AuxBoxStart { + ty: ContainerBoxType::EXIF, + brotli_compressed: true, + .. + } => { + self.exif.ensure_brotli()?; + } + ParseEvent::AuxBoxStart { last_box: true, .. } => { + self.exif.finalize()?; + } + ParseEvent::AuxBoxStart { .. } => {} + ParseEvent::AuxBoxData(ContainerBoxType::EXIF, buf) => { + self.exif.feed_data(buf)?; + } + ParseEvent::AuxBoxData(..) => {} + ParseEvent::AuxBoxEnd(ContainerBoxType::EXIF) => { + self.exif.finalize()?; + } + ParseEvent::AuxBoxEnd(..) => {} } } Ok(self.reader.previous_consumed_bytes()) @@ -418,6 +447,7 @@ impl UninitializedJxlImage { buffer: Vec::new(), buffer_offset: bytes_read, frame_offsets: Vec::new(), + exif: self.exif, }, }; image.inner.feed_bytes_inner(&mut image.ctx, &self.buffer)?; @@ -462,10 +492,37 @@ impl JxlImage { ParseEvent::Codestream(buf) => { self.inner.feed_bytes_inner(&mut self.ctx, buf)?; } + ParseEvent::NoMoreAuxBox => { + self.inner.exif.finalize()?; + } + ParseEvent::AuxBoxStart { + ty: ContainerBoxType::EXIF, + brotli_compressed: true, + .. + } => { + self.inner.exif.ensure_brotli()?; + } + ParseEvent::AuxBoxStart { last_box: true, .. } => { + self.inner.exif.finalize()?; + } + ParseEvent::AuxBoxStart { .. } => {} + ParseEvent::AuxBoxData(ContainerBoxType::EXIF, buf) => { + self.inner.exif.feed_data(buf)?; + } + ParseEvent::AuxBoxData(..) => {} + ParseEvent::AuxBoxEnd(ContainerBoxType::EXIF) => { + self.inner.exif.finalize()?; + } + ParseEvent::AuxBoxEnd(..) => {} } } Ok(self.reader.previous_consumed_bytes()) } + + pub fn finalize(&mut self) -> Result<()> { + self.inner.exif.finalize()?; + Ok(()) + } } #[derive(Debug)] @@ -474,6 +531,7 @@ struct JxlImageInner { buffer: Vec, buffer_offset: usize, frame_offsets: Vec, + exif: AuxBoxReader, } impl JxlImageInner { @@ -672,6 +730,22 @@ impl JxlImage { } } +impl JxlImage { + /// Returns the raw Exif metadata, if any. + /// + /// Returns `Ok(None)` if more data is needed. + pub fn raw_exif_data(&self) -> Result> { + match self.inner.exif.data() { + aux_box::AuxBoxData::Data(data) => { + let exif = RawExif::new(data)?; + Ok(Some(exif)) + } + aux_box::AuxBoxData::Decoding => Ok(None), + aux_box::AuxBoxData::NotFound => Ok(Some(RawExif::empty())), + } + } +} + impl JxlImage { /// Returns the number of currently loaded keyframes. #[inline]