diff --git a/src/reader/decoder.rs b/src/reader/decoder.rs index 4c97c8d..177a73f 100644 --- a/src/reader/decoder.rs +++ b/src/reader/decoder.rs @@ -19,8 +19,9 @@ use weezl::{decode::Decoder as LzwDecoder, BitOrder, LzwError, LzwStatus}; /// GIF palettes are RGB pub const PLTE_CHANNELS: usize = 3; /// Headers for supported extensions. -const EXT_NAME_NETSCAPE: &[u8] = b"\x0bNETSCAPE2.0\x03"; +const EXT_NAME_NETSCAPE: &[u8] = b"\x0bNETSCAPE2.0\x01"; const EXT_NAME_XMP: &[u8] = b"\x0bXMP DataXMP"; +const EXT_NAME_ICC: &[u8] = b"\x0bICCRGBG1012"; /// An error returned in the case of the image not being formatted properly. #[derive(Debug)] @@ -382,6 +383,8 @@ pub struct StreamingDecoder { header_end_reached: bool, /// XMP metadata bytes. xmp_metadata: Option>, + /// ICC profile bytes. + icc_profile: Option>, } /// One version number of the GIF standard. @@ -396,6 +399,7 @@ pub enum Version { struct ExtensionData { id: AnyExtension, data: Vec, + sub_block_lens: Vec, is_block_end: bool, } @@ -465,11 +469,13 @@ impl StreamingDecoder { ext: ExtensionData { id: AnyExtension(0), data: Vec::with_capacity(256), // 0xFF + 1 byte length + sub_block_lens: Vec::new(), is_block_end: true, }, current: None, header_end_reached: false, xmp_metadata: None, + icc_profile: None, } } @@ -542,6 +548,12 @@ impl StreamingDecoder { self.xmp_metadata.as_deref() } + /// ICC profile stored in the image. + #[must_use] + pub fn icc_profile(&self) -> Option<&[u8]> { + self.icc_profile.as_deref() + } + /// The version number of the GIF standard used in this image. /// /// We suppose a minimum of `V87a` compatibility. This value will be reported until we have @@ -705,6 +717,7 @@ impl StreamingDecoder { } Some(Block::Extension) => { self.ext.data.clear(); + self.ext.sub_block_lens.clear(); self.ext.id = AnyExtension(b); if self.ext.id.into_known().is_none() { if !self.allow_unknown_blocks { @@ -766,36 +779,15 @@ impl StreamingDecoder { } } } else { - self.ext.data.push(b); + self.ext.sub_block_lens.push(b); self.ext.is_block_end = false; goto!(ExtensionDataBlock(b as usize), emit Decoded::SubBlockFinished(self.ext.id)) } } ApplicationExtension => { debug_assert_eq!(0, b); - // We can consume the extension data here as the next states won't access it anymore. - let mut data = std::mem::take(&mut self.ext.data); - - // the parser removes sub-block lenghts, so app name and data are concatenated - if let Some(&[first, second, ..]) = data.strip_prefix(EXT_NAME_NETSCAPE) { - let repeat = u16::from(first) | u16::from(second) << 8; - goto!(BlockEnd, emit Decoded::Repetitions(if repeat == 0 { Repeat::Infinite } else { Repeat::Finite(repeat) })) - } else if data.starts_with(EXT_NAME_XMP) { - data.drain(..EXT_NAME_XMP.len()); - // XMP adds a "ramp" of 257 bytes to the end of the metadata to let the "pascal-strings" - // parser converge to the null byte. The ramp looks like "0x01, 0xff, .., 0x01, 0x00". - // For convenience and to allow consumers to not be bothered with this implementation detail, - // we cut the ramp. - const RAMP_SIZE: usize = 257; - if data.len() >= RAMP_SIZE - && data.ends_with(&[0x03, 0x02, 0x01, 0x00]) - && data[data.len() - RAMP_SIZE..].starts_with(&[0x01, 0x0ff]) - { - data.truncate(data.len() - RAMP_SIZE); - } - - self.xmp_metadata = Some(data); - goto!(BlockEnd) + if let Some(decoded) = self.read_application_extension() { + goto!(BlockEnd, emit decoded) } else { goto!(BlockEnd) } @@ -904,6 +896,44 @@ impl StreamingDecoder { Ok(()) } + fn read_application_extension(&mut self) -> Option { + if let Some(&[first, second]) = self.ext.data.strip_prefix(EXT_NAME_NETSCAPE) { + let repeat = u16::from(first) | u16::from(second) << 8; + return Some(Decoded::Repetitions(if repeat == 0 { + Repeat::Infinite + } else { + Repeat::Finite(repeat) + })); + } else if let Some(mut rest) = self.ext.data.strip_prefix(EXT_NAME_XMP) { + // XMP is not written as a valid "pascal-string", so we need to stitch together + // the text from our collected sublock-lengths. + let mut xmp_metadata = Vec::new(); + for len in self.ext.sub_block_lens.iter() { + xmp_metadata.push(*len); + let (sub_block, tail) = rest.split_at(*len as usize); + xmp_metadata.extend_from_slice(sub_block); + rest = tail; + } + + // XMP adds a "ramp" of 257 bytes to the end of the metadata to let the "pascal-strings" + // parser converge to the null byte. The ramp looks like "0x01, 0xff, .., 0x01, 0x00". + // For convenience and to allow consumers to not be bothered with this implementation detail, + // we cut the ramp. + const RAMP_SIZE: usize = 257; + if xmp_metadata.len() >= RAMP_SIZE + && xmp_metadata.ends_with(&[0x03, 0x02, 0x01, 0x00]) + && xmp_metadata[xmp_metadata.len() - RAMP_SIZE..].starts_with(&[0x01, 0x0ff]) + { + xmp_metadata.truncate(xmp_metadata.len() - RAMP_SIZE); + } + + self.xmp_metadata = Some(xmp_metadata); + } else if let Some(rest) = self.ext.data.strip_prefix(EXT_NAME_ICC) { + self.icc_profile = Some(rest.to_vec()); + } + None + } + fn add_frame(&mut self) { if self.current.is_none() { self.current = Some(Frame::default()); diff --git a/src/reader/mod.rs b/src/reader/mod.rs index 87ef363..a9dc262 100644 --- a/src/reader/mod.rs +++ b/src/reader/mod.rs @@ -479,6 +479,12 @@ where self.decoder.decoder.xmp_metadata() } + /// ICC profile stored in the image. + #[inline] + pub fn icc_profile(&self) -> Option<&[u8]> { + self.decoder.decoder.icc_profile() + } + /// Abort decoding and recover the `io::Read` instance pub fn into_inner(self) -> io::BufReader { self.decoder.into_inner() diff --git a/tests/decode.rs b/tests/decode.rs index 0ea3f74..392b98b 100644 --- a/tests/decode.rs +++ b/tests/decode.rs @@ -1,7 +1,10 @@ #![cfg(feature = "std")] -use gif::{DecodeOptions, Decoder, DisposalMethod, Encoder, Frame}; -use std::fs::File; +use gif::{ + streaming_decoder::{Decoded, OutputBuffer, StreamingDecoder}, + DecodeOptions, Decoder, DisposalMethod, Encoder, Frame, +}; +use std::{fs::File, io::BufRead}; #[test] fn test_simple_indexed() { @@ -269,3 +272,38 @@ fn xmp_metadata() { assert_eq!(decoder.xmp_metadata(), Some(EXPECTED_METADATA.as_bytes())) } + +#[test] +fn icc_profile() { + let image: &[u8] = include_bytes!("samples/beacon_icc.gif"); + let icc_profile: &[u8] = include_bytes!("samples/profile.icc"); + let decoder = DecodeOptions::new().read_info(image).unwrap(); + + assert_eq!(decoder.icc_profile(), Some(icc_profile)) +} + +#[test] +fn check_last_extension_returns() { + let mut buf: &[u8] = include_bytes!("samples/beacon_xmp.gif"); + + let mut out_buf = Vec::new(); + let mut output = OutputBuffer::Vec(&mut out_buf); + + let mut decoder = StreamingDecoder::new(); + + loop { + let (consumed, result) = { + if buf.is_empty() { + break; + } + + decoder.update(&mut buf, &mut output).unwrap() + }; + buf.consume(consumed); + if let Decoded::HeaderEnd = result { + break; + } + } + + assert_eq!(decoder.last_ext().1.len(), 3048); +} diff --git a/tests/results.txt b/tests/results.txt index b9e50a1..118402d 100644 --- a/tests/results.txt +++ b/tests/results.txt @@ -1,5 +1,6 @@ tests/samples/alpha_gif_a.gif: 3871893825 tests/samples/anim-gr.gif: 291646878 +tests/samples/beacon_icc.gif: 2462153529 tests/samples/beacon_xmp.gif: 2462153529 tests/samples/beacon.gif: 2462153529 tests/samples/interlaced.gif: 2115495372 diff --git a/tests/samples/beacon_icc.gif b/tests/samples/beacon_icc.gif new file mode 100644 index 0000000..a63eed6 Binary files /dev/null and b/tests/samples/beacon_icc.gif differ diff --git a/tests/samples/profile.icc b/tests/samples/profile.icc new file mode 100644 index 0000000..e17f504 Binary files /dev/null and b/tests/samples/profile.icc differ