extern crate crc32fast; use std::convert::From; use std::default::Default; use std::error; use std::fmt; use std::io; use std::{borrow::Cow, cmp::min}; use crc32fast::Hasher as Crc32; use super::zlib::ZlibStream; use crate::chunk::{self, ChunkType, IDAT, IEND, IHDR}; use crate::common::{ AnimationControl, BitDepth, BlendOp, ColorType, DisposeOp, FrameControl, Info, ParameterError, PixelDimensions, ScaledFloat, SourceChromaticities, Unit, }; use crate::text_metadata::{ITXtChunk, TEXtChunk, TextDecodingError, ZTXtChunk}; use crate::traits::ReadBytesExt; /// TODO check if these size are reasonable pub const CHUNCK_BUFFER_SIZE: usize = 32 * 1024; /// Determines if checksum checks should be disabled globally. /// /// This is used only in fuzzing. `afl` automatically adds `--cfg fuzzing` to RUSTFLAGS which can /// be used to detect that build. const CHECKSUM_DISABLED: bool = cfg!(fuzzing); #[derive(Debug)] enum U32Value { // CHUNKS Length, Type(u32), Crc(ChunkType), } #[derive(Debug)] enum State { Signature(u8, [u8; 7]), U32Byte3(U32Value, u32), U32Byte2(U32Value, u32), U32Byte1(U32Value, u32), U32(U32Value), ReadChunk(ChunkType), PartialChunk(ChunkType), DecodeData(ChunkType, usize), } #[derive(Debug)] /// Result of the decoding process pub enum Decoded { /// Nothing decoded yet Nothing, Header(u32, u32, BitDepth, ColorType, bool), ChunkBegin(u32, ChunkType), ChunkComplete(u32, ChunkType), PixelDimensions(PixelDimensions), AnimationControl(AnimationControl), FrameControl(FrameControl), /// Decoded raw image data. ImageData, /// The last of a consecutive chunk of IDAT was done. /// This is distinct from ChunkComplete which only marks that some IDAT chunk was completed but /// not that no additional IDAT chunk follows. ImageDataFlushed, PartialChunk(ChunkType), ImageEnd, } /// Any kind of error during PNG decoding. /// /// This enumeration provides a very rough analysis on the origin of the failure. That is, each /// variant corresponds to one kind of actor causing the error. It should not be understood as a /// direct blame but can inform the search for a root cause or if such a search is required. #[derive(Debug)] pub enum DecodingError { /// An error in IO of the underlying reader. IoError(io::Error), /// The input image was not a valid PNG. /// /// There isn't a lot that can be done here, except if the program itself was responsible for /// creating this image then investigate the generator. This is internally implemented with a /// large Enum. If You are interested in accessing some of the more exact information on the /// variant then we can discuss in an issue. Format(FormatError), /// An interface was used incorrectly. /// /// This is used in cases where it's expected that the programmer might trip up and stability /// could be affected. For example when: /// /// * The decoder is polled for more animation frames despite being done (or not being animated /// in the first place). /// * The output buffer does not have the required size. /// /// As a rough guideline for introducing new variants parts of the requirements are dynamically /// derived from the (untrusted) input data while the other half is from the caller. In the /// above cases the number of frames respectively the size is determined by the file while the /// number of calls /// /// If you're an application you might want to signal that a bug report is appreciated. Parameter(ParameterError), /// The image would have required exceeding the limits configured with the decoder. /// /// Note that Your allocations, e.g. when reading into a pre-allocated buffer, is __NOT__ /// considered part of the limits. Nevertheless, required intermediate buffers such as for /// singular lines is checked against the limit. /// /// Note that this is a best-effort basis. LimitsExceeded, } #[derive(Debug)] pub struct FormatError { inner: FormatErrorInner, } #[derive(Debug)] pub(crate) enum FormatErrorInner { /// Bad framing. CrcMismatch { /// Stored CRC32 value crc_val: u32, /// Calculated CRC32 sum crc_sum: u32, /// The chunk type that has the CRC mismatch. chunk: ChunkType, }, /// Not a PNG, the magic signature is missing. InvalidSignature, /// End of file, within a chunk event. UnexpectedEof, /// End of file, while expecting more image data. UnexpectedEndOfChunk, // Errors of chunk level ordering, missing etc. /// Ihdr must occur. MissingIhdr, /// Fctl must occur if an animated chunk occurs. MissingFctl, /// Image data that was indicated in IHDR or acTL is missing. MissingImageData, /// 4.3., Must be first. ChunkBeforeIhdr { kind: ChunkType, }, /// 4.3., some chunks must be before IDAT. AfterIdat { kind: ChunkType, }, /// 4.3., some chunks must be before PLTE. AfterPlte { kind: ChunkType, }, /// 4.3., some chunks must be between PLTE and IDAT. OutsidePlteIdat { kind: ChunkType, }, /// 4.3., some chunks must be unique. DuplicateChunk { kind: ChunkType, }, /// Specifically for fdat there is an embedded sequence number for chunks. ApngOrder { /// The sequence number in the chunk. present: u32, /// The one that should have been present. expected: u32, }, // Errors specific to particular chunk data to be validated. /// The palette did not even contain a single pixel data. ShortPalette { expected: usize, len: usize, }, /// A palletized image did not have a palette. PaletteRequired, /// The color-depth combination is not valid according to Table 11.1. InvalidColorBitDepth { color_type: ColorType, bit_depth: BitDepth, }, ColorWithBadTrns(ColorType), InvalidBitDepth(u8), InvalidColorType(u8), InvalidDisposeOp(u8), InvalidBlendOp(u8), InvalidUnit(u8), /// The rendering intent of the sRGB chunk is invalid. InvalidSrgbRenderingIntent(u8), UnknownCompressionMethod(u8), UnknownFilterMethod(u8), UnknownInterlaceMethod(u8), /// The subframe is not in bounds of the image. /// TODO: fields with relevant data. BadSubFrameBounds {}, // Errors specific to the IDAT/fDAT chunks. /// The compression of the data stream was faulty. CorruptFlateStream { err: fdeflate::DecompressionError, }, /// The image data chunk was too short for the expected pixel count. NoMoreImageData, /// Bad text encoding BadTextEncoding(TextDecodingError), } impl error::Error for DecodingError { fn cause(&self) -> Option<&(dyn error::Error + 'static)> { match self { DecodingError::IoError(err) => Some(err), _ => None, } } } impl fmt::Display for DecodingError { fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { use self::DecodingError::*; match self { IoError(err) => write!(fmt, "{}", err), Parameter(desc) => write!(fmt, "{}", &desc), Format(desc) => write!(fmt, "{}", desc), LimitsExceeded => write!(fmt, "limits are exceeded"), } } } impl fmt::Display for FormatError { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { use FormatErrorInner::*; match &self.inner { CrcMismatch { crc_val, crc_sum, chunk, .. } => write!( fmt, "CRC error: expected 0x{:x} have 0x{:x} while decoding {:?} chunk.", crc_val, crc_sum, chunk ), MissingIhdr => write!(fmt, "IHDR chunk missing"), MissingFctl => write!(fmt, "fcTL chunk missing before fdAT chunk."), 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), AfterPlte { kind } => write!(fmt, "Chunk {:?} is invalid after PLTE chunk.", kind), OutsidePlteIdat { kind } => write!( fmt, "Chunk {:?} must appear between PLTE and IDAT chunks.", kind ), DuplicateChunk { kind } => write!(fmt, "Chunk {:?} must appear at most once.", kind), ApngOrder { present, expected } => write!( fmt, "Sequence is not in order, expected #{} got #{}.", expected, present, ), ShortPalette { expected, len } => write!( fmt, "Not enough palette entries, expect {} got {}.", expected, len ), PaletteRequired => write!(fmt, "Missing palette of indexed image."), InvalidColorBitDepth { color_type, bit_depth, } => write!( fmt, "Invalid color/depth combination in header: {:?}/{:?}", color_type, bit_depth, ), ColorWithBadTrns(color_type) => write!( fmt, "Transparency chunk found for color type {:?}.", color_type ), InvalidBitDepth(nr) => write!(fmt, "Invalid dispose operation {}.", nr), InvalidColorType(nr) => write!(fmt, "Invalid color type {}.", nr), InvalidDisposeOp(nr) => write!(fmt, "Invalid dispose op {}.", nr), InvalidBlendOp(nr) => write!(fmt, "Invalid blend op {}.", nr), InvalidUnit(nr) => write!(fmt, "Invalid physical pixel size unit {}.", nr), InvalidSrgbRenderingIntent(nr) => write!(fmt, "Invalid sRGB rendering intent {}.", nr), UnknownCompressionMethod(nr) => write!(fmt, "Unknown compression method {}.", nr), UnknownFilterMethod(nr) => write!(fmt, "Unknown filter method {}.", nr), UnknownInterlaceMethod(nr) => write!(fmt, "Unknown interlace method {}.", nr), BadSubFrameBounds {} => write!(fmt, "Sub frame is out-of-bounds."), InvalidSignature => write!(fmt, "Invalid PNG signature."), UnexpectedEof => write!(fmt, "Unexpected end of data before image end."), UnexpectedEndOfChunk => write!(fmt, "Unexpected end of data within a chunk."), NoMoreImageData => write!(fmt, "IDAT or fDAT chunk is has not enough data for image."), CorruptFlateStream { err } => { write!(fmt, "Corrupt deflate stream. ")?; write!(fmt, "{:?}", err) } // TODO: Wrap more info in the enum variant BadTextEncoding(tde) => { match tde { TextDecodingError::Unrepresentable => { write!(fmt, "Unrepresentable data in tEXt chunk.") } TextDecodingError::InvalidKeywordSize => { write!(fmt, "Keyword empty or longer than 79 bytes.") } TextDecodingError::MissingNullSeparator => { write!(fmt, "No null separator in tEXt chunk.") } TextDecodingError::InflationError => { write!(fmt, "Invalid compressed text data.") } TextDecodingError::OutOfDecompressionSpace => { write!(fmt, "Out of decompression space. Try with a larger limit.") } TextDecodingError::InvalidCompressionMethod => { write!(fmt, "Using an unrecognized byte as compression method.") } TextDecodingError::InvalidCompressionFlag => { write!(fmt, "Using a flag that is not 0 or 255 as a compression flag for iTXt chunk.") } TextDecodingError::MissingCompressionFlag => { write!(fmt, "No compression flag in the iTXt chunk.") } } } } } } impl From for DecodingError { fn from(err: io::Error) -> DecodingError { DecodingError::IoError(err) } } impl From for DecodingError { fn from(err: FormatError) -> DecodingError { DecodingError::Format(err) } } impl From for FormatError { fn from(inner: FormatErrorInner) -> Self { FormatError { inner } } } impl From for io::Error { fn from(err: DecodingError) -> io::Error { match err { DecodingError::IoError(err) => err, err => io::Error::new(io::ErrorKind::Other, err.to_string()), } } } impl From for DecodingError { fn from(tbe: TextDecodingError) -> Self { DecodingError::Format(FormatError { inner: FormatErrorInner::BadTextEncoding(tbe), }) } } /// Decoder configuration options #[derive(Clone)] pub struct DecodeOptions { ignore_adler32: bool, ignore_crc: bool, ignore_text_chunk: bool, } impl Default for DecodeOptions { fn default() -> Self { Self { ignore_adler32: true, ignore_crc: false, ignore_text_chunk: false, } } } impl DecodeOptions { /// When set, the decoder will not compute and verify the Adler-32 checksum. /// /// Defaults to `true`. pub fn set_ignore_adler32(&mut self, ignore_adler32: bool) { self.ignore_adler32 = ignore_adler32; } /// When set, the decoder will not compute and verify the CRC code. /// /// Defaults to `false`. pub fn set_ignore_crc(&mut self, ignore_crc: bool) { self.ignore_crc = ignore_crc; } /// Flag to ignore computing and verifying the Adler-32 checksum and CRC /// code. pub fn set_ignore_checksums(&mut self, ignore_checksums: bool) { self.ignore_adler32 = ignore_checksums; self.ignore_crc = ignore_checksums; } /// Ignore text chunks while decoding. /// /// Defaults to `false`. pub fn set_ignore_text_chunk(&mut self, ignore_text_chunk: bool) { self.ignore_text_chunk = ignore_text_chunk; } } /// PNG StreamingDecoder (low-level interface) /// /// By default, the decoder does not verify Adler-32 checksum computation. To /// enable checksum verification, set it with [`StreamingDecoder::set_ignore_adler32`] /// before starting decompression. pub struct StreamingDecoder { state: Option, current_chunk: ChunkState, /// The inflater state handling consecutive `IDAT` and `fdAT` chunks. inflater: ZlibStream, /// The complete image info read from all prior chunks. pub(crate) info: Option>, /// The animation chunk sequence number. current_seq_no: Option, /// Stores where in decoding an `fdAT` chunk we are. apng_seq_handled: bool, have_idat: bool, decode_options: DecodeOptions, } struct ChunkState { /// The type of the current chunk. /// Relevant for `IDAT` and `fdAT` which aggregate consecutive chunks of their own type. type_: ChunkType, /// Partial crc until now. crc: Crc32, /// Remaining bytes to be read. remaining: u32, /// Non-decoded bytes in the chunk. raw_bytes: Vec, } impl StreamingDecoder { /// Creates a new StreamingDecoder /// /// Allocates the internal buffers. pub fn new() -> StreamingDecoder { StreamingDecoder::new_with_options(DecodeOptions::default()) } pub fn new_with_options(decode_options: DecodeOptions) -> StreamingDecoder { let mut inflater = ZlibStream::new(); inflater.set_ignore_adler32(decode_options.ignore_adler32); StreamingDecoder { state: Some(State::Signature(0, [0; 7])), current_chunk: ChunkState::default(), inflater, info: None, current_seq_no: None, apng_seq_handled: false, have_idat: false, decode_options, } } /// Resets the StreamingDecoder pub fn reset(&mut self) { self.state = Some(State::Signature(0, [0; 7])); self.current_chunk.crc = Crc32::new(); self.current_chunk.remaining = 0; self.current_chunk.raw_bytes.clear(); self.inflater.reset(); self.info = None; self.current_seq_no = None; self.apng_seq_handled = false; self.have_idat = false; } /// Provides access to the inner `info` field pub fn info(&self) -> Option<&Info<'static>> { self.info.as_ref() } pub fn set_ignore_text_chunk(&mut self, ignore_text_chunk: bool) { self.decode_options.set_ignore_text_chunk(ignore_text_chunk); } /// Return whether the decoder is set to ignore the Adler-32 checksum. pub fn ignore_adler32(&self) -> bool { self.inflater.ignore_adler32() } /// Set whether to compute and verify the Adler-32 checksum during /// decompression. Return `true` if the flag was successfully set. /// /// The decoder defaults to `true`. /// /// This flag cannot be modified after decompression has started until the /// [`StreamingDecoder`] is reset. pub fn set_ignore_adler32(&mut self, ignore_adler32: bool) -> bool { self.inflater.set_ignore_adler32(ignore_adler32) } /// Set whether to compute and verify the Adler-32 checksum during /// decompression. /// /// The decoder defaults to `false`. pub fn set_ignore_crc(&mut self, ignore_crc: bool) { self.decode_options.set_ignore_crc(ignore_crc) } /// Low level StreamingDecoder interface. /// /// Allows to stream partial data to the encoder. Returns a tuple containing the bytes that have /// been consumed from the input buffer and the current decoding result. If the decoded chunk /// was an image data chunk, it also appends the read data to `image_data`. pub fn update( &mut self, mut buf: &[u8], image_data: &mut Vec, ) -> Result<(usize, Decoded), DecodingError> { let len = buf.len(); while !buf.is_empty() && self.state.is_some() { match self.next_state(buf, image_data) { Ok((bytes, Decoded::Nothing)) => buf = &buf[bytes..], Ok((bytes, result)) => { buf = &buf[bytes..]; return Ok((len - buf.len(), result)); } Err(err) => return Err(err), } } Ok((len - buf.len(), Decoded::Nothing)) } fn next_state<'a>( &'a mut self, buf: &[u8], image_data: &mut Vec, ) -> Result<(usize, Decoded), DecodingError> { use self::State::*; let current_byte = buf[0]; // Driver should ensure that state is never None let state = self.state.take().unwrap(); match state { Signature(i, mut signature) if i < 7 => { signature[i as usize] = current_byte; self.state = Some(Signature(i + 1, signature)); Ok((1, Decoded::Nothing)) } Signature(_, signature) if signature == [137, 80, 78, 71, 13, 10, 26] && current_byte == 10 => { self.state = Some(U32(U32Value::Length)); Ok((1, Decoded::Nothing)) } Signature(..) => Err(DecodingError::Format( FormatErrorInner::InvalidSignature.into(), )), U32Byte3(type_, mut val) => { use self::U32Value::*; val |= u32::from(current_byte); match type_ { Length => { self.state = Some(U32(Type(val))); Ok((1, Decoded::Nothing)) } Type(length) => { let type_str = ChunkType([ (val >> 24) as u8, (val >> 16) as u8, (val >> 8) as u8, val as u8, ]); if type_str != self.current_chunk.type_ && (self.current_chunk.type_ == IDAT || self.current_chunk.type_ == chunk::fdAT) { self.current_chunk.type_ = type_str; self.inflater.finish_compressed_chunks(image_data)?; self.inflater.reset(); self.state = Some(U32Byte3(Type(length), val & !0xff)); return Ok((0, Decoded::ImageDataFlushed)); } self.current_chunk.type_ = type_str; if !self.decode_options.ignore_crc { self.current_chunk.crc.reset(); self.current_chunk.crc.update(&type_str.0); } self.current_chunk.remaining = length; self.apng_seq_handled = false; self.current_chunk.raw_bytes.clear(); self.state = Some(ReadChunk(type_str)); Ok((1, Decoded::ChunkBegin(length, type_str))) } Crc(type_str) => { // If ignore_crc is set, do not calculate CRC. We set // sum=val so that it short-circuits to true in the next // if-statement block let sum = if self.decode_options.ignore_crc { val } else { self.current_chunk.crc.clone().finalize() }; if val == sum || CHECKSUM_DISABLED { self.state = Some(State::U32(U32Value::Length)); if type_str == IEND { Ok((1, Decoded::ImageEnd)) } else { Ok((1, Decoded::ChunkComplete(val, type_str))) } } else { Err(DecodingError::Format( FormatErrorInner::CrcMismatch { crc_val: val, crc_sum: sum, chunk: type_str, } .into(), )) } } } } U32Byte2(type_, val) => { self.state = Some(U32Byte3(type_, val | u32::from(current_byte) << 8)); Ok((1, Decoded::Nothing)) } U32Byte1(type_, val) => { self.state = Some(U32Byte2(type_, val | u32::from(current_byte) << 16)); Ok((1, Decoded::Nothing)) } U32(type_) => { self.state = Some(U32Byte1(type_, u32::from(current_byte) << 24)); Ok((1, Decoded::Nothing)) } PartialChunk(type_str) => { match type_str { IDAT => { self.have_idat = true; self.state = Some(DecodeData(type_str, 0)); Ok((0, Decoded::PartialChunk(type_str))) } chunk::fdAT => { let data_start; if let Some(seq_no) = self.current_seq_no { if !self.apng_seq_handled { data_start = 4; let mut buf = &self.current_chunk.raw_bytes[..]; let next_seq_no = buf.read_be()?; if next_seq_no != seq_no + 1 { return Err(DecodingError::Format( FormatErrorInner::ApngOrder { present: next_seq_no, expected: seq_no + 1, } .into(), )); } self.current_seq_no = Some(next_seq_no); self.apng_seq_handled = true; } else { data_start = 0; } } else { return Err(DecodingError::Format( FormatErrorInner::MissingFctl.into(), )); } self.state = Some(DecodeData(type_str, data_start)); Ok((0, Decoded::PartialChunk(type_str))) } // Handle other chunks _ => { if self.current_chunk.remaining == 0 { // complete chunk Ok((0, self.parse_chunk(type_str)?)) } else { // Make sure we have room to read more of the chunk. // We need it fully before parsing. self.reserve_current_chunk()?; self.state = Some(ReadChunk(type_str)); Ok((0, Decoded::PartialChunk(type_str))) } } } } ReadChunk(type_str) => { // The _previous_ event wanted to return the contents of raw_bytes, and let the // caller consume it, if self.current_chunk.remaining == 0 { self.state = Some(U32(U32Value::Crc(type_str))); Ok((0, Decoded::Nothing)) } else { let ChunkState { crc, remaining, raw_bytes, type_: _, } = &mut self.current_chunk; let buf_avail = raw_bytes.capacity() - raw_bytes.len(); let bytes_avail = min(buf.len(), buf_avail); let n = min(*remaining, bytes_avail as u32); if buf_avail == 0 { self.state = Some(PartialChunk(type_str)); Ok((0, Decoded::Nothing)) } else { let buf = &buf[..n as usize]; if !self.decode_options.ignore_crc { crc.update(buf); } raw_bytes.extend_from_slice(buf); *remaining -= n; if *remaining == 0 { self.state = Some(PartialChunk(type_str)); } else { self.state = Some(ReadChunk(type_str)); } Ok((n as usize, Decoded::Nothing)) } } } DecodeData(type_str, mut n) => { let chunk_len = self.current_chunk.raw_bytes.len(); let chunk_data = &self.current_chunk.raw_bytes[n..]; let c = self.inflater.decompress(chunk_data, image_data)?; n += c; if n == chunk_len && c == 0 { self.current_chunk.raw_bytes.clear(); self.state = Some(ReadChunk(type_str)); Ok((0, Decoded::ImageData)) } else { self.state = Some(DecodeData(type_str, n)); Ok((0, Decoded::ImageData)) } } } } fn reserve_current_chunk(&mut self) -> Result<(), DecodingError> { // FIXME: use limits, also do so in iccp/zlib decompression. const MAX: usize = 0x10_0000; let buffer = &mut self.current_chunk.raw_bytes; // Double if necessary, but no more than until the limit is reached. let reserve_size = MAX.saturating_sub(buffer.capacity()).min(buffer.len()); buffer.reserve_exact(reserve_size); if buffer.capacity() == buffer.len() { Err(DecodingError::LimitsExceeded) } else { Ok(()) } } fn parse_chunk(&mut self, type_str: ChunkType) -> Result { self.state = Some(State::U32(U32Value::Crc(type_str))); if self.info.is_none() && type_str != IHDR { return Err(DecodingError::Format( FormatErrorInner::ChunkBeforeIhdr { kind: type_str }.into(), )); } match match type_str { IHDR => self.parse_ihdr(), chunk::PLTE => self.parse_plte(), chunk::tRNS => self.parse_trns(), chunk::pHYs => self.parse_phys(), chunk::gAMA => self.parse_gama(), chunk::acTL => self.parse_actl(), chunk::fcTL => self.parse_fctl(), chunk::cHRM => self.parse_chrm(), chunk::sRGB => self.parse_srgb(), chunk::iCCP => self.parse_iccp(), chunk::tEXt if !self.decode_options.ignore_text_chunk => self.parse_text(), chunk::zTXt if !self.decode_options.ignore_text_chunk => self.parse_ztxt(), chunk::iTXt if !self.decode_options.ignore_text_chunk => self.parse_itxt(), _ => Ok(Decoded::PartialChunk(type_str)), } { Err(err) => { // Borrow of self ends here, because Decoding error does not borrow self. self.state = None; Err(err) } ok => ok, } } fn parse_fctl(&mut self) -> Result { let mut buf = &self.current_chunk.raw_bytes[..]; let next_seq_no = buf.read_be()?; // Assuming that fcTL is required before *every* fdAT-sequence self.current_seq_no = Some(if let Some(seq_no) = self.current_seq_no { if next_seq_no != seq_no + 1 { return Err(DecodingError::Format( FormatErrorInner::ApngOrder { expected: seq_no + 1, present: next_seq_no, } .into(), )); } next_seq_no } else { if next_seq_no != 0 { return Err(DecodingError::Format( FormatErrorInner::ApngOrder { expected: 0, present: next_seq_no, } .into(), )); } 0 }); self.inflater.reset(); let fc = FrameControl { sequence_number: next_seq_no, width: buf.read_be()?, height: buf.read_be()?, x_offset: buf.read_be()?, y_offset: buf.read_be()?, delay_num: buf.read_be()?, delay_den: buf.read_be()?, dispose_op: { let dispose_op = buf.read_be()?; match DisposeOp::from_u8(dispose_op) { Some(dispose_op) => dispose_op, None => { return Err(DecodingError::Format( FormatErrorInner::InvalidDisposeOp(dispose_op).into(), )) } } }, blend_op: { let blend_op = buf.read_be()?; match BlendOp::from_u8(blend_op) { Some(blend_op) => blend_op, None => { return Err(DecodingError::Format( FormatErrorInner::InvalidBlendOp(blend_op).into(), )) } } }, }; self.info.as_ref().unwrap().validate(&fc)?; self.info.as_mut().unwrap().frame_control = Some(fc); Ok(Decoded::FrameControl(fc)) } fn parse_actl(&mut self) -> Result { if self.have_idat { Err(DecodingError::Format( FormatErrorInner::AfterIdat { kind: chunk::acTL }.into(), )) } else { let mut buf = &self.current_chunk.raw_bytes[..]; let actl = AnimationControl { num_frames: buf.read_be()?, num_plays: buf.read_be()?, }; self.info.as_mut().unwrap().animation_control = Some(actl); Ok(Decoded::AnimationControl(actl)) } } fn parse_plte(&mut self) -> Result { let info = self.info.as_mut().unwrap(); if info.palette.is_some() { // Only one palette is allowed Err(DecodingError::Format( FormatErrorInner::DuplicateChunk { kind: chunk::PLTE }.into(), )) } else { info.palette = Some(Cow::Owned(self.current_chunk.raw_bytes.clone())); Ok(Decoded::Nothing) } } fn parse_trns(&mut self) -> Result { let info = self.info.as_mut().unwrap(); if info.trns.is_some() { return Err(DecodingError::Format( FormatErrorInner::DuplicateChunk { kind: chunk::PLTE }.into(), )); } let (color_type, bit_depth) = { (info.color_type, info.bit_depth as u8) }; let mut vec = self.current_chunk.raw_bytes.clone(); let len = vec.len(); match color_type { ColorType::Grayscale => { if len < 2 { return Err(DecodingError::Format( FormatErrorInner::ShortPalette { expected: 2, len }.into(), )); } if bit_depth < 16 { vec[0] = vec[1]; vec.truncate(1); } info.trns = Some(Cow::Owned(vec)); Ok(Decoded::Nothing) } ColorType::Rgb => { if len < 6 { return Err(DecodingError::Format( FormatErrorInner::ShortPalette { expected: 6, len }.into(), )); } if bit_depth < 16 { vec[0] = vec[1]; vec[1] = vec[3]; vec[2] = vec[5]; vec.truncate(3); } info.trns = Some(Cow::Owned(vec)); Ok(Decoded::Nothing) } ColorType::Indexed => { // The transparency chunk must be after the palette chunk and // before the data chunk. if info.palette.is_none() { return Err(DecodingError::Format( FormatErrorInner::AfterPlte { kind: chunk::tRNS }.into(), )); } else if self.have_idat { return Err(DecodingError::Format( FormatErrorInner::OutsidePlteIdat { kind: chunk::tRNS }.into(), )); } info.trns = Some(Cow::Owned(vec)); Ok(Decoded::Nothing) } c => Err(DecodingError::Format( FormatErrorInner::ColorWithBadTrns(c).into(), )), } } fn parse_phys(&mut self) -> Result { let info = self.info.as_mut().unwrap(); if self.have_idat { Err(DecodingError::Format( FormatErrorInner::AfterIdat { kind: chunk::pHYs }.into(), )) } else if info.pixel_dims.is_some() { Err(DecodingError::Format( FormatErrorInner::DuplicateChunk { kind: chunk::pHYs }.into(), )) } else { let mut buf = &self.current_chunk.raw_bytes[..]; let xppu = buf.read_be()?; let yppu = buf.read_be()?; let unit = buf.read_be()?; let unit = match Unit::from_u8(unit) { Some(unit) => unit, None => { return Err(DecodingError::Format( FormatErrorInner::InvalidUnit(unit).into(), )) } }; let pixel_dims = PixelDimensions { xppu, yppu, unit }; info.pixel_dims = Some(pixel_dims); Ok(Decoded::PixelDimensions(pixel_dims)) } } fn parse_chrm(&mut self) -> Result { let info = self.info.as_mut().unwrap(); if self.have_idat { Err(DecodingError::Format( FormatErrorInner::AfterIdat { kind: chunk::cHRM }.into(), )) } else if info.chrm_chunk.is_some() { Err(DecodingError::Format( FormatErrorInner::DuplicateChunk { kind: chunk::cHRM }.into(), )) } else { let mut buf = &self.current_chunk.raw_bytes[..]; let white_x: u32 = buf.read_be()?; let white_y: u32 = buf.read_be()?; let red_x: u32 = buf.read_be()?; let red_y: u32 = buf.read_be()?; let green_x: u32 = buf.read_be()?; let green_y: u32 = buf.read_be()?; let blue_x: u32 = buf.read_be()?; let blue_y: u32 = buf.read_be()?; let source_chromaticities = SourceChromaticities { white: ( ScaledFloat::from_scaled(white_x), ScaledFloat::from_scaled(white_y), ), red: ( ScaledFloat::from_scaled(red_x), ScaledFloat::from_scaled(red_y), ), green: ( ScaledFloat::from_scaled(green_x), ScaledFloat::from_scaled(green_y), ), blue: ( ScaledFloat::from_scaled(blue_x), ScaledFloat::from_scaled(blue_y), ), }; info.chrm_chunk = Some(source_chromaticities); // Ignore chromaticities if sRGB profile is used. if info.srgb.is_none() { info.source_chromaticities = Some(source_chromaticities); } Ok(Decoded::Nothing) } } fn parse_gama(&mut self) -> Result { let info = self.info.as_mut().unwrap(); if self.have_idat { Err(DecodingError::Format( FormatErrorInner::AfterIdat { kind: chunk::gAMA }.into(), )) } else if info.gama_chunk.is_some() { Err(DecodingError::Format( FormatErrorInner::DuplicateChunk { kind: chunk::gAMA }.into(), )) } else { let mut buf = &self.current_chunk.raw_bytes[..]; let source_gamma: u32 = buf.read_be()?; let source_gamma = ScaledFloat::from_scaled(source_gamma); info.gama_chunk = Some(source_gamma); // Ignore chromaticities if sRGB profile is used. if info.srgb.is_none() { info.source_gamma = Some(source_gamma); } Ok(Decoded::Nothing) } } fn parse_srgb(&mut self) -> Result { let info = self.info.as_mut().unwrap(); if self.have_idat { Err(DecodingError::Format( FormatErrorInner::AfterIdat { kind: chunk::acTL }.into(), )) } else if info.srgb.is_some() { Err(DecodingError::Format( FormatErrorInner::DuplicateChunk { kind: chunk::sRGB }.into(), )) } else { let mut buf = &self.current_chunk.raw_bytes[..]; let raw: u8 = buf.read_be()?; // BE is is nonsense for single bytes, but this way the size is checked. let rendering_intent = crate::SrgbRenderingIntent::from_raw(raw).ok_or_else(|| { FormatError::from(FormatErrorInner::InvalidSrgbRenderingIntent(raw)) })?; // Set srgb and override source gamma and chromaticities. info.srgb = Some(rendering_intent); info.source_gamma = Some(crate::srgb::substitute_gamma()); info.source_chromaticities = Some(crate::srgb::substitute_chromaticities()); Ok(Decoded::Nothing) } } fn parse_iccp(&mut self) -> Result { let info = self.info.as_mut().unwrap(); if self.have_idat { Err(DecodingError::Format( FormatErrorInner::AfterIdat { kind: chunk::iCCP }.into(), )) } else if info.icc_profile.is_some() { Err(DecodingError::Format( FormatErrorInner::DuplicateChunk { kind: chunk::iCCP }.into(), )) } else { let mut buf = &self.current_chunk.raw_bytes[..]; // read profile name let _: u8 = buf.read_be()?; for _ in 1..80 { let raw: u8 = buf.read_be()?; if raw == 0 { break; } } match buf.read_be()? { // compression method 0u8 => (), n => { return Err(DecodingError::Format( FormatErrorInner::UnknownCompressionMethod(n).into(), )) } } let mut profile = Vec::new(); let mut inflater = ZlibStream::new(); while !buf.is_empty() { let consumed_bytes = inflater.decompress(buf, &mut profile)?; if profile.len() > 8000000 { // TODO: this should use Limits.bytes return Err(DecodingError::LimitsExceeded); } buf = &buf[consumed_bytes..]; } inflater.finish_compressed_chunks(&mut profile)?; info.icc_profile = Some(Cow::Owned(profile)); Ok(Decoded::Nothing) } } fn parse_ihdr(&mut self) -> Result { if self.info.is_some() { return Err(DecodingError::Format( FormatErrorInner::DuplicateChunk { kind: IHDR }.into(), )); } let mut buf = &self.current_chunk.raw_bytes[..]; let width = buf.read_be()?; let height = buf.read_be()?; let bit_depth = buf.read_be()?; let bit_depth = match BitDepth::from_u8(bit_depth) { Some(bits) => bits, None => { return Err(DecodingError::Format( FormatErrorInner::InvalidBitDepth(bit_depth).into(), )) } }; let color_type = buf.read_be()?; let color_type = match ColorType::from_u8(color_type) { Some(color_type) => { if color_type.is_combination_invalid(bit_depth) { return Err(DecodingError::Format( FormatErrorInner::InvalidColorBitDepth { color_type, bit_depth, } .into(), )); } else { color_type } } None => { return Err(DecodingError::Format( FormatErrorInner::InvalidColorType(color_type).into(), )) } }; match buf.read_be()? { // compression method 0u8 => (), n => { return Err(DecodingError::Format( FormatErrorInner::UnknownCompressionMethod(n).into(), )) } } match buf.read_be()? { // filter method 0u8 => (), n => { return Err(DecodingError::Format( FormatErrorInner::UnknownFilterMethod(n).into(), )) } } let interlaced = match buf.read_be()? { 0u8 => false, 1 => true, n => { return Err(DecodingError::Format( FormatErrorInner::UnknownInterlaceMethod(n).into(), )) } }; self.info = Some(Info { width, height, bit_depth, color_type, interlaced, ..Default::default() }); Ok(Decoded::Header( width, height, bit_depth, color_type, interlaced, )) } fn split_keyword(buf: &[u8]) -> Result<(&[u8], &[u8]), DecodingError> { let null_byte_index = buf .iter() .position(|&b| b == 0) .ok_or_else(|| DecodingError::from(TextDecodingError::MissingNullSeparator))?; if null_byte_index == 0 || null_byte_index > 79 { return Err(DecodingError::from(TextDecodingError::InvalidKeywordSize)); } Ok((&buf[..null_byte_index], &buf[null_byte_index + 1..])) } fn parse_text(&mut self) -> Result { let buf = &self.current_chunk.raw_bytes[..]; let (keyword_slice, value_slice) = Self::split_keyword(buf)?; self.info .as_mut() .unwrap() .uncompressed_latin1_text .push(TEXtChunk::decode(keyword_slice, value_slice).map_err(DecodingError::from)?); Ok(Decoded::Nothing) } fn parse_ztxt(&mut self) -> Result { let buf = &self.current_chunk.raw_bytes[..]; let (keyword_slice, value_slice) = Self::split_keyword(buf)?; let compression_method = *value_slice .first() .ok_or_else(|| DecodingError::from(TextDecodingError::InvalidCompressionMethod))?; let text_slice = &value_slice[1..]; self.info.as_mut().unwrap().compressed_latin1_text.push( ZTXtChunk::decode(keyword_slice, compression_method, text_slice) .map_err(DecodingError::from)?, ); Ok(Decoded::Nothing) } fn parse_itxt(&mut self) -> Result { let buf = &self.current_chunk.raw_bytes[..]; let (keyword_slice, value_slice) = Self::split_keyword(buf)?; let compression_flag = *value_slice .first() .ok_or_else(|| DecodingError::from(TextDecodingError::MissingCompressionFlag))?; let compression_method = *value_slice .get(1) .ok_or_else(|| DecodingError::from(TextDecodingError::InvalidCompressionMethod))?; let second_null_byte_index = value_slice[2..] .iter() .position(|&b| b == 0) .ok_or_else(|| DecodingError::from(TextDecodingError::MissingNullSeparator))? + 2; let language_tag_slice = &value_slice[2..second_null_byte_index]; let third_null_byte_index = value_slice[second_null_byte_index + 1..] .iter() .position(|&b| b == 0) .ok_or_else(|| DecodingError::from(TextDecodingError::MissingNullSeparator))? + (second_null_byte_index + 1); let translated_keyword_slice = &value_slice[second_null_byte_index + 1..third_null_byte_index]; let text_slice = &value_slice[third_null_byte_index + 1..]; self.info.as_mut().unwrap().utf8_text.push( ITXtChunk::decode( keyword_slice, compression_flag, compression_method, language_tag_slice, translated_keyword_slice, text_slice, ) .map_err(DecodingError::from)?, ); Ok(Decoded::Nothing) } } impl Info<'_> { fn validate(&self, fc: &FrameControl) -> Result<(), DecodingError> { // Validate mathematically: fc.width + fc.x_offset <= self.width let in_x_bounds = Some(fc.width) <= self.width.checked_sub(fc.x_offset); // Validate mathematically: fc.height + fc.y_offset <= self.height let in_y_bounds = Some(fc.height) <= self.height.checked_sub(fc.y_offset); if !in_x_bounds || !in_y_bounds { return Err(DecodingError::Format( // TODO: do we want to display the bad bounds? FormatErrorInner::BadSubFrameBounds {}.into(), )); } Ok(()) } } impl Default for StreamingDecoder { fn default() -> Self { Self::new() } } impl Default for ChunkState { fn default() -> Self { ChunkState { type_: ChunkType([0; 4]), crc: Crc32::new(), remaining: 0, raw_bytes: Vec::with_capacity(CHUNCK_BUFFER_SIZE), } } } #[cfg(test)] mod tests { use super::ScaledFloat; use super::SourceChromaticities; use std::fs::File; #[test] fn image_gamma() -> Result<(), ()> { 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().source_gamma; assert!(actual == expected); } trial("tests/pngsuite/f00n0g08.png", None); trial("tests/pngsuite/f00n2c08.png", None); trial("tests/pngsuite/f01n0g08.png", None); trial("tests/pngsuite/f01n2c08.png", None); trial("tests/pngsuite/f02n0g08.png", None); trial("tests/pngsuite/f02n2c08.png", None); trial("tests/pngsuite/f03n0g08.png", None); trial("tests/pngsuite/f03n2c08.png", None); trial("tests/pngsuite/f04n0g08.png", None); trial("tests/pngsuite/f04n2c08.png", None); trial("tests/pngsuite/f99n0g04.png", None); trial("tests/pngsuite/tm3n3p02.png", None); trial("tests/pngsuite/g03n0g16.png", Some(ScaledFloat::new(0.35))); trial("tests/pngsuite/g03n2c08.png", Some(ScaledFloat::new(0.35))); trial("tests/pngsuite/g03n3p04.png", Some(ScaledFloat::new(0.35))); trial("tests/pngsuite/g04n0g16.png", Some(ScaledFloat::new(0.45))); trial("tests/pngsuite/g04n2c08.png", Some(ScaledFloat::new(0.45))); trial("tests/pngsuite/g04n3p04.png", Some(ScaledFloat::new(0.45))); trial("tests/pngsuite/g05n0g16.png", Some(ScaledFloat::new(0.55))); trial("tests/pngsuite/g05n2c08.png", Some(ScaledFloat::new(0.55))); trial("tests/pngsuite/g05n3p04.png", Some(ScaledFloat::new(0.55))); trial("tests/pngsuite/g07n0g16.png", Some(ScaledFloat::new(0.7))); trial("tests/pngsuite/g07n2c08.png", Some(ScaledFloat::new(0.7))); trial("tests/pngsuite/g07n3p04.png", Some(ScaledFloat::new(0.7))); trial("tests/pngsuite/g10n0g16.png", Some(ScaledFloat::new(1.0))); trial("tests/pngsuite/g10n2c08.png", Some(ScaledFloat::new(1.0))); trial("tests/pngsuite/g10n3p04.png", Some(ScaledFloat::new(1.0))); trial("tests/pngsuite/g25n0g16.png", Some(ScaledFloat::new(2.5))); trial("tests/pngsuite/g25n2c08.png", Some(ScaledFloat::new(2.5))); trial("tests/pngsuite/g25n3p04.png", Some(ScaledFloat::new(2.5))); Ok(()) } #[test] fn image_source_chromaticities() -> Result<(), ()> { 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().source_chromaticities; assert!(actual == expected); } trial( "tests/pngsuite/ccwn2c08.png", Some(SourceChromaticities::new( (0.3127, 0.3290), (0.64, 0.33), (0.30, 0.60), (0.15, 0.06), )), ); trial( "tests/pngsuite/ccwn3p08.png", Some(SourceChromaticities::new( (0.3127, 0.3290), (0.64, 0.33), (0.30, 0.60), (0.15, 0.06), )), ); trial("tests/pngsuite/basi0g01.png", None); trial("tests/pngsuite/basi0g02.png", None); trial("tests/pngsuite/basi0g04.png", None); trial("tests/pngsuite/basi0g08.png", None); trial("tests/pngsuite/basi0g16.png", None); trial("tests/pngsuite/basi2c08.png", None); trial("tests/pngsuite/basi2c16.png", None); trial("tests/pngsuite/basi3p01.png", None); trial("tests/pngsuite/basi3p02.png", None); trial("tests/pngsuite/basi3p04.png", None); trial("tests/pngsuite/basi3p08.png", None); trial("tests/pngsuite/basi4a08.png", None); trial("tests/pngsuite/basi4a16.png", None); trial("tests/pngsuite/basi6a08.png", None); trial("tests/pngsuite/basi6a16.png", None); trial("tests/pngsuite/basn0g01.png", None); trial("tests/pngsuite/basn0g02.png", None); trial("tests/pngsuite/basn0g04.png", None); trial("tests/pngsuite/basn0g08.png", None); trial("tests/pngsuite/basn0g16.png", None); trial("tests/pngsuite/basn2c08.png", None); trial("tests/pngsuite/basn2c16.png", None); trial("tests/pngsuite/basn3p01.png", None); trial("tests/pngsuite/basn3p02.png", None); trial("tests/pngsuite/basn3p04.png", None); trial("tests/pngsuite/basn3p08.png", None); trial("tests/pngsuite/basn4a08.png", None); trial("tests/pngsuite/basn4a16.png", None); trial("tests/pngsuite/basn6a08.png", None); trial("tests/pngsuite/basn6a16.png", None); trial("tests/pngsuite/bgai4a08.png", None); trial("tests/pngsuite/bgai4a16.png", None); trial("tests/pngsuite/bgan6a08.png", None); trial("tests/pngsuite/bgan6a16.png", None); trial("tests/pngsuite/bgbn4a08.png", None); trial("tests/pngsuite/bggn4a16.png", None); trial("tests/pngsuite/bgwn6a08.png", None); trial("tests/pngsuite/bgyn6a16.png", None); trial("tests/pngsuite/cdfn2c08.png", None); trial("tests/pngsuite/cdhn2c08.png", None); trial("tests/pngsuite/cdsn2c08.png", None); trial("tests/pngsuite/cdun2c08.png", None); trial("tests/pngsuite/ch1n3p04.png", None); trial("tests/pngsuite/ch2n3p08.png", None); trial("tests/pngsuite/cm0n0g04.png", None); trial("tests/pngsuite/cm7n0g04.png", None); trial("tests/pngsuite/cm9n0g04.png", None); trial("tests/pngsuite/cs3n2c16.png", None); trial("tests/pngsuite/cs3n3p08.png", None); trial("tests/pngsuite/cs5n2c08.png", None); trial("tests/pngsuite/cs5n3p08.png", None); trial("tests/pngsuite/cs8n2c08.png", None); trial("tests/pngsuite/cs8n3p08.png", None); trial("tests/pngsuite/ct0n0g04.png", None); trial("tests/pngsuite/ct1n0g04.png", None); trial("tests/pngsuite/cten0g04.png", None); trial("tests/pngsuite/ctfn0g04.png", None); trial("tests/pngsuite/ctgn0g04.png", None); trial("tests/pngsuite/cthn0g04.png", None); trial("tests/pngsuite/ctjn0g04.png", None); trial("tests/pngsuite/ctzn0g04.png", None); trial("tests/pngsuite/f00n0g08.png", None); trial("tests/pngsuite/f00n2c08.png", None); trial("tests/pngsuite/f01n0g08.png", None); trial("tests/pngsuite/f01n2c08.png", None); trial("tests/pngsuite/f02n0g08.png", None); trial("tests/pngsuite/f02n2c08.png", None); trial("tests/pngsuite/f03n0g08.png", None); trial("tests/pngsuite/f03n2c08.png", None); trial("tests/pngsuite/f04n0g08.png", None); trial("tests/pngsuite/f04n2c08.png", None); trial("tests/pngsuite/f99n0g04.png", None); trial("tests/pngsuite/g03n0g16.png", None); trial("tests/pngsuite/g03n2c08.png", None); trial("tests/pngsuite/g03n3p04.png", None); trial("tests/pngsuite/g04n0g16.png", None); trial("tests/pngsuite/g04n2c08.png", None); trial("tests/pngsuite/g04n3p04.png", None); trial("tests/pngsuite/g05n0g16.png", None); trial("tests/pngsuite/g05n2c08.png", None); trial("tests/pngsuite/g05n3p04.png", None); trial("tests/pngsuite/g07n0g16.png", None); trial("tests/pngsuite/g07n2c08.png", None); trial("tests/pngsuite/g07n3p04.png", None); trial("tests/pngsuite/g10n0g16.png", None); trial("tests/pngsuite/g10n2c08.png", None); trial("tests/pngsuite/g10n3p04.png", None); trial("tests/pngsuite/g25n0g16.png", None); trial("tests/pngsuite/g25n2c08.png", None); trial("tests/pngsuite/g25n3p04.png", None); trial("tests/pngsuite/oi1n0g16.png", None); trial("tests/pngsuite/oi1n2c16.png", None); trial("tests/pngsuite/oi2n0g16.png", None); trial("tests/pngsuite/oi2n2c16.png", None); trial("tests/pngsuite/oi4n0g16.png", None); trial("tests/pngsuite/oi4n2c16.png", None); trial("tests/pngsuite/oi9n0g16.png", None); trial("tests/pngsuite/oi9n2c16.png", None); trial("tests/pngsuite/PngSuite.png", None); trial("tests/pngsuite/pp0n2c16.png", None); trial("tests/pngsuite/pp0n6a08.png", None); trial("tests/pngsuite/ps1n0g08.png", None); trial("tests/pngsuite/ps1n2c16.png", None); trial("tests/pngsuite/ps2n0g08.png", None); trial("tests/pngsuite/ps2n2c16.png", None); trial("tests/pngsuite/s01i3p01.png", None); trial("tests/pngsuite/s01n3p01.png", None); trial("tests/pngsuite/s02i3p01.png", None); trial("tests/pngsuite/s02n3p01.png", None); trial("tests/pngsuite/s03i3p01.png", None); trial("tests/pngsuite/s03n3p01.png", None); trial("tests/pngsuite/s04i3p01.png", None); trial("tests/pngsuite/s04n3p01.png", None); trial("tests/pngsuite/s05i3p02.png", None); trial("tests/pngsuite/s05n3p02.png", None); trial("tests/pngsuite/s06i3p02.png", None); trial("tests/pngsuite/s06n3p02.png", None); trial("tests/pngsuite/s07i3p02.png", None); trial("tests/pngsuite/s07n3p02.png", None); trial("tests/pngsuite/s08i3p02.png", None); trial("tests/pngsuite/s08n3p02.png", None); trial("tests/pngsuite/s09i3p02.png", None); trial("tests/pngsuite/s09n3p02.png", None); trial("tests/pngsuite/s32i3p04.png", None); trial("tests/pngsuite/s32n3p04.png", None); trial("tests/pngsuite/s33i3p04.png", None); trial("tests/pngsuite/s33n3p04.png", None); trial("tests/pngsuite/s34i3p04.png", None); trial("tests/pngsuite/s34n3p04.png", None); trial("tests/pngsuite/s35i3p04.png", None); trial("tests/pngsuite/s35n3p04.png", None); trial("tests/pngsuite/s36i3p04.png", None); trial("tests/pngsuite/s36n3p04.png", None); trial("tests/pngsuite/s37i3p04.png", None); trial("tests/pngsuite/s37n3p04.png", None); trial("tests/pngsuite/s38i3p04.png", None); trial("tests/pngsuite/s38n3p04.png", None); trial("tests/pngsuite/s39i3p04.png", None); trial("tests/pngsuite/s39n3p04.png", None); trial("tests/pngsuite/s40i3p04.png", None); trial("tests/pngsuite/s40n3p04.png", None); trial("tests/pngsuite/tbbn0g04.png", None); trial("tests/pngsuite/tbbn2c16.png", None); trial("tests/pngsuite/tbbn3p08.png", None); trial("tests/pngsuite/tbgn2c16.png", None); trial("tests/pngsuite/tbgn3p08.png", None); trial("tests/pngsuite/tbrn2c08.png", None); trial("tests/pngsuite/tbwn0g16.png", None); trial("tests/pngsuite/tbwn3p08.png", None); trial("tests/pngsuite/tbyn3p08.png", None); trial("tests/pngsuite/tm3n3p02.png", None); trial("tests/pngsuite/tp0n0g08.png", None); trial("tests/pngsuite/tp0n2c08.png", None); trial("tests/pngsuite/tp0n3p08.png", None); trial("tests/pngsuite/tp1n3p08.png", None); trial("tests/pngsuite/z00n2c08.png", None); trial("tests/pngsuite/z03n2c08.png", None); trial("tests/pngsuite/z06n2c08.png", None); Ok(()) } }