Skip to content

Commit 901951b

Browse files
authored
Merge pull request #216 from 1c3t3a/icc-profile
Add support for reading ICC metadata and fix `StreamingDecoder::last_ext`
2 parents e497980 + 85f4274 commit 901951b

File tree

6 files changed

+102
-27
lines changed

6 files changed

+102
-27
lines changed

src/reader/decoder.rs

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ use weezl::{decode::Decoder as LzwDecoder, BitOrder, LzwError, LzwStatus};
1919
/// GIF palettes are RGB
2020
pub const PLTE_CHANNELS: usize = 3;
2121
/// Headers for supported extensions.
22-
const EXT_NAME_NETSCAPE: &[u8] = b"\x0bNETSCAPE2.0\x03";
22+
const EXT_NAME_NETSCAPE: &[u8] = b"\x0bNETSCAPE2.0\x01";
2323
const EXT_NAME_XMP: &[u8] = b"\x0bXMP DataXMP";
24+
const EXT_NAME_ICC: &[u8] = b"\x0bICCRGBG1012";
2425

2526
/// An error returned in the case of the image not being formatted properly.
2627
#[derive(Debug)]
@@ -382,6 +383,8 @@ pub struct StreamingDecoder {
382383
header_end_reached: bool,
383384
/// XMP metadata bytes.
384385
xmp_metadata: Option<Vec<u8>>,
386+
/// ICC profile bytes.
387+
icc_profile: Option<Vec<u8>>,
385388
}
386389

387390
/// One version number of the GIF standard.
@@ -396,6 +399,7 @@ pub enum Version {
396399
struct ExtensionData {
397400
id: AnyExtension,
398401
data: Vec<u8>,
402+
sub_block_lens: Vec<u8>,
399403
is_block_end: bool,
400404
}
401405

@@ -465,11 +469,13 @@ impl StreamingDecoder {
465469
ext: ExtensionData {
466470
id: AnyExtension(0),
467471
data: Vec::with_capacity(256), // 0xFF + 1 byte length
472+
sub_block_lens: Vec::new(),
468473
is_block_end: true,
469474
},
470475
current: None,
471476
header_end_reached: false,
472477
xmp_metadata: None,
478+
icc_profile: None,
473479
}
474480
}
475481

@@ -542,6 +548,12 @@ impl StreamingDecoder {
542548
self.xmp_metadata.as_deref()
543549
}
544550

