Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for parsing the sBIT chunk in the decoder #524

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/chunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,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 @@ -480,6 +480,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 @@ -524,6 +526,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
119 changes: 118 additions & 1 deletion src/decoder/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,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 @@ -205,6 +209,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 @@ -296,6 +310,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 @@ -313,6 +328,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 @@ -952,6 +977,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 @@ -1081,6 +1107,74 @@ impl StreamingDecoder {
}
}

fn parse_sbit(&mut self) -> Result<Decoded, DecodingError> {
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)
}

fn parse_trns(&mut self) -> Result<Decoded, DecodingError> {
let info = self.info.as_mut().unwrap();
if info.trns.is_some() {
Expand Down Expand Up @@ -1127,7 +1221,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 @@ -1580,6 +1674,7 @@ mod tests {
use crate::test_utils::*;
use crate::{Decoder, DecodingError, Reader};
use byteorder::WriteBytesExt;
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::VecDeque;
use std::fs::File;
Expand Down Expand Up @@ -1814,6 +1909,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.
Loading