Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 55 additions & 25 deletions src/reader/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -382,6 +383,8 @@ pub struct StreamingDecoder {
header_end_reached: bool,
/// XMP metadata bytes.
xmp_metadata: Option<Vec<u8>>,
/// ICC profile bytes.
icc_profile: Option<Vec<u8>>,
}

/// One version number of the GIF standard.
Expand All @@ -396,6 +399,7 @@ pub enum Version {
struct ExtensionData {
id: AnyExtension,
data: Vec<u8>,
sub_block_lens: Vec<u8>,
is_block_end: bool,
}

Expand Down Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -904,6 +896,44 @@ impl StreamingDecoder {
Ok(())
}

fn read_application_extension(&mut self) -> Option<Decoded> {
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());
Expand Down
6 changes: 6 additions & 0 deletions src/reader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<R> {
self.decoder.into_inner()
Expand Down
42 changes: 40 additions & 2 deletions tests/decode.rs
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -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);
}
1 change: 1 addition & 0 deletions tests/results.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Binary file added tests/samples/beacon_icc.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/samples/profile.icc
Binary file not shown.
Loading