551+
/// ICC profile stored in the image.
552+
#[must_use]
553+
pub fn icc_profile(&self) -> Option<&[u8]> {
554+
self.icc_profile.as_deref()
555+
}
556+
545557
/// The version number of the GIF standard used in this image.
546558
///
547559
/// We suppose a minimum of `V87a` compatibility. This value will be reported until we have
@@ -705,6 +717,7 @@ impl StreamingDecoder {
705717
}
706718
Some(Block::Extension) => {
707719
self.ext.data.clear();
720+
self.ext.sub_block_lens.clear();
708721
self.ext.id = AnyExtension(b);
709722
if self.ext.id.into_known().is_none() {
710723
if !self.allow_unknown_blocks {
@@ -766,36 +779,15 @@ impl StreamingDecoder {
766779
}
767780
}
768781
} else {
769-
self.ext.data.push(b);
782+
self.ext.sub_block_lens.push(b);
770783
self.ext.is_block_end = false;
771784
goto!(ExtensionDataBlock(b as usize), emit Decoded::SubBlockFinished(self.ext.id))
772785
}
773786
}
774787
ApplicationExtension => {
775788
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-
779-
// the parser removes sub-block lenghts, so app name and data are concatenated
780-
if let Some(&[first, second, ..]) = data.strip_prefix(EXT_NAME_NETSCAPE) {
781-
let repeat = u16::from(first) | u16::from(second) << 8;
782-
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)
789+
if let Some(decoded) = self.read_application_extension() {
790+
goto!(BlockEnd, emit decoded)
799791
} else {
800792
goto!(BlockEnd)
801793
}
@@ -904,6 +896,44 @@ impl StreamingDecoder {
904896
Ok(())
905897
}
906898

899+
fn read_application_extension(&mut self) -> Option<Decoded> {
900+
if let Some(&[first, second]) = self.ext.data.strip_prefix(EXT_NAME_NETSCAPE) {
901+
let repeat = u16::from(first) | u16::from(second) << 8;
902+
return Some(Decoded::Repetitions(if repeat == 0 {
903+
Repeat::Infinite
904+
} else {
905+
Repeat::Finite(repeat)
906+
}));
907+
} else if let Some(mut rest) = self.ext.data.strip_prefix(EXT_NAME_XMP) {
908+
// XMP is not written as a valid "pascal-string", so we need to stitch together
909+
// the text from our collected sublock-lengths.
910+
let mut xmp_metadata = Vec::new();
911+
for len in self.ext.sub_block_lens.iter() {
912+
xmp_metadata.push(*len);
913+
let (sub_block, tail) = rest.split_at(*len as usize);
914+
xmp_metadata.extend_from_slice(sub_block);
915+
rest = tail;
916+
}
917+
918+
// XMP adds a "ramp" of 257 bytes to the end of the metadata to let the "pascal-strings"
919+
// parser converge to the null byte. The ramp looks like "0x01, 0xff, .., 0x01, 0x00".
920+
// For convenience and to allow consumers to not be bothered with this implementation detail,
921+
// we cut the ramp.
922+
const RAMP_SIZE: usize = 257;
923+
if xmp_metadata.len() >= RAMP_SIZE
924+
&& xmp_metadata.ends_with(&[0x03, 0x02, 0x01, 0x00])
925+
&& xmp_metadata[xmp_metadata.len() - RAMP_SIZE..].starts_with(&[0x01, 0x0ff])
926+
{
927+
xmp_metadata.truncate(xmp_metadata.len() - RAMP_SIZE);
928+
}
929+
930+
self.xmp_metadata = Some(xmp_metadata);
931+
} else if let Some(rest) = self.ext.data.strip_prefix(EXT_NAME_ICC) {
932+
self.icc_profile = Some(rest.to_vec());
933+
}
934+
None
935+
}
936+
907937
fn add_frame(&mut self) {
908938
if self.current.is_none() {
909939
self.current = Some(Frame::default());

src/reader/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,12 @@ where
479479
self.decoder.decoder.xmp_metadata()
480480
}
481481

482+
/// ICC profile stored in the image.
483+
#[inline]
484+
pub fn icc_profile(&self) -> Option<&[u8]> {
485+
self.decoder.decoder.icc_profile()
486+
}
487+
482488
/// Abort decoding and recover the `io::Read` instance
483489
pub fn into_inner(self) -> io::BufReader<R> {
484490
self.decoder.into_inner()

tests/decode.rs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
#![cfg(feature = "std")]
22

3-
use gif::{DecodeOptions, Decoder, DisposalMethod, Encoder, Frame};
4-
use std::fs::File;
3+
use gif::{
4+
streaming_decoder::{Decoded, OutputBuffer, StreamingDecoder},
5+
DecodeOptions, Decoder, DisposalMethod, Encoder, Frame,
6+
};
7+
use std::{fs::File, io::BufRead};
58

69
#[test]
710
fn test_simple_indexed() {
@@ -269,3 +272,38 @@ fn xmp_metadata() {
269272

270273
assert_eq!(decoder.xmp_metadata(), Some(EXPECTED_METADATA.as_bytes()))
271274
}
275+
276+
#[test]
277+
fn icc_profile() {
278+
let image: &[u8] = include_bytes!("samples/beacon_icc.gif");
279+
let icc_profile: &[u8] = include_bytes!("samples/profile.icc");
280+
let decoder = DecodeOptions::new().read_info(image).unwrap();
281+
282+
assert_eq!(decoder.icc_profile(), Some(icc_profile))
283+
}
284+
285+
#[test]
286+
fn check_last_extension_returns() {
287+
let mut buf: &[u8] = include_bytes!("samples/beacon_xmp.gif");
288+
289+
let mut out_buf = Vec::new();
290+
let mut output = OutputBuffer::Vec(&mut out_buf);
291+
292+
let mut decoder = StreamingDecoder::new();
293+
294+
loop {
295+
let (consumed, result) = {
296+
if buf.is_empty() {
297+
break;
298+
}
299+
300+
decoder.update(&mut buf, &mut output).unwrap()
301+
};
302+
buf.consume(consumed);
303+
if let Decoded::HeaderEnd = result {
304+
break;
305+
}
306+
}
307+
308+
assert_eq!(decoder.last_ext().1.len(), 3048);
309+
}

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_icc.gif: 2462153529
34
tests/samples/beacon_xmp.gif: 2462153529
45
tests/samples/beacon.gif: 2462153529
56
tests/samples/interlaced.gif: 2115495372

tests/samples/beacon_icc.gif

589 Bytes
Loading

tests/samples/profile.icc

474 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)