Skip to content

Commit 85f4274

Browse files
1c3t3aShaddyDC
andcommitted
Add support for reading the ICC profile
Co-authored-by: Rasmus Piorr <piorr@google.com>
1 parent 975752e commit 85f4274

File tree

6 files changed

+72
-23
lines changed

6 files changed

+72
-23
lines changed

src/reader/decoder.rs

Lines changed: 55 additions & 22 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,33 +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-
777-
// the parser removes sub-block lenghts, so app name and data are concatenated
778-
if let Some(&[first, second, ..]) = self.ext.data.strip_prefix(EXT_NAME_NETSCAPE) {
779-
let repeat = u16::from(first) | u16::from(second) << 8;
780-
goto!(BlockEnd, emit Decoded::Repetitions(if repeat == 0 { Repeat::Infinite } else { Repeat::Finite(repeat) }))
781-
} else if let Some(mut rest) = self.ext.data.strip_prefix(EXT_NAME_XMP) {
782-
// XMP adds a "ramp" of 257 bytes to the end of the metadata to let the "pascal-strings"
783-
// parser converge to the null byte. The ramp looks like "0x01, 0xff, .., 0x01, 0x00".
784-
// For convenience and to allow consumers to not be bothered with this implementation detail,
785-
// we cut the ramp.
786-
const RAMP_SIZE: usize = 257;
787-
if rest.len() >= RAMP_SIZE
788-
&& rest.ends_with(&[0x03, 0x02, 0x01, 0x00])
789-
&& rest[rest.len() - RAMP_SIZE..].starts_with(&[0x01, 0x0ff])
790-
{
791-
rest = &rest[..(rest.len() - RAMP_SIZE)];
792-
}
793-
794-
self.xmp_metadata = Some(rest.to_vec());
795-
goto!(BlockEnd)
789+
if let Some(decoded) = self.read_application_extension() {
790+
goto!(BlockEnd, emit decoded)
796791
} else {
797792
goto!(BlockEnd)
798793
}
@@ -901,6 +896,44 @@ impl StreamingDecoder {
901896
Ok(())
902897
}
903898

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+
904937
fn add_frame(&mut self) {
905938
if self.current.is_none() {
906939
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: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,15 @@ fn xmp_metadata() {
273273
assert_eq!(decoder.xmp_metadata(), Some(EXPECTED_METADATA.as_bytes()))
274274
}
275275

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+
276285
#[test]
277286
fn check_last_extension_returns() {
278287
let mut buf: &[u8] = include_bytes!("samples/beacon_xmp.gif");
@@ -296,5 +305,5 @@ fn check_last_extension_returns() {
296305
}
297306
}
298307

299-
assert_eq!(decoder.last_ext().1.len(), 3129);
308+
assert_eq!(decoder.last_ext().1.len(), 3048);
300309
}

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)