Skip to content

Commit 948744a

Browse files
1c3t3aShaddyDC
andcommitted
Add support for decoding XMP metadata
This change adds support for accessing the XMP metadata stored in a GIF. This extends the StreamingDecoder implementation that now reads this Extension section correctly. Co-authored-by: Rasmus Piorr <piorr@google.com>
1 parent 6b720f0 commit 948744a

File tree

5 files changed

+54
-3
lines changed

5 files changed

+54
-3
lines changed

src/reader/decoder.rs

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ use weezl::{decode::Decoder as LzwDecoder, BitOrder, LzwError, LzwStatus};
1818

1919
/// GIF palettes are RGB
2020
pub const PLTE_CHANNELS: usize = 3;
21+
/// Headers for supported extensions.
22+
const EXT_NAME_NETSCAPE: &[u8] = b"\x0bNETSCAPE2.0\x03";
23+
const EXT_NAME_XMP: &[u8] = b"\x0bXMP DataXMP";
2124

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

382387
/// One version number of the GIF standard.
@@ -464,6 +469,7 @@ impl StreamingDecoder {
464469
},
465470
current: None,
466471
header_end_reached: false,
472+
xmp_metadata: None,
467473
}
468474
}
469475

@@ -530,6 +536,12 @@ impl StreamingDecoder {
530536
self.height
531537
}
532538

539+
/// XMP metadata stored in the image.
540+
#[must_use]
541+
pub fn xmp_metadata(&self) -> Option<&[u8]> {
542+
self.xmp_metadata.as_deref()
543+
}
544+
533545
/// The version number of the GIF standard used in this image.
534546
///
535547
/// We suppose a minimum of `V87a` compatibility. This value will be reported until we have
@@ -754,17 +766,36 @@ impl StreamingDecoder {
754766
}
755767
}
756768
} else {
769+
self.ext.data.push(b);
757770
self.ext.is_block_end = false;
758771
goto!(ExtensionDataBlock(b as usize), emit Decoded::SubBlockFinished(self.ext.id))
759772
}
760773
}
761774
ApplicationExtension => {
762775
debug_assert_eq!(0, b);
776+
// We can consume the extension data here as the next states won't access it anymore.
777+
let mut data = std::mem::take(&mut self.ext.data);
778+
763779
// the parser removes sub-block lenghts, so app name and data are concatenated
764-
if self.ext.data.len() >= 15 && &self.ext.data[1..13] == b"NETSCAPE2.0\x01" {
765-
let repeat = &self.ext.data[13..15];
766-
let repeat = u16::from(repeat[0]) | u16::from(repeat[1]) << 8;
780+
if let Some(&[first, second, ..]) = data.strip_prefix(EXT_NAME_NETSCAPE) {
781+
let repeat = u16::from(first) | u16::from(second) << 8;
767782
goto!(BlockEnd, emit Decoded::Repetitions(if repeat == 0 { Repeat::Infinite } else { Repeat::Finite(repeat) }))
783+
} else if data.starts_with(EXT_NAME_XMP) {
784+
data.drain(..EXT_NAME_XMP.len());
785+
// XMP adds a "ramp" of 257 bytes to the end of the metadata to let the "pascal-strings"
786+
// parser converge to the null byte. The ramp looks like "0x01, 0xff, .., 0x01, 0x00".
787+
// For convenience and to allow consumers to not be bothered with this implementation detail,
788+
// we cut the ramp.
789+
const RAMP_SIZE: usize = 257;
790+
if data.len() >= RAMP_SIZE
791+
&& data.ends_with(&[0x03, 0x02, 0x01, 0x00])
792+
&& data[data.len() - RAMP_SIZE..].starts_with(&[0x01, 0x0ff])
793+
{
794+
data.truncate(data.len() - RAMP_SIZE);
795+
}
796+
797+
self.xmp_metadata = Some(data);
798+
goto!(BlockEnd)
768799
} else {
769800
goto!(BlockEnd)
770801
}

src/reader/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,12 @@ where
473473
self.decoder.decoder.height()
474474
}
475475

476+
/// XMP metadata.
477+
#[inline]
478+
pub fn xmp_metadata(&self) -> Option<&[u8]> {
479+
self.decoder.decoder.xmp_metadata()
480+
}
481+
476482
/// Abort decoding and recover the `io::Read` instance
477483
pub fn into_inner(self) -> io::BufReader<R> {
478484
self.decoder.into_inner()

tests/decode.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,3 +259,16 @@ fn issue_209_exension_block() {
259259
}
260260
})();
261261
}
262+
263+
#[test]
264+
fn xmp_metadata() {
265+
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'?>";
266+
267+
let image: &[u8] = include_bytes!("samples/beacon_xmp.gif");
268+
let decoder = DecodeOptions::new().read_info(image).unwrap();
269+
270+
assert_eq!(
271+
decoder.xmp_metadata(),
272+
Some(EXPECTED_METADATA.as_bytes())
273+
)
274+
}

tests/results.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
tests/samples/alpha_gif_a.gif: 3871893825
22
tests/samples/anim-gr.gif: 291646878
3+
tests/samples/beacon_xmp.gif: 2462153529
34
tests/samples/beacon.gif: 2462153529
45
tests/samples/interlaced.gif: 2115495372
56
tests/samples/moon_impact.gif: 2438689726

tests/samples/beacon_xmp.gif

3.15 KB
Loading

0 commit comments

Comments
 (0)