Skip to content

Commit

Permalink
Fix iCCP chunk encoding (#548)
Browse files Browse the repository at this point in the history
  • Loading branch information
kornelski authored Dec 9, 2024
1 parent 2232f83 commit 3ef4af6
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 5 deletions.
2 changes: 1 addition & 1 deletion src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,7 @@ impl Info<'_> {
chrms.encode(&mut w)?;
}
if let Some(iccp) = &self.icc_profile {
encoder::write_chunk(&mut w, chunk::iCCP, iccp)?;
encoder::write_iccp_chunk(&mut w, "_", iccp)?
}
}

Expand Down
23 changes: 21 additions & 2 deletions src/decoder/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1528,9 +1528,11 @@ impl StreamingDecoder {
let mut buf = &self.current_chunk.raw_bytes[..];

// read profile name
let _: u8 = buf.read_be()?;
for _ in 1..80 {
for len in 0..=80 {
let raw: u8 = buf.read_be()?;
if (raw == 0 && len == 0) || (raw != 0 && len == 80) {
return Err(DecodingError::from(TextDecodingError::InvalidKeywordSize));
}
if raw == 0 {
break;
}
Expand Down Expand Up @@ -2108,6 +2110,23 @@ mod tests {
assert_eq!(4070462061, crc32fast::hash(&icc_profile));
}

#[test]
fn test_iccp_roundtrip() {
let dummy_icc = b"I'm a profile";

let mut info = crate::Info::with_size(1, 1);
info.icc_profile = Some(dummy_icc.into());
let mut encoded_image = Vec::new();
let enc = crate::Encoder::with_info(&mut encoded_image, info).unwrap();
let mut enc = enc.write_header().unwrap();
enc.write_image_data(&[0]).unwrap();
enc.finish().unwrap();

let dec = crate::Decoder::new(encoded_image.as_slice());
let dec = dec.read_info().unwrap();
assert_eq!(dummy_icc, &**dec.info().icc_profile.as_ref().unwrap());
}

#[test]
fn test_png_with_broken_iccp() {
let decoder = crate::Decoder::new(File::open("tests/iccp/broken_iccp.png").unwrap());
Expand Down
32 changes: 31 additions & 1 deletion src/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::common::{
};
use crate::filter::{filter, AdaptiveFilterType, FilterType};
use crate::text_metadata::{
EncodableTextChunk, ITXtChunk, TEXtChunk, TextEncodingError, ZTXtChunk,
encode_iso_8859_1, EncodableTextChunk, ITXtChunk, TEXtChunk, TextEncodingError, ZTXtChunk,
};
use crate::traits::WriteBytesExt;

Expand Down Expand Up @@ -1040,6 +1040,36 @@ impl<W: Write> Drop for Writer<W> {
}
}

// This should be moved to Writer after `Info::encoding` is gone
pub(crate) fn write_iccp_chunk<W: Write>(
w: &mut W,
profile_name: &str,
icc_profile: &[u8],
) -> Result<()> {
let profile_name = encode_iso_8859_1(profile_name)?;
if profile_name.len() < 1 || profile_name.len() > 79 {
return Err(TextEncodingError::InvalidKeywordSize.into());
}

let estimated_compressed_size = icc_profile.len() * 3 / 4;
let chunk_size = profile_name
.len()
.checked_add(2) // string NUL + compression type. Checked add optimizes out later Vec reallocations.
.and_then(|s| s.checked_add(estimated_compressed_size))
.ok_or(EncodingError::LimitsExceeded)?;

let mut data = Vec::new();
data.try_reserve_exact(chunk_size)
.map_err(|_| EncodingError::LimitsExceeded)?;

data.extend(profile_name.into_iter().chain([0, 0]));

let mut encoder = ZlibEncoder::new(data, flate2::Compression::default());
encoder.write_all(icc_profile)?;

write_chunk(w, chunk::iCCP, &encoder.finish()?)
}

enum ChunkOutput<'a, W: Write> {
Borrowed(&'a mut Writer<W>),
Owned(Writer<W>),
Expand Down
2 changes: 1 addition & 1 deletion src/text_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ fn decode_iso_8859_1(text: &[u8]) -> String {
text.iter().map(|&b| b as char).collect()
}

fn encode_iso_8859_1(text: &str) -> Result<Vec<u8>, TextEncodingError> {
pub(crate) fn encode_iso_8859_1(text: &str) -> Result<Vec<u8>, TextEncodingError> {
encode_iso_8859_1_iter(text).collect()
}

Expand Down

0 comments on commit 3ef4af6

Please sign in to comment.