Skip to content

Commit

Permalink
Merge pull request #524 from thirumurugan-git/impl-sbit-chunk
Browse files Browse the repository at this point in the history
Add support for parsing the sBIT chunk in the decoder
  • Loading branch information
kornelski authored Nov 16, 2024
2 parents a31e67a + 194605e commit a8bf9fb
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/chunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 --

Expand Down
3 changes: 3 additions & 0 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Cow<'a, [u8]>>,
/// The image's `tRNS` chunk, if present; contains the alpha channel of the image's palette, 1 byte per entry.
pub trns: Option<Cow<'a, [u8]>>,
pub pixel_dims: Option<PixelDimensions>,
Expand Down Expand Up @@ -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,
Expand Down
123 changes: 122 additions & 1 deletion src/decoder/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -1083,6 +1109,78 @@ impl StreamingDecoder {
}
}

fn parse_sbit(&mut self) -> Result<Decoded, DecodingError> {
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<Decoded, DecodingError> {
let info = self.info.as_mut().unwrap();
if info.trns.is_some() {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1941,6 +2040,28 @@ mod tests {
Ok(())
}

#[test]
fn image_source_sbit() {
fn trial(path: &str, expected: Option<Cow<[u8]>>) {
let decoder = crate::Decoder::new(File::open(path).unwrap());
let reader = decoder.read_info().unwrap();
let actual: Option<Cow<[u8]>> = 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]
Expand Down
Binary file added tests/sbit/g.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/sbit/ga.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/sbit/indexed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/sbit/rgb.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/sbit/rgba.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit a8bf9fb

Please sign in to comment.