diff --git a/src/chunk.rs b/src/chunk.rs index b83ce54c..3e5c13f5 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -49,6 +49,8 @@ pub const tEXt: ChunkType = ChunkType(*b"tEXt"); pub const zTXt: ChunkType = ChunkType(*b"zTXt"); /// UTF-8 textual data pub const iTXt: ChunkType = ChunkType(*b"iTXt"); +// Significant bits +pub const sBIT: ChunkType = ChunkType(*b"sBIT"); // -- Extension chunks -- diff --git a/src/common.rs b/src/common.rs index 3d9dc13b..73e7e23b 100644 --- a/src/common.rs +++ b/src/common.rs @@ -569,6 +569,8 @@ pub struct Info<'a> { /// How colors are stored in the image. pub color_type: ColorType, pub interlaced: bool, + /// The image's `sBIT` chunk, if present; contains significant bits of the sample. + pub sbit: Option>, /// The image's `tRNS` chunk, if present; contains the alpha channel of the image's palette, 1 byte per entry. pub trns: Option>, pub pixel_dims: Option, @@ -621,6 +623,7 @@ impl Default for Info<'_> { color_type: ColorType::Grayscale, interlaced: false, palette: None, + sbit: None, trns: None, gama_chunk: None, chrm_chunk: None, diff --git a/src/decoder/stream.rs b/src/decoder/stream.rs index d0b7cb5c..974d98dc 100644 --- a/src/decoder/stream.rs +++ b/src/decoder/stream.rs @@ -179,6 +179,10 @@ pub(crate) enum FormatErrorInner { AfterIdat { kind: ChunkType, }, + // 4.3., Some chunks must be after PLTE. + BeforePlte { + kind: ChunkType, + }, /// 4.3., some chunks must be before PLTE. AfterPlte { kind: ChunkType, @@ -204,6 +208,16 @@ pub(crate) enum FormatErrorInner { expected: usize, len: usize, }, + /// sBIT chunk size based on color type. + InvalidSbitChunkSize { + color_type: ColorType, + expected: usize, + len: usize, + }, + InvalidSbit { + sample_depth: BitDepth, + sbit: u8, + }, /// A palletized image did not have a palette. PaletteRequired, /// The color-depth combination is not valid according to Table 11.1. @@ -294,6 +308,7 @@ impl fmt::Display for FormatError { MissingImageData => write!(fmt, "IDAT or fdAT chunk is missing."), ChunkBeforeIhdr { kind } => write!(fmt, "{:?} chunk appeared before IHDR chunk", kind), AfterIdat { kind } => write!(fmt, "Chunk {:?} is invalid after IDAT chunk.", kind), + BeforePlte { kind } => write!(fmt, "Chunk {:?} is invalid before PLTE chunk.", kind), AfterPlte { kind } => write!(fmt, "Chunk {:?} is invalid after PLTE chunk.", kind), OutsidePlteIdat { kind } => write!( fmt, @@ -311,6 +326,16 @@ impl fmt::Display for FormatError { "Not enough palette entries, expect {} got {}.", expected, len ), + InvalidSbitChunkSize {color_type, expected, len} => write!( + fmt, + "The size of the sBIT chunk should be {} byte(s), but {} byte(s) were provided for the {:?} color type.", + expected, len, color_type + ), + InvalidSbit {sample_depth, sbit} => write!( + fmt, + "Invalid sBIT value {}. It must be greater than zero and less than the sample depth {:?}.", + sbit, sample_depth + ), PaletteRequired => write!(fmt, "Missing palette of indexed image."), InvalidDimensions => write!(fmt, "Invalid image dimensions"), InvalidColorBitDepth { @@ -951,6 +976,7 @@ impl StreamingDecoder { self.state = Some(State::new_u32(U32ValueKind::Crc(type_str))); let parse_result = match type_str { IHDR => self.parse_ihdr(), + chunk::sBIT => self.parse_sbit(), chunk::PLTE => self.parse_plte(), chunk::tRNS => self.parse_trns(), chunk::pHYs => self.parse_phys(), @@ -1083,6 +1109,78 @@ impl StreamingDecoder { } } + fn parse_sbit(&mut self) -> Result { + let mut parse = || { + let info = self.info.as_mut().unwrap(); + if info.palette.is_some() { + return Err(DecodingError::Format( + FormatErrorInner::AfterPlte { kind: chunk::sBIT }.into(), + )); + } + + if self.have_idat { + return Err(DecodingError::Format( + FormatErrorInner::AfterIdat { kind: chunk::sBIT }.into(), + )); + } + + if info.sbit.is_some() { + return Err(DecodingError::Format( + FormatErrorInner::DuplicateChunk { kind: chunk::sBIT }.into(), + )); + } + + let (color_type, bit_depth) = { (info.color_type, info.bit_depth) }; + // The sample depth for color type 3 is fixed at eight bits. + let sample_depth = if color_type == ColorType::Indexed { + BitDepth::Eight + } else { + bit_depth + }; + self.limits + .reserve_bytes(self.current_chunk.raw_bytes.len())?; + let vec = self.current_chunk.raw_bytes.clone(); + let len = vec.len(); + + // expected lenth of the chunk + let expected = match color_type { + ColorType::Grayscale => 1, + ColorType::Rgb | ColorType::Indexed => 3, + ColorType::GrayscaleAlpha => 2, + ColorType::Rgba => 4, + }; + + // Check if the sbit chunk size is valid. + if expected != len { + return Err(DecodingError::Format( + FormatErrorInner::InvalidSbitChunkSize { + color_type, + expected, + len, + } + .into(), + )); + } + + for sbit in &vec { + if *sbit < 1 || *sbit > sample_depth as u8 { + return Err(DecodingError::Format( + FormatErrorInner::InvalidSbit { + sample_depth, + sbit: *sbit, + } + .into(), + )); + } + } + info.sbit = Some(Cow::Owned(vec)); + Ok(Decoded::Nothing) + }; + + parse().ok(); + Ok(Decoded::Nothing) + } + fn parse_trns(&mut self) -> Result { let info = self.info.as_mut().unwrap(); if info.trns.is_some() { @@ -1129,7 +1227,7 @@ impl StreamingDecoder { // before the data chunk. if info.palette.is_none() { return Err(DecodingError::Format( - FormatErrorInner::AfterPlte { kind: chunk::tRNS }.into(), + FormatErrorInner::BeforePlte { kind: chunk::tRNS }.into(), )); } else if self.have_idat { return Err(DecodingError::Format( @@ -1707,6 +1805,7 @@ mod tests { use crate::{Decoder, DecodingError, Reader}; use approx::assert_relative_eq; use byteorder::WriteBytesExt; + use std::borrow::Cow; use std::cell::RefCell; use std::collections::VecDeque; use std::fs::File; @@ -1941,6 +2040,28 @@ mod tests { Ok(()) } + #[test] + fn image_source_sbit() { + fn trial(path: &str, expected: Option>) { + let decoder = crate::Decoder::new(File::open(path).unwrap()); + let reader = decoder.read_info().unwrap(); + let actual: Option> = reader.info().sbit.clone(); + assert!(actual == expected); + } + + trial("tests/sbit/g.png", Some(Cow::Owned(vec![5u8]))); + trial("tests/sbit/ga.png", Some(Cow::Owned(vec![5u8, 3u8]))); + trial( + "tests/sbit/indexed.png", + Some(Cow::Owned(vec![5u8, 6u8, 5u8])), + ); + trial("tests/sbit/rgb.png", Some(Cow::Owned(vec![5u8, 6u8, 5u8]))); + trial( + "tests/sbit/rgba.png", + Some(Cow::Owned(vec![5u8, 6u8, 5u8, 8u8])), + ); + } + /// Test handling of a PNG file that contains *two* iCCP chunks. /// This is a regression test for https://github.com/image-rs/image/issues/1825. #[test] diff --git a/tests/sbit/g.png b/tests/sbit/g.png new file mode 100644 index 00000000..9b7c4229 Binary files /dev/null and b/tests/sbit/g.png differ diff --git a/tests/sbit/ga.png b/tests/sbit/ga.png new file mode 100644 index 00000000..6917409c Binary files /dev/null and b/tests/sbit/ga.png differ diff --git a/tests/sbit/indexed.png b/tests/sbit/indexed.png new file mode 100644 index 00000000..d66b1b04 Binary files /dev/null and b/tests/sbit/indexed.png differ diff --git a/tests/sbit/rgb.png b/tests/sbit/rgb.png new file mode 100644 index 00000000..a5009c45 Binary files /dev/null and b/tests/sbit/rgb.png differ diff --git a/tests/sbit/rgba.png b/tests/sbit/rgba.png new file mode 100644 index 00000000..16e26431 Binary files /dev/null and b/tests/sbit/rgba.png differ