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
37 changes: 34 additions & 3 deletions src/reader/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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_XMP: &[u8] = b"\x0bXMP DataXMP";

/// An error returned in the case of the image not being formatted properly.
#[derive(Debug)]
Expand Down Expand Up @@ -377,6 +380,8 @@ pub struct StreamingDecoder {
current: Option<Frame<'static>>,
/// Needs to emit `HeaderEnd` once
header_end_reached: bool,
/// XMP metadata bytes.
xmp_metadata: Option<Vec<u8>>,
}

/// One version number of the GIF standard.
Expand Down Expand Up @@ -464,6 +469,7 @@ impl StreamingDecoder {
},
current: None,
header_end_reached: false,
xmp_metadata: None,
}
}

Expand Down Expand Up @@ -530,6 +536,12 @@ impl StreamingDecoder {
self.height
}

/// XMP metadata stored in the image.
#[must_use]
pub fn xmp_metadata(&self) -> Option<&[u8]> {
self.xmp_metadata.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 @@ -754,17 +766,36 @@ impl StreamingDecoder {
}
}
} else {
self.ext.data.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 self.ext.data.len() >= 15 && &self.ext.data[1..13] == b"NETSCAPE2.0\x01" {
let repeat = &self.ext.data[13..15];
let repeat = u16::from(repeat[0]) | u16::from(repeat[1]) << 8;
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)
} else {
goto!(BlockEnd)
}
Expand Down
6 changes: 6 additions & 0 deletions src/reader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,12 @@ where
self.decoder.decoder.height()
}

/// XMP metadata.
#[inline]
pub fn xmp_metadata(&self) -> Option<&[u8]> {
self.decoder.decoder.xmp_metadata()
}

/// Abort decoding and recover the `io::Read` instance
pub fn into_inner(self) -> io::BufReader<R> {
self.decoder.into_inner()
Expand Down
10 changes: 10 additions & 0 deletions tests/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,13 @@ fn issue_209_exension_block() {
}
})();
}

#[test]
fn xmp_metadata() {
const EXPECTED_METADATA: &str = "<?xpacket begin='\u{feff}' id='W5M0MpCehiHzreSzNTczkc9d'?>\n<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 13.25'>\n<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>\n\n <rdf:Description rdf:about=''\n xmlns:dc='http://purl.org/dc/elements/1.1/'>\n <dc:subject>\n <rdf:Bag>\n <rdf:li>sunset, mountains, nature</rdf:li>\n </rdf:Bag>\n </dc:subject>\n </rdf:Description>\n</rdf:RDF>\n</x:xmpmeta>\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n<?xpacket end='w'?>";

let image: &[u8] = include_bytes!("samples/beacon_xmp.gif");
let decoder = DecodeOptions::new().read_info(image).unwrap();

assert_eq!(decoder.xmp_metadata(), Some(EXPECTED_METADATA.as_bytes()))
}
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_xmp.gif: 2462153529
tests/samples/beacon.gif: 2462153529
tests/samples/interlaced.gif: 2115495372
tests/samples/moon_impact.gif: 2438689726
Expand Down
Binary file added tests/samples/beacon_xmp.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading