Skip to content

Commit 2b632be

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

File tree

6 files changed

+72
-20
lines changed

6 files changed

+72
-20
lines changed

src/reader/decoder.rs

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub const PLTE_CHANNELS: usize = 3;
2121
/// Headers for supported extensions.
2222
const EXT_NAME_NETSCAPE: &[u8] = b"\x0bNETSCAPE2.0\x03";
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.
@@ -470,6 +473,7 @@ impl StreamingDecoder {
470473
current: None,
471474
header_end_reached: false,
472475
xmp_metadata: None,
476+
icc_profile: None,
473477
}
474478
}
475479

@@ -542,6 +546,12 @@ impl StreamingDecoder {
542546
self.xmp_metadata.as_deref()
543547
}
544548

549+
/// ICC profile stored in the image.
550+
#[must_use]
551+
pub fn icc_profile(&self) -> Option<&[u8]> {
552+
self.icc_profile.as_deref()
553+
}
554+
545555
/// The version number of the GIF standard used in this image.
546556
///
547557
/// We suppose a minimum of `V87a` compatibility. This value will be reported until we have
@@ -773,26 +783,8 @@ impl StreamingDecoder {
773783
}
774784
ApplicationExtension => {
775785
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)
786+
if let Some(decoded) = self.read_application_extension() {
787+
goto!(BlockEnd, emit decoded)
796788
} else {
797789
goto!(BlockEnd)
798790
}
@@ -901,6 +893,50 @@ impl StreamingDecoder {
901893
Ok(())
902894
}
903895

896+
fn read_application_extension(&mut self) -> Option<Decoded> {
897+
// the parser removes sub-block lenghts, so app name and data are concatenated
898+
if let Some(rest) = self.ext.data.strip_prefix(EXT_NAME_NETSCAPE) {
899+
if rest.len() != 3 {
900+
return None;
901+
}
902+
let repeat = u16::from(rest[0]) | u16::from(rest[1]) << 8;
903+
return Some(Decoded::Repetitions(if repeat == 0 {
904+
Repeat::Infinite
905+
} else {
906+
Repeat::Finite(repeat)
907+
}));
908+
} else if let Some(mut rest) = self.ext.data.strip_prefix(EXT_NAME_XMP) {
909+
// XMP adds a "ramp" of 257 bytes to the end of the metadata to let the "pascal-strings"
910+
// parser converge to the null byte. The ramp looks like "0x01, 0xff, .., 0x01, 0x00".
911+
// For convenience and to allow consumers to not be bothered with this implementation detail,
912+
// we cut the ramp.
913+
const RAMP_SIZE: usize = 257;
914+
if rest.len() >= RAMP_SIZE
915+
&& rest.ends_with(&[0x03, 0x02, 0x01, 0x00])
916+
&& rest[rest.len() - RAMP_SIZE..].starts_with(&[0x01, 0x0ff])
917+
{
918+
rest = &rest[..(rest.len() - RAMP_SIZE)];
919+
}
920+
921+
self.xmp_metadata = Some(rest.to_vec());
922+
} else if let Some(mut rest) = self.ext.data.strip_prefix(EXT_NAME_ICC) {
923+
// The extension data always contains the length byte (see the XMP metadata extension
924+
// for why that is necessary), so we need to remove it again for the ICC profile.
925+
let mut icc_profile = Vec::new();
926+
loop {
927+
let block_length = *rest.first().unwrap_or(&0) as usize;
928+
if block_length == 0 {
929+
break;
930+
}
931+
let (subblock, tail) = rest.split_at(1 + block_length);
932+
icc_profile.extend_from_slice(&subblock[1..]);
933+
rest = tail;
934+
}
935+
self.icc_profile = Some(icc_profile);
936+
}
937+
None
938+
}
939+
904940
fn add_frame(&mut self) {
905941
if self.current.is_none() {
906942
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: 9 additions & 0 deletions
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");

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)