use oxiarc_core::Crc32;
use oxiarc_deflate::zlib_decompress_into;
const PNG_SIGNATURE: [u8; 8] = [0x89, b'P', b'N', b'G', b'\r', b'\n', 0x1a, b'\n'];
const MAX_DEFLATE_RATIO: usize = 1032;
const MAX_DIMENSION: u32 = 0x7fff_ffff;
const ADAM7_ROW_START: [u32; 7] = [0, 0, 4, 0, 2, 0, 1];
const ADAM7_COL_START: [u32; 7] = [0, 4, 0, 2, 0, 1, 0];
const ADAM7_ROW_STEP: [u32; 7] = [8, 8, 8, 4, 4, 2, 2];
const ADAM7_COL_STEP: [u32; 7] = [8, 8, 4, 4, 2, 2, 1];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PngImage {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PngDecodeError {
NotAPng,
Truncated {
offset: usize,
needed: usize,
},
InvalidHeader,
AppleCgBi,
ChecksumMismatch {
chunk: [u8; 4],
stored: u32,
computed: u32,
},
InvalidDimensions {
width: u32,
height: u32,
},
UnsupportedColorType(u8),
UnsupportedBitDepth {
color_type: u8,
bit_depth: u8,
},
UnsupportedCompressionMethod(u8),
UnsupportedFilterMethod(u8),
UnsupportedInterlaceMethod(u8),
MissingPalette,
InvalidPalette {
length: usize,
},
PaletteIndexOutOfRange {
index: usize,
palette_len: usize,
},
MissingImageData,
Decompression(String),
DataSize {
expected: usize,
actual: usize,
},
ExcessImageData {
expected: usize,
},
CompressionRatioExceeded {
expected: usize,
compressed: usize,
},
InvalidFilterType(u8),
ImageTooLarge {
width: u32,
height: u32,
},
}
impl core::fmt::Display for PngDecodeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NotAPng => write!(f, "data does not start with the PNG signature"),
Self::Truncated { offset, needed } => write!(
f,
"PNG data ends prematurely at byte {offset}: {needed} more byte(s) required"
),
Self::InvalidHeader => write!(f, "PNG IHDR chunk is missing or malformed"),
Self::AppleCgBi => write!(
f,
"file is Apple's proprietary CgBI PNG variant (premultiplied BGRA, \
zlib-wrapper-less deflate), not a standard PNG"
),
Self::ChecksumMismatch {
chunk,
stored,
computed,
} => {
let name = String::from_utf8_lossy(chunk).into_owned();
write!(
f,
"PNG chunk {name} CRC mismatch: stored {stored:#010x}, computed {computed:#010x}"
)
}
Self::InvalidDimensions { width, height } => write!(
f,
"invalid PNG dimensions {width}×{height}: each side must be 1..={MAX_DIMENSION}"
),
Self::UnsupportedColorType(ct) => {
write!(
f,
"unsupported PNG colour type {ct} (expected 0, 2, 3, 4 or 6)"
)
}
Self::UnsupportedBitDepth {
color_type,
bit_depth,
} => write!(
f,
"PNG bit depth {bit_depth} is not allowed for colour type {color_type}"
),
Self::UnsupportedCompressionMethod(m) => {
write!(f, "unsupported PNG compression method {m} (expected 0)")
}
Self::UnsupportedFilterMethod(m) => {
write!(f, "unsupported PNG filter method {m} (expected 0)")
}
Self::UnsupportedInterlaceMethod(m) => {
write!(f, "unsupported PNG interlace method {m} (expected 0 or 1)")
}
Self::MissingPalette => write!(f, "indexed-colour PNG has no PLTE chunk"),
Self::InvalidPalette { length } => write!(
f,
"PNG PLTE chunk length {length} is not a positive multiple of 3 (max 768)"
),
Self::PaletteIndexOutOfRange { index, palette_len } => write!(
f,
"PNG palette index {index} is out of range for a {palette_len}-entry PLTE"
),
Self::MissingImageData => write!(f, "PNG has no IDAT chunk"),
Self::Decompression(msg) => write!(f, "PNG inflate stage failed: {msg}"),
Self::DataSize { expected, actual } => write!(
f,
"PNG image data has {actual} bytes after inflate, expected at least {expected}"
),
Self::ExcessImageData { expected } => write!(
f,
"PNG image data inflates past the {expected} bytes its IHDR accounts for"
),
Self::CompressionRatioExceeded {
expected,
compressed,
} => write!(
f,
"PNG IHDR needs {expected} bytes of image data, unreachable from a \
{compressed}-byte IDAT payload (deflate expands at most {MAX_DEFLATE_RATIO}:1)"
),
Self::InvalidFilterType(t) => {
write!(
f,
"PNG scanline declares unknown filter type {t} (expected 0..=4)"
)
}
Self::ImageTooLarge { width, height } => {
write!(f, "PNG image {width}×{height} does not fit in memory")
}
}
}
}
impl std::error::Error for PngDecodeError {}
#[derive(Debug, Clone, Copy)]
struct Header {
width: u32,
height: u32,
bit_depth: u8,
color_type: u8,
interlace: u8,
}
impl Header {
const fn channels(self) -> usize {
match self.color_type {
0 | 3 => 1,
4 => 2,
2 => 3,
_ => 4,
}
}
const fn filter_unit(self) -> usize {
let bits = self.channels() * self.bit_depth as usize;
let bytes = bits / 8;
if bytes == 0 {
1
} else {
bytes
}
}
fn row_bytes(self, pixels_wide: u32) -> Option<usize> {
let bits = (pixels_wide as usize)
.checked_mul(self.channels())?
.checked_mul(self.bit_depth as usize)?;
Some(bits.div_ceil(8))
}
}
#[derive(Debug, Clone, Default)]
struct Transparency {
grey_key: Option<u16>,
rgb_key: Option<(u16, u16, u16)>,
palette_alpha: Vec<u8>,
}
pub fn decode_png_rgba8(data: &[u8]) -> Result<PngImage, PngDecodeError> {
if data.len() < PNG_SIGNATURE.len() || data[..PNG_SIGNATURE.len()] != PNG_SIGNATURE {
return Err(PngDecodeError::NotAPng);
}
let mut cursor = PNG_SIGNATURE.len();
let mut header: Option<Header> = None;
let mut palette: Vec<[u8; 3]> = Vec::new();
let mut trns_raw: Option<Vec<u8>> = None;
let mut idat: Vec<u8> = Vec::new();
let mut saw_idat = false;
while cursor < data.len() {
let (kind, payload, next) = read_chunk(data, cursor)?;
cursor = next;
match &kind {
b"IHDR" => {
if header.is_some() {
return Err(PngDecodeError::InvalidHeader);
}
header = Some(parse_header(payload)?);
}
b"PLTE" => {
if payload.is_empty() || payload.len() % 3 != 0 || payload.len() > 768 {
return Err(PngDecodeError::InvalidPalette {
length: payload.len(),
});
}
palette = payload
.chunks_exact(3)
.map(|c| [c[0], c[1], c[2]])
.collect();
}
b"tRNS" => trns_raw = Some(payload.to_vec()),
b"IDAT" => {
saw_idat = true;
idat.extend_from_slice(payload);
}
b"IEND" => break,
b"CgBI" => return Err(PngDecodeError::AppleCgBi),
_ => {}
}
if header.is_none() {
return Err(PngDecodeError::InvalidHeader);
}
}
let header = header.ok_or(PngDecodeError::InvalidHeader)?;
if !saw_idat {
return Err(PngDecodeError::MissingImageData);
}
if header.color_type == 3 && palette.is_empty() {
return Err(PngDecodeError::MissingPalette);
}
let transparency = parse_transparency(trns_raw.as_deref(), header, palette.len());
let raw = inflate_image_data(&idat, header)?;
decode_pixels(&raw, header, &palette, &transparency)
}
fn inflate_image_data(compressed: &[u8], header: Header) -> Result<Vec<u8>, PngDecodeError> {
let required = required_raw_len(header).ok_or(PngDecodeError::ImageTooLarge {
width: header.width,
height: header.height,
})?;
let ceiling = compressed.len().saturating_mul(MAX_DEFLATE_RATIO);
if required > ceiling {
return Err(PngDecodeError::CompressionRatioExceeded {
expected: required,
compressed: compressed.len(),
});
}
let mut raw = vec![0u8; required];
let written = zlib_decompress_into(compressed, &mut raw).map_err(|e| {
if matches!(e, oxiarc_core::OxiArcError::BufferTooSmall { .. }) {
PngDecodeError::ExcessImageData { expected: required }
} else {
PngDecodeError::Decompression(e.to_string())
}
})?;
if written < required {
return Err(PngDecodeError::DataSize {
expected: required,
actual: written,
});
}
Ok(raw)
}
fn read_chunk(data: &[u8], offset: usize) -> Result<([u8; 4], &[u8], usize), PngDecodeError> {
let need_header = 8usize;
if data.len() - offset < need_header {
return Err(PngDecodeError::Truncated {
offset,
needed: need_header - (data.len() - offset),
});
}
let mut len_bytes = [0u8; 4];
len_bytes.copy_from_slice(&data[offset..offset + 4]);
let length = u32::from_be_bytes(len_bytes) as usize;
let mut kind = [0u8; 4];
kind.copy_from_slice(&data[offset + 4..offset + 8]);
let payload_start = offset + 8;
let total = length
.checked_add(4)
.and_then(|t| payload_start.checked_add(t))
.ok_or(PngDecodeError::Truncated {
offset,
needed: length,
})?;
if total > data.len() {
return Err(PngDecodeError::Truncated {
offset,
needed: total - data.len(),
});
}
let payload = &data[payload_start..payload_start + length];
let mut stored_bytes = [0u8; 4];
stored_bytes.copy_from_slice(&data[payload_start + length..total]);
let stored = u32::from_be_bytes(stored_bytes);
let mut crc = Crc32::new();
crc.update(&kind);
crc.update(payload);
let computed = crc.value();
if computed != stored {
return Err(PngDecodeError::ChecksumMismatch {
chunk: kind,
stored,
computed,
});
}
Ok((kind, payload, total))
}
fn parse_header(payload: &[u8]) -> Result<Header, PngDecodeError> {
if payload.len() != 13 {
return Err(PngDecodeError::InvalidHeader);
}
let mut buf = [0u8; 4];
buf.copy_from_slice(&payload[0..4]);
let width = u32::from_be_bytes(buf);
buf.copy_from_slice(&payload[4..8]);
let height = u32::from_be_bytes(buf);
let bit_depth = payload[8];
let color_type = payload[9];
let compression = payload[10];
let filter = payload[11];
let interlace = payload[12];
if width == 0 || height == 0 || width > MAX_DIMENSION || height > MAX_DIMENSION {
return Err(PngDecodeError::InvalidDimensions { width, height });
}
if compression != 0 {
return Err(PngDecodeError::UnsupportedCompressionMethod(compression));
}
if filter != 0 {
return Err(PngDecodeError::UnsupportedFilterMethod(filter));
}
if interlace > 1 {
return Err(PngDecodeError::UnsupportedInterlaceMethod(interlace));
}
let depth_ok = match color_type {
0 => matches!(bit_depth, 1 | 2 | 4 | 8 | 16),
3 => matches!(bit_depth, 1 | 2 | 4 | 8),
2 | 4 | 6 => matches!(bit_depth, 8 | 16),
other => return Err(PngDecodeError::UnsupportedColorType(other)),
};
if !depth_ok {
return Err(PngDecodeError::UnsupportedBitDepth {
color_type,
bit_depth,
});
}
Ok(Header {
width,
height,
bit_depth,
color_type,
interlace,
})
}
fn parse_transparency(payload: Option<&[u8]>, header: Header, palette_len: usize) -> Transparency {
let mut out = Transparency::default();
let Some(payload) = payload else {
return out;
};
match header.color_type {
0 if payload.len() >= 2 => {
out.grey_key = Some(u16::from_be_bytes([payload[0], payload[1]]));
}
2 if payload.len() >= 6 => {
out.rgb_key = Some((
u16::from_be_bytes([payload[0], payload[1]]),
u16::from_be_bytes([payload[2], payload[3]]),
u16::from_be_bytes([payload[4], payload[5]]),
));
}
3 => {
let take = payload.len().min(palette_len);
out.palette_alpha = payload[..take].to_vec();
}
_ => {}
}
out
}
fn decode_pixels(
raw: &[u8],
header: Header,
palette: &[[u8; 3]],
trns: &Transparency,
) -> Result<PngImage, PngDecodeError> {
let too_large = PngDecodeError::ImageTooLarge {
width: header.width,
height: header.height,
};
let out_len = (header.width as usize)
.checked_mul(header.height as usize)
.and_then(|px| px.checked_mul(4))
.ok_or(too_large.clone())?;
let required = required_raw_len(header).ok_or(too_large)?;
if raw.len() < required {
return Err(PngDecodeError::DataSize {
expected: required,
actual: raw.len(),
});
}
let mut rgba = vec![0u8; out_len];
let mut consumed = 0usize;
if header.interlace == 0 {
expand_pass(
raw,
&mut consumed,
header,
palette,
trns,
PassGeometry {
width: header.width,
height: header.height,
col_start: 0,
col_step: 1,
row_start: 0,
row_step: 1,
},
&mut rgba,
)?;
} else {
for pass in 0..7 {
let geometry = adam7_geometry(header, pass);
if geometry.width == 0 || geometry.height == 0 {
continue;
}
expand_pass(
raw,
&mut consumed,
header,
palette,
trns,
geometry,
&mut rgba,
)?;
}
}
Ok(PngImage {
width: header.width,
height: header.height,
rgba,
})
}
#[derive(Debug, Clone, Copy)]
struct PassGeometry {
width: u32,
height: u32,
col_start: u32,
col_step: u32,
row_start: u32,
row_step: u32,
}
fn adam7_geometry(header: Header, pass: usize) -> PassGeometry {
let col_start = ADAM7_COL_START[pass];
let row_start = ADAM7_ROW_START[pass];
let col_step = ADAM7_COL_STEP[pass];
let row_step = ADAM7_ROW_STEP[pass];
let width = header.width.saturating_sub(col_start).div_ceil(col_step);
let height = header.height.saturating_sub(row_start).div_ceil(row_step);
PassGeometry {
width,
height,
col_start,
col_step,
row_start,
row_step,
}
}
fn required_raw_len(header: Header) -> Option<usize> {
if header.interlace == 0 {
header
.row_bytes(header.width)?
.checked_add(1)?
.checked_mul(header.height as usize)
} else {
let mut total = 0usize;
for pass in 0..7 {
let geometry = adam7_geometry(header, pass);
if geometry.width == 0 || geometry.height == 0 {
continue;
}
let pass_len = header
.row_bytes(geometry.width)?
.checked_add(1)?
.checked_mul(geometry.height as usize)?;
total = total.checked_add(pass_len)?;
}
Some(total)
}
}
fn expand_pass(
raw: &[u8],
consumed: &mut usize,
header: Header,
palette: &[[u8; 3]],
trns: &Transparency,
geometry: PassGeometry,
rgba: &mut [u8],
) -> Result<(), PngDecodeError> {
let too_large = PngDecodeError::ImageTooLarge {
width: header.width,
height: header.height,
};
let row_len = header.row_bytes(geometry.width).ok_or(too_large)?;
let filter_unit = header.filter_unit();
let mut previous = vec![0u8; row_len];
let mut current = vec![0u8; row_len];
for pass_y in 0..geometry.height {
let stride = row_len + 1;
if raw.len() - *consumed < stride {
return Err(PngDecodeError::DataSize {
expected: *consumed + stride,
actual: raw.len(),
});
}
let filter = raw[*consumed];
current.copy_from_slice(&raw[*consumed + 1..*consumed + stride]);
*consumed += stride;
unfilter_row(filter, &mut current, &previous, filter_unit)?;
let dest_y = geometry.row_start + pass_y * geometry.row_step;
for pass_x in 0..geometry.width {
let pixel = read_pixel(¤t, header, palette, trns, pass_x)?;
let dest_x = geometry.col_start + pass_x * geometry.col_step;
let idx = ((dest_y as usize) * (header.width as usize) + dest_x as usize) * 4;
rgba[idx..idx + 4].copy_from_slice(&pixel);
}
core::mem::swap(&mut previous, &mut current);
}
Ok(())
}
fn unfilter_row(
filter: u8,
current: &mut [u8],
previous: &[u8],
bpp: usize,
) -> Result<(), PngDecodeError> {
match filter {
0 => {}
1 => {
for i in bpp..current.len() {
current[i] = current[i].wrapping_add(current[i - bpp]);
}
}
2 => {
for (cur, up) in current.iter_mut().zip(previous.iter()) {
*cur = cur.wrapping_add(*up);
}
}
3 => {
for i in 0..current.len() {
let left = if i >= bpp {
u16::from(current[i - bpp])
} else {
0
};
let up = u16::from(previous[i]);
current[i] = current[i].wrapping_add(((left + up) / 2) as u8);
}
}
4 => {
for i in 0..current.len() {
let left = if i >= bpp { current[i - bpp] } else { 0 };
let up = previous[i];
let up_left = if i >= bpp { previous[i - bpp] } else { 0 };
current[i] = current[i].wrapping_add(paeth(left, up, up_left));
}
}
other => return Err(PngDecodeError::InvalidFilterType(other)),
}
Ok(())
}
fn paeth(a: u8, b: u8, c: u8) -> u8 {
let p = i16::from(a) + i16::from(b) - i16::from(c);
let pa = (p - i16::from(a)).abs();
let pb = (p - i16::from(b)).abs();
let pc = (p - i16::from(c)).abs();
if pa <= pb && pa <= pc {
a
} else if pb <= pc {
b
} else {
c
}
}
fn sample(row: &[u8], bit_depth: u8, index: usize) -> u16 {
match bit_depth {
16 => {
let byte = index * 2;
u16::from_be_bytes([row[byte], row[byte + 1]])
}
8 => u16::from(row[index]),
_ => {
let bits = bit_depth as usize;
let bit_pos = index * bits;
let byte = bit_pos / 8;
let shift = 8 - bits - (bit_pos % 8);
let mask = (1u16 << bits) - 1;
(u16::from(row[byte]) >> shift) & mask
}
}
}
fn scale_to_u8(value: u16, bit_depth: u8) -> u8 {
match bit_depth {
16 => (value >> 8) as u8,
8 => value as u8,
4 => (value * 17) as u8,
2 => (value * 85) as u8,
_ => (value * 255) as u8,
}
}
fn read_pixel(
row: &[u8],
header: Header,
palette: &[[u8; 3]],
trns: &Transparency,
x: u32,
) -> Result<[u8; 4], PngDecodeError> {
let depth = header.bit_depth;
let x = x as usize;
let channels = header.channels();
let base = x * channels;
let pixel = match header.color_type {
0 => {
let raw_grey = sample(row, depth, base);
let grey = scale_to_u8(raw_grey, depth);
let alpha = if trns.grey_key == Some(raw_grey) {
0
} else {
255
};
[grey, grey, grey, alpha]
}
2 => {
let raw_r = sample(row, depth, base);
let raw_g = sample(row, depth, base + 1);
let raw_b = sample(row, depth, base + 2);
let alpha = if trns.rgb_key == Some((raw_r, raw_g, raw_b)) {
0
} else {
255
};
[
scale_to_u8(raw_r, depth),
scale_to_u8(raw_g, depth),
scale_to_u8(raw_b, depth),
alpha,
]
}
3 => {
let index = sample(row, depth, base) as usize;
let entry = palette
.get(index)
.ok_or(PngDecodeError::PaletteIndexOutOfRange {
index,
palette_len: palette.len(),
})?;
let alpha = trns.palette_alpha.get(index).copied().unwrap_or(255);
[entry[0], entry[1], entry[2], alpha]
}
4 => {
let grey = scale_to_u8(sample(row, depth, base), depth);
let alpha = scale_to_u8(sample(row, depth, base + 1), depth);
[grey, grey, grey, alpha]
}
_ => [
scale_to_u8(sample(row, depth, base), depth),
scale_to_u8(sample(row, depth, base + 1), depth),
scale_to_u8(sample(row, depth, base + 2), depth),
scale_to_u8(sample(row, depth, base + 3), depth),
],
};
Ok(pixel)
}
#[cfg(test)]
mod tests {
use super::*;
use oxiarc_deflate::zlib_compress;
fn build_png(ihdr: [u8; 13], extra_chunks: &[(&[u8; 4], Vec<u8>)], raw: &[u8]) -> Vec<u8> {
fn chunk(out: &mut Vec<u8>, kind: &[u8; 4], data: &[u8]) {
let len = u32::try_from(data.len()).unwrap_or(u32::MAX);
out.extend_from_slice(&len.to_be_bytes());
out.extend_from_slice(kind);
out.extend_from_slice(data);
let mut crc = Crc32::new();
crc.update(kind);
crc.update(data);
out.extend_from_slice(&crc.value().to_be_bytes());
}
let mut out = Vec::new();
out.extend_from_slice(&PNG_SIGNATURE);
chunk(&mut out, b"IHDR", &ihdr);
for (kind, data) in extra_chunks {
chunk(&mut out, kind, data);
}
let compressed = zlib_compress(raw, 6).expect("zlib_compress of a test vector");
chunk(&mut out, b"IDAT", &compressed);
chunk(&mut out, b"IEND", &[]);
out
}
fn ihdr(width: u32, height: u32, bit_depth: u8, color_type: u8, interlace: u8) -> [u8; 13] {
let mut out = [0u8; 13];
out[..4].copy_from_slice(&width.to_be_bytes());
out[4..8].copy_from_slice(&height.to_be_bytes());
out[8] = bit_depth;
out[9] = color_type;
out[12] = interlace;
out
}
#[cfg(feature = "png-encode")]
use crate::png_encode::{encode_png, PngColorType};
#[cfg(feature = "png-encode")]
#[test]
fn round_trip_rgba8() {
let pixels: Vec<u8> = (0..(4 * 3 * 4)).map(|i| (i * 7 % 256) as u8).collect();
let png = encode_png(4, 3, PngColorType::Rgba8, &pixels).expect("encode");
let image = decode_png_rgba8(&png).expect("decode");
assert_eq!((image.width, image.height), (4, 3));
assert_eq!(image.rgba, pixels);
}
#[cfg(feature = "png-encode")]
#[test]
fn round_trip_rgb8_gains_opaque_alpha() {
let pixels = vec![10u8, 20, 30, 40, 50, 60];
let png = encode_png(2, 1, PngColorType::Rgb8, &pixels).expect("encode");
let image = decode_png_rgba8(&png).expect("decode");
assert_eq!(image.rgba, vec![10, 20, 30, 255, 40, 50, 60, 255]);
}
#[cfg(feature = "png-encode")]
#[test]
fn round_trip_grayscale8() {
let pixels = vec![0u8, 128, 255, 64];
let png = encode_png(2, 2, PngColorType::Grayscale8, &pixels).expect("encode");
let image = decode_png_rgba8(&png).expect("decode");
assert_eq!(
image.rgba,
vec![0, 0, 0, 255, 128, 128, 128, 255, 255, 255, 255, 255, 64, 64, 64, 255]
);
}
#[cfg(feature = "png-encode")]
#[test]
fn round_trip_grayscale_alpha8() {
let pixels = vec![200u8, 10, 30, 255];
let png = encode_png(2, 1, PngColorType::GrayscaleAlpha8, &pixels).expect("encode");
let image = decode_png_rgba8(&png).expect("decode");
assert_eq!(image.rgba, vec![200, 200, 200, 10, 30, 30, 30, 255]);
}
#[cfg(feature = "png-encode")]
#[test]
fn round_trip_large_image_spanning_many_scanlines() {
let mut pixels = Vec::with_capacity(64 * 64 * 4);
for y in 0..64u32 {
for x in 0..64u32 {
pixels.extend_from_slice(&[
(x * 4) as u8,
(y * 4) as u8,
((x ^ y) * 3) as u8,
255 - (x as u8 / 2),
]);
}
}
let png = encode_png(64, 64, PngColorType::Rgba8, &pixels).expect("encode");
let image = decode_png_rgba8(&png).expect("decode");
assert_eq!(image.rgba, pixels);
}
#[test]
fn all_five_filter_types_reverse_correctly() {
let row: [u8; 6] = [10, 20, 30, 200, 100, 50];
let bpp = 3usize;
let mut raw = Vec::new();
raw.push(0);
raw.extend_from_slice(&row);
raw.push(1);
for (i, &value) in row.iter().enumerate() {
let left = if i >= bpp { row[i - bpp] } else { 0 };
raw.push(value.wrapping_sub(left));
}
raw.push(2);
raw.extend_from_slice(&[0u8; 6]);
raw.push(3);
for (i, &value) in row.iter().enumerate() {
let left = if i >= bpp { u16::from(row[i - bpp]) } else { 0 };
let up = u16::from(value);
raw.push(value.wrapping_sub(((left + up) / 2) as u8));
}
raw.push(4);
for (i, &value) in row.iter().enumerate() {
let left = if i >= bpp { row[i - bpp] } else { 0 };
let up = value;
let up_left = if i >= bpp { row[i - bpp] } else { 0 };
raw.push(value.wrapping_sub(paeth(left, up, up_left)));
}
let png = build_png(ihdr(2, 5, 8, 2, 0), &[], &raw);
let image = decode_png_rgba8(&png).expect("decode");
assert_eq!((image.width, image.height), (2, 5));
for y in 0..5usize {
let off = y * 2 * 4;
assert_eq!(
&image.rgba[off..off + 8],
&[10, 20, 30, 255, 200, 100, 50, 255],
"row {y} (filter {y}) decoded incorrectly"
);
}
}
#[test]
fn grayscale_1bit_with_trns_key() {
let raw = vec![0u8, 0b1011_0000];
let trns = vec![0x00, 0x00]; let png = build_png(ihdr(4, 1, 1, 0, 0), &[(b"tRNS", trns)], &raw);
let image = decode_png_rgba8(&png).expect("decode");
assert_eq!(
image.rgba,
vec![
255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255,
]
);
}
#[test]
fn grayscale_2bit_scales_by_85() {
let raw = vec![0u8, 0b0001_1011];
let png = build_png(ihdr(4, 1, 2, 0, 0), &[], &raw);
let image = decode_png_rgba8(&png).expect("decode");
let greys: Vec<u8> = image.rgba.chunks_exact(4).map(|p| p[0]).collect();
assert_eq!(greys, vec![0, 85, 170, 255]);
}
#[test]
fn grayscale_4bit_scales_by_17() {
let raw = vec![0u8, 0x0F];
let png = build_png(ihdr(2, 1, 4, 0, 0), &[], &raw);
let image = decode_png_rgba8(&png).expect("decode");
assert_eq!(image.rgba, vec![0, 0, 0, 255, 255, 255, 255, 255]);
}
#[test]
fn indexed_4bit_with_palette_and_trns() {
let plte = vec![
255, 0, 0, 0, 255, 0, 0, 0, 255, ];
let trns = vec![0, 128]; let raw = vec![0u8, 0x01, 0x20];
let png = build_png(
ihdr(3, 1, 4, 3, 0),
&[(b"PLTE", plte), (b"tRNS", trns)],
&raw,
);
let image = decode_png_rgba8(&png).expect("decode");
assert_eq!(
image.rgba,
vec![255, 0, 0, 0, 0, 255, 0, 128, 0, 0, 255, 255]
);
}
#[test]
fn truecolor_16bit_trns_matches_at_native_depth() {
let mut raw = vec![0u8];
raw.extend_from_slice(&[0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc]);
raw.extend_from_slice(&[0x12, 0xff, 0x56, 0x78, 0x9a, 0xbc]);
let trns = vec![0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc];
let png = build_png(ihdr(2, 1, 16, 2, 0), &[(b"tRNS", trns)], &raw);
let image = decode_png_rgba8(&png).expect("decode");
assert_eq!(
image.rgba,
vec![0x12, 0x56, 0x9a, 0x00, 0x12, 0x56, 0x9a, 0xff]
);
}
#[test]
fn grayscale_alpha_16bit() {
let mut raw = vec![0u8];
raw.extend_from_slice(&[0xab, 0xcd, 0x80, 0x00]);
let png = build_png(ihdr(1, 1, 16, 4, 0), &[], &raw);
let image = decode_png_rgba8(&png).expect("decode");
assert_eq!(image.rgba, vec![0xab, 0xab, 0xab, 0x80]);
}
#[test]
fn adam7_interlaced_matches_non_interlaced() {
let mut reference = Vec::with_capacity(64);
for y in 0..8u32 {
for x in 0..8u32 {
reference.push((y * 8 + x) as u8);
}
}
let header = Header {
width: 8,
height: 8,
bit_depth: 8,
color_type: 0,
interlace: 1,
};
let mut raw = Vec::new();
for pass in 0..7usize {
let geometry = adam7_geometry(header, pass);
if geometry.width == 0 || geometry.height == 0 {
continue;
}
for py in 0..geometry.height {
raw.push(0u8); let y = geometry.row_start + py * geometry.row_step;
for px in 0..geometry.width {
let x = geometry.col_start + px * geometry.col_step;
raw.push(reference[(y * 8 + x) as usize]);
}
}
}
let png = build_png(ihdr(8, 8, 8, 0, 1), &[], &raw);
let image = decode_png_rgba8(&png).expect("decode");
assert_eq!((image.width, image.height), (8, 8));
let greys: Vec<u8> = image.rgba.chunks_exact(4).map(|p| p[0]).collect();
assert_eq!(greys, reference);
assert!(image.rgba.chunks_exact(4).all(|p| p[3] == 255));
}
#[test]
fn adam7_single_pixel_image() {
let raw = vec![0u8, 0x7f];
let png = build_png(ihdr(1, 1, 8, 0, 1), &[], &raw);
let image = decode_png_rgba8(&png).expect("decode");
assert_eq!(image.rgba, vec![0x7f, 0x7f, 0x7f, 255]);
}
const ADAM7_PALETTE_9X7: [u8; 342] = [
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
0x52, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x07, 0x08, 0x03, 0x00, 0x00, 0x01, 0x9a,
0x42, 0xa7, 0xc4, 0x00, 0x00, 0x00, 0xbd, 0x50, 0x4c, 0x54, 0x45, 0x00, 0x00, 0x00, 0xa0,
0xb4, 0x8c, 0x00, 0x1e, 0x0a, 0x14, 0x00, 0x0a, 0x00, 0x3c, 0x14, 0x14, 0x1e, 0x14, 0x00,
0x5a, 0x1e, 0x28, 0x00, 0x14, 0x14, 0x3c, 0x1e, 0x00, 0x78, 0x28, 0x28, 0x1e, 0x1e, 0x14,
0x5a, 0x28, 0x00, 0x96, 0x32, 0x3c, 0x00, 0x1e, 0x28, 0x3c, 0x28, 0x14, 0x78, 0x32, 0x00,
0xb4, 0x3c, 0x3c, 0x1e, 0x28, 0x28, 0x5a, 0x32, 0x14, 0x96, 0x3c, 0x50, 0x00, 0x28, 0x3c,
0x3c, 0x32, 0x28, 0x78, 0x3c, 0x14, 0xb4, 0x46, 0x50, 0x1e, 0x32, 0x3c, 0x5a, 0x3c, 0x28,
0x96, 0x46, 0x64, 0x00, 0x32, 0x50, 0x3c, 0x3c, 0x3c, 0x78, 0x46, 0x28, 0xb4, 0x50, 0x64,
0x1e, 0x3c, 0x50, 0x5a, 0x46, 0x3c, 0x96, 0x50, 0x78, 0x00, 0x3c, 0x64, 0x3c, 0x46, 0x50,
0x78, 0x50, 0x3c, 0xb4, 0x5a, 0x78, 0x1e, 0x46, 0x64, 0x5a, 0x50, 0x50, 0x96, 0x5a, 0x8c,
0x00, 0x46, 0x78, 0x3c, 0x50, 0x64, 0x78, 0x5a, 0x50, 0xb4, 0x64, 0x8c, 0x1e, 0x50, 0x78,
0x5a, 0x5a, 0x64, 0x96, 0x64, 0xa0, 0x00, 0x50, 0x8c, 0x3c, 0x5a, 0x78, 0x78, 0x64, 0x64,
0xb4, 0x6e, 0xa0, 0x1e, 0x5a, 0x8c, 0x5a, 0x64, 0x78, 0x96, 0x6e, 0xa0, 0x3c, 0x64, 0x8c,
0x78, 0x6e, 0x78, 0xb4, 0x78, 0xa0, 0x5a, 0x6e, 0x8c, 0x96, 0x78, 0xa0, 0x78, 0x78, 0x8c,
0xb4, 0x82, 0xa0, 0x96, 0x82, 0x24, 0x34, 0x45, 0xf1, 0x00, 0x00, 0x00, 0x54, 0x49, 0x44,
0x41, 0x54, 0x08, 0x99, 0x05, 0xc1, 0x87, 0x02, 0x42, 0x00, 0x00, 0x05, 0xc0, 0x67, 0xcb,
0x4a, 0x56, 0xd9, 0x51, 0x59, 0x59, 0x45, 0xd9, 0xfe, 0xff, 0xb3, 0xdc, 0x01, 0x2d, 0x0c,
0x9c, 0x9e, 0x2b, 0xb8, 0x18, 0xd6, 0x17, 0xb4, 0xe2, 0x15, 0x03, 0xd4, 0xa0, 0x9c, 0x08,
0x50, 0xb2, 0x9b, 0x83, 0x37, 0x1f, 0x1f, 0x9c, 0xfd, 0xf7, 0x88, 0xeb, 0xab, 0xdb, 0x40,
0x32, 0xc2, 0xe5, 0x16, 0x26, 0x55, 0x0f, 0x56, 0xd4, 0xec, 0x7b, 0x5a, 0xff, 0x66, 0x48,
0xba, 0x13, 0x65, 0xcd, 0x7f, 0xd9, 0x0f, 0x01, 0x3c, 0x07, 0xa2, 0x99, 0xed, 0x69, 0xfa,
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
];
const GRAY16_3X1: [u8; 72] = [
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
0x52, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x10, 0x00, 0x00, 0x00, 0x00, 0x6e,
0x1b, 0x97, 0x2b, 0x00, 0x00, 0x00, 0x0f, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x60,
0x60, 0x68, 0x60, 0xf8, 0xff, 0x1f, 0x00, 0x05, 0x04, 0x02, 0x7f, 0xe3, 0x80, 0x4b, 0xe0,
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
];
const BILEVEL_8X2: [u8; 69] = [
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
0x52, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x4d,
0xef, 0xa0, 0x40, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x58,
0xc3, 0x60, 0x0a, 0x00, 0x02, 0x3d, 0x00, 0xe2, 0x5e, 0x17, 0x3c, 0xc0, 0x00, 0x00, 0x00,
0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
];
#[test]
fn third_party_adam7_indexed_image_decodes_exactly() {
let image = decode_png_rgba8(&ADAM7_PALETTE_9X7).expect("decode");
assert_eq!((image.width, image.height), (9, 7));
for y in 0..7u32 {
for x in 0..9u32 {
let idx = ((y * 9 + x) * 4) as usize;
let expected = [
(x * 20 % 256) as u8,
(y * 30 % 256) as u8,
((x + y) * 10 % 256) as u8,
255,
];
assert_eq!(
&image.rgba[idx..idx + 4],
&expected,
"pixel ({x}, {y}) mismatched"
);
}
}
}
#[test]
fn third_party_16bit_greyscale_takes_high_byte() {
let image = decode_png_rgba8(&GRAY16_3X1).expect("decode");
assert_eq!((image.width, image.height), (3, 1));
assert_eq!(
image.rgba,
vec![
0x00, 0x00, 0x00, 0xff, 0x80, 0x80, 0x80, 0xff, 0xff, 0xff, 0xff, 0xff, ]
);
}
#[test]
fn third_party_1bit_bilevel_unpacks_msb_first() {
let image = decode_png_rgba8(&BILEVEL_8X2).expect("decode");
assert_eq!((image.width, image.height), (8, 2));
let bits = [
1u8, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, ];
let greys: Vec<u8> = image.rgba.chunks_exact(4).map(|p| p[0]).collect();
let expected: Vec<u8> = bits.iter().map(|&b| if b == 1 { 255 } else { 0 }).collect();
assert_eq!(greys, expected);
}
#[cfg(feature = "png-encode")]
#[test]
fn trailing_bytes_after_iend_are_ignored() {
let pixels = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
let png = encode_png(2, 1, PngColorType::Rgba8, &pixels).expect("encode");
let clean = decode_png_rgba8(&png).expect("decode");
for padding in [&[0u8][..], &[0u8; 3][..], &[0xff; 64][..]] {
let mut padded = png.clone();
padded.extend_from_slice(padding);
assert_eq!(
decode_png_rgba8(&padded).expect("padded decode"),
clean,
"{} trailing byte(s) changed the result",
padding.len()
);
}
}
#[test]
fn ancillary_chunks_are_skipped() {
let raw = vec![0u8, 0x40];
let png = build_png(
ihdr(1, 1, 8, 0, 0),
&[
(b"gAMA", vec![0, 1, 0x86, 0xa0]),
(b"tEXt", b"key\0v".to_vec()),
],
&raw,
);
let image = decode_png_rgba8(&png).expect("decode");
assert_eq!(image.rgba, vec![0x40, 0x40, 0x40, 255]);
}
#[cfg(feature = "png-encode")]
#[test]
fn multiple_idat_chunks_are_concatenated() {
let pixels: Vec<u8> = (0..(16 * 16 * 4)).map(|i| (i % 251) as u8).collect();
let png = encode_png(16, 16, PngColorType::Rgba8, &pixels).expect("encode");
let decoded_once = decode_png_rgba8(&png).expect("decode");
let mut idat_payload = Vec::new();
let mut cursor = PNG_SIGNATURE.len();
let mut rebuilt = Vec::new();
rebuilt.extend_from_slice(&PNG_SIGNATURE);
let mut tail = Vec::new();
while cursor < png.len() {
let (kind, payload, next) = read_chunk(&png, cursor).expect("chunk");
match &kind {
b"IDAT" => idat_payload.extend_from_slice(payload),
b"IEND" => tail.extend_from_slice(&png[cursor..next]),
_ => rebuilt.extend_from_slice(&png[cursor..next]),
}
cursor = next;
}
let (first, second) = idat_payload.split_at(idat_payload.len() / 2);
for part in [first, second] {
let len = u32::try_from(part.len()).unwrap_or(u32::MAX);
rebuilt.extend_from_slice(&len.to_be_bytes());
rebuilt.extend_from_slice(b"IDAT");
rebuilt.extend_from_slice(part);
let mut crc = Crc32::new();
crc.update(b"IDAT");
crc.update(part);
rebuilt.extend_from_slice(&crc.value().to_be_bytes());
}
rebuilt.extend_from_slice(&tail);
let decoded_split = decode_png_rgba8(&rebuilt).expect("decode split");
assert_eq!(decoded_split, decoded_once);
}
#[test]
fn rejects_non_png() {
assert_eq!(
decode_png_rgba8(b"not a png at all"),
Err(PngDecodeError::NotAPng)
);
assert_eq!(decode_png_rgba8(&[]), Err(PngDecodeError::NotAPng));
}
#[cfg(feature = "png-encode")]
#[test]
fn rejects_truncated_stream() {
let png = encode_png(2, 2, PngColorType::Rgba8, &[0u8; 16]).expect("encode");
let cut = &png[..png.len() - 6];
assert!(matches!(
decode_png_rgba8(cut),
Err(PngDecodeError::Truncated { .. })
));
}
#[cfg(feature = "png-encode")]
#[test]
fn rejects_crc_mismatch() {
let mut png = encode_png(1, 1, PngColorType::Grayscale8, &[9]).expect("encode");
png[32] ^= 0xff;
assert!(matches!(
decode_png_rgba8(&png),
Err(PngDecodeError::ChecksumMismatch { .. })
));
}
#[test]
fn rejects_zero_dimension() {
let png = build_png(ihdr(0, 1, 8, 0, 0), &[], &[0u8]);
assert_eq!(
decode_png_rgba8(&png),
Err(PngDecodeError::InvalidDimensions {
width: 0,
height: 1
})
);
}
#[test]
fn rejects_unknown_color_type() {
let png = build_png(ihdr(1, 1, 8, 5, 0), &[], &[0u8, 0]);
assert_eq!(
decode_png_rgba8(&png),
Err(PngDecodeError::UnsupportedColorType(5))
);
}
#[test]
fn rejects_bad_bit_depth_for_color_type() {
let png = build_png(ihdr(1, 1, 4, 2, 0), &[], &[0u8, 0]);
assert_eq!(
decode_png_rgba8(&png),
Err(PngDecodeError::UnsupportedBitDepth {
color_type: 2,
bit_depth: 4
})
);
}
#[test]
fn rejects_unknown_interlace_method() {
let png = build_png(ihdr(1, 1, 8, 0, 2), &[], &[0u8, 0]);
assert_eq!(
decode_png_rgba8(&png),
Err(PngDecodeError::UnsupportedInterlaceMethod(2))
);
}
#[test]
fn rejects_indexed_without_palette() {
let png = build_png(ihdr(1, 1, 8, 3, 0), &[], &[0u8, 0]);
assert_eq!(decode_png_rgba8(&png), Err(PngDecodeError::MissingPalette));
}
#[test]
fn rejects_palette_index_beyond_plte() {
let plte = vec![1, 2, 3]; let raw = vec![0u8, 1]; let png = build_png(ihdr(1, 1, 8, 3, 0), &[(b"PLTE", plte)], &raw);
assert_eq!(
decode_png_rgba8(&png),
Err(PngDecodeError::PaletteIndexOutOfRange {
index: 1,
palette_len: 1
})
);
}
#[test]
fn rejects_malformed_palette_length() {
let png = build_png(ihdr(1, 1, 8, 3, 0), &[(b"PLTE", vec![1, 2])], &[0u8, 0]);
assert_eq!(
decode_png_rgba8(&png),
Err(PngDecodeError::InvalidPalette { length: 2 })
);
}
#[test]
fn rejects_short_image_data() {
let png = build_png(ihdr(1, 4, 8, 0, 0), &[], &[0u8, 0]);
assert!(matches!(
decode_png_rgba8(&png),
Err(PngDecodeError::DataSize { .. })
));
}
#[test]
fn rejects_image_data_that_inflates_past_the_declared_geometry() {
let bomb = vec![0u8; 10 * 1024 * 1024];
let compressed = zlib_compress(&bomb, 6).expect("compress");
assert!(
compressed.len() * MAX_DEFLATE_RATIO > bomb.len(),
"test vector must pass the ratio pre-check so the buffer guard is what fires"
);
let mut out = Vec::new();
out.extend_from_slice(&PNG_SIGNATURE);
let mut push = |kind: &[u8; 4], data: &[u8]| {
let len = u32::try_from(data.len()).unwrap_or(u32::MAX);
out.extend_from_slice(&len.to_be_bytes());
out.extend_from_slice(kind);
out.extend_from_slice(data);
let mut crc = Crc32::new();
crc.update(kind);
crc.update(data);
out.extend_from_slice(&crc.value().to_be_bytes());
};
push(b"IHDR", &ihdr(2, 2, 8, 6, 0));
push(b"IDAT", &compressed);
push(b"IEND", &[]);
assert_eq!(
decode_png_rgba8(&out),
Err(PngDecodeError::ExcessImageData { expected: 18 })
);
}
#[test]
fn rejects_geometry_unreachable_from_the_idat_length() {
let raw = vec![0u8; 4];
let png = build_png(ihdr(65535, 65535, 8, 0, 0), &[], &raw);
match decode_png_rgba8(&png) {
Err(PngDecodeError::CompressionRatioExceeded {
expected,
compressed,
}) => {
assert_eq!(expected, (65535usize + 1) * 65535);
assert!(compressed > 0 && compressed * MAX_DEFLATE_RATIO < expected);
}
other => panic!("expected CompressionRatioExceeded, got {other:?}"),
}
}
#[test]
fn rejects_unknown_filter_type() {
let png = build_png(ihdr(1, 1, 8, 0, 0), &[], &[7u8, 0]);
assert_eq!(
decode_png_rgba8(&png),
Err(PngDecodeError::InvalidFilterType(7))
);
}
#[test]
fn rejects_missing_idat() {
let mut out = Vec::new();
out.extend_from_slice(&PNG_SIGNATURE);
let header = ihdr(1, 1, 8, 0, 0);
let mut push = |kind: &[u8; 4], data: &[u8]| {
let len = u32::try_from(data.len()).unwrap_or(u32::MAX);
out.extend_from_slice(&len.to_be_bytes());
out.extend_from_slice(kind);
out.extend_from_slice(data);
let mut crc = Crc32::new();
crc.update(kind);
crc.update(data);
out.extend_from_slice(&crc.value().to_be_bytes());
};
push(b"IHDR", &header);
push(b"IEND", &[]);
assert_eq!(
decode_png_rgba8(&out),
Err(PngDecodeError::MissingImageData)
);
}
#[test]
fn reports_apple_cgbi_variant_distinctly() {
let mut out = Vec::new();
out.extend_from_slice(&PNG_SIGNATURE);
let payload = [0x50, 0x00, 0x20, 0x02];
let len = u32::try_from(payload.len()).unwrap_or(u32::MAX);
out.extend_from_slice(&len.to_be_bytes());
out.extend_from_slice(b"CgBI");
out.extend_from_slice(&payload);
let mut crc = Crc32::new();
crc.update(b"CgBI");
crc.update(&payload);
out.extend_from_slice(&crc.value().to_be_bytes());
assert_eq!(decode_png_rgba8(&out), Err(PngDecodeError::AppleCgBi));
}
#[test]
fn rejects_stream_whose_first_chunk_is_not_ihdr() {
let mut out = Vec::new();
out.extend_from_slice(&PNG_SIGNATURE);
let data = [0u8; 4];
let len = u32::try_from(data.len()).unwrap_or(u32::MAX);
out.extend_from_slice(&len.to_be_bytes());
out.extend_from_slice(b"gAMA");
out.extend_from_slice(&data);
let mut crc = Crc32::new();
crc.update(b"gAMA");
crc.update(&data);
out.extend_from_slice(&crc.value().to_be_bytes());
assert_eq!(decode_png_rgba8(&out), Err(PngDecodeError::InvalidHeader));
}
#[test]
fn rejects_corrupt_deflate_stream() {
let mut out = Vec::new();
out.extend_from_slice(&PNG_SIGNATURE);
let header = ihdr(1, 1, 8, 0, 0);
let mut push = |kind: &[u8; 4], data: &[u8]| {
let len = u32::try_from(data.len()).unwrap_or(u32::MAX);
out.extend_from_slice(&len.to_be_bytes());
out.extend_from_slice(kind);
out.extend_from_slice(data);
let mut crc = Crc32::new();
crc.update(kind);
crc.update(data);
out.extend_from_slice(&crc.value().to_be_bytes());
};
push(b"IHDR", &header);
push(b"IDAT", &[0xde, 0xad, 0xbe, 0xef]);
push(b"IEND", &[]);
assert!(matches!(
decode_png_rgba8(&out),
Err(PngDecodeError::Decompression(_))
));
}
#[test]
fn error_display_is_populated() {
let variants = [
PngDecodeError::NotAPng,
PngDecodeError::Truncated {
offset: 1,
needed: 2,
},
PngDecodeError::InvalidHeader,
PngDecodeError::AppleCgBi,
PngDecodeError::ChecksumMismatch {
chunk: *b"IDAT",
stored: 1,
computed: 2,
},
PngDecodeError::InvalidDimensions {
width: 0,
height: 0,
},
PngDecodeError::UnsupportedColorType(9),
PngDecodeError::UnsupportedBitDepth {
color_type: 2,
bit_depth: 3,
},
PngDecodeError::UnsupportedCompressionMethod(1),
PngDecodeError::UnsupportedFilterMethod(1),
PngDecodeError::UnsupportedInterlaceMethod(3),
PngDecodeError::MissingPalette,
PngDecodeError::InvalidPalette { length: 2 },
PngDecodeError::PaletteIndexOutOfRange {
index: 4,
palette_len: 2,
},
PngDecodeError::MissingImageData,
PngDecodeError::Decompression("boom".to_string()),
PngDecodeError::DataSize {
expected: 4,
actual: 1,
},
PngDecodeError::ExcessImageData { expected: 4 },
PngDecodeError::CompressionRatioExceeded {
expected: 4096,
compressed: 1,
},
PngDecodeError::InvalidFilterType(9),
PngDecodeError::ImageTooLarge {
width: 1,
height: 2,
},
];
for v in &variants {
assert!(!v.to_string().is_empty());
}
}
#[test]
fn paeth_predictor_matches_specification_examples() {
assert_eq!(paeth(10, 20, 15), 15);
assert_eq!(paeth(0, 0, 0), 0);
assert_eq!(paeth(255, 0, 0), 255);
assert_eq!(paeth(1, 2, 3), 1);
}
}