pub struct Decoded<'a> {
pub width: u32,
pub height: u32,
pub pixels: &'a [u8],
}
const SIGNATURE: [u8; 8] = [0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a];
pub fn decode_png<'a>(data: &[u8], out: &'a mut [u8]) -> Option<Decoded<'a>> {
if !data.starts_with(&SIGNATURE) {
return None;
}
let mut reader = Chunks::new(data, SIGNATURE.len());
let mut header = None;
let mut palette = None;
let mut transparency = None;
let mut idat = crate::compat::Vec::new();
while let Some(chunk) = reader.next_chunk()? {
match &chunk.kind {
b"IHDR" => header = Some(parse_ihdr(chunk.data)?),
b"PLTE" => palette = Some(chunk.data),
b"tRNS" => transparency = Some(chunk.data),
b"IDAT" => idat.extend_from_slice(chunk.data),
b"IEND" => break,
_ => {
let critical = chunk.kind[0].is_ascii_uppercase();
if critical {
return None;
}
}
}
}
let header = header?;
if header.interlaced {
return None;
}
let channels = channels_for(header.color_type)?;
let bytes_per_sample = match header.bit_depth {
8 => 1usize,
16 => 2usize,
1 | 2 | 4 if matches!(header.color_type, 0 | 3) => 1usize,
_ => return None,
};
let bits_per_sample = header.bit_depth as usize;
if header.color_type == 3 && palette.is_none() {
return None;
}
let row_bytes = if bits_per_sample < 8 {
(header.width as usize).checked_mul(bits_per_sample)?.checked_add(7)? / 8
} else {
(header.width as usize).checked_mul(channels)?.checked_mul(bytes_per_sample)?
};
let expected = header.height as usize * (row_bytes + 1);
let mut raw = crate::compat::vec![0u8; expected];
inflate_into(&idat, &mut raw)?;
let filter_bpp = if bits_per_sample < 8 { 1 } else { bytes_per_sample * channels };
unfilter(&mut raw, header.height as usize, row_bytes, filter_bpp)?;
let needed = (header.width as usize).checked_mul(header.height as usize)?.checked_mul(4)?;
if out.len() < needed {
return None;
}
expand(
&raw,
header,
channels,
bytes_per_sample,
bits_per_sample,
row_bytes,
palette,
transparency,
&mut out[..needed],
)?;
Some(Decoded { width: header.width, height: header.height, pixels: &out[..needed] })
}
#[derive(Debug, Clone, Copy)]
struct Ihdr {
width: u32,
height: u32,
bit_depth: u8,
color_type: u8,
interlaced: bool,
}
fn parse_ihdr(data: &[u8]) -> Option<Ihdr> {
if data.len() < 13 {
return None;
}
let width = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
let height = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
if width == 0 || height == 0 {
return None;
}
let bit_depth = data[8];
let color_type = data[9];
if data[10] != 0 || data[11] != 0 {
return None;
}
let interlace = match data[12] {
0 => false,
1 => true,
_ => return None,
};
Some(Ihdr { width, height, bit_depth, color_type, interlaced: interlace })
}
fn channels_for(color_type: u8) -> Option<usize> {
match color_type {
0 => Some(1), 2 => Some(3), 3 => Some(1), 4 => Some(2), 6 => Some(4), _ => None,
}
}
struct Chunks<'a> {
data: &'a [u8],
offset: usize,
}
struct Chunk<'a> {
kind: [u8; 4],
data: &'a [u8],
}
impl<'a> Chunks<'a> {
const fn new(data: &'a [u8], offset: usize) -> Self {
Self { data, offset }
}
fn next_chunk(&mut self) -> Option<Option<Chunk<'a>>> {
let header = self.data.get(self.offset..self.offset + 8)?;
let length = u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as usize;
let kind = [header[4], header[5], header[6], header[7]];
let start = self.offset + 8;
let payload = self.data.get(start..start.checked_add(length)?)?;
let crc_bytes = self.data.get(start + length..start + length + 4)?;
let expected = u32::from_be_bytes([crc_bytes[0], crc_bytes[1], crc_bytes[2], crc_bytes[3]]);
if crc32(&kind, payload) != expected {
return None;
}
self.offset = start + length + 4;
Some(Some(Chunk { kind, data: payload }))
}
}
fn crc32(kind: &[u8; 4], payload: &[u8]) -> u32 {
let mut crc = 0xffff_ffffu32;
for byte in kind.iter().chain(payload.iter()) {
crc ^= u32::from(*byte);
for _ in 0..8 {
let mask = (crc & 1).wrapping_neg();
crc = (crc >> 1) ^ (0xedb8_8320 & mask);
}
}
!crc
}
fn inflate_into(data: &[u8], out: &mut [u8]) -> Option<()> {
use miniz_oxide::inflate::core::{decompress, inflate_flags};
use miniz_oxide::inflate::TINFLStatus;
if out.is_empty() {
return None;
}
let mut decompressor = miniz_oxide::inflate::core::DecompressorOxide::new();
let flags = inflate_flags::TINFL_FLAG_PARSE_ZLIB_HEADER
| inflate_flags::TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF;
let mut in_pos = 0usize;
let mut out_pos = 0usize;
loop {
let (status, consumed, produced) =
decompress(&mut decompressor, &data[in_pos..], &mut out[out_pos..], 0, flags);
in_pos = in_pos.checked_add(consumed)?;
out_pos = out_pos.checked_add(produced)?;
match status {
TINFLStatus::Done => break,
TINFLStatus::HasMoreOutput => {
if produced == 0 && consumed == 0 {
return None;
}
}
_ => return None,
}
}
if out_pos != out.len() {
return None;
}
Some(())
}
fn unfilter(raw: &mut [u8], height: usize, row_bytes: usize, bpp: usize) -> Option<()> {
let stride = row_bytes + 1;
if raw.len() < stride * height {
return None;
}
let mut previous = crate::compat::vec![0u8; row_bytes];
for row in 0..height {
let start = row * stride;
let filter = *raw.get(start)?;
let row_start = start + 1;
let row_slice = raw.get_mut(row_start..row_start + row_bytes)?;
let mut current = previous.clone();
current.copy_from_slice(row_slice);
for i in 0..row_bytes {
let left = if i >= bpp { row_slice[i - bpp] } else { 0 };
let above = previous.get(i).copied().unwrap_or(0);
let corner = if i >= bpp { previous.get(i - bpp).copied().unwrap_or(0) } else { 0 };
let value = row_slice[i];
row_slice[i] = match filter {
0 => value,
1 => value.wrapping_add(left),
2 => value.wrapping_add(above),
3 => value.wrapping_add(((u16::from(left) + u16::from(above)) / 2) as u8),
4 => value.wrapping_add(paeth(left, above, corner)),
_ => return None,
};
}
previous.copy_from_slice(¤t);
previous.copy_from_slice(raw.get(row_start..row_start + row_bytes)?);
}
Some(())
}
fn paeth(left: u8, above: u8, corner: u8) -> u8 {
let p = i16::from(left) + i16::from(above) - i16::from(corner);
let pa = (p - i16::from(left)).abs();
let pb = (p - i16::from(above)).abs();
let pc = (p - i16::from(corner)).abs();
if pa <= pb && pa <= pc {
left
} else if pb <= pc {
above
} else {
corner
}
}
#[allow(clippy::too_many_arguments)]
fn expand(
raw: &[u8],
header: Ihdr,
channels: usize,
bytes_per_sample: usize,
bits_per_sample: usize,
row_bytes: usize,
palette: Option<&[u8]>,
transparency: Option<&[u8]>,
out: &mut [u8],
) -> Option<()> {
let width = header.width as usize;
let height = header.height as usize;
let stride = row_bytes + 1;
let sample = |row: &[u8], index: usize| -> u8 { row.get(index).copied().unwrap_or(0) };
let packed = |row: &[u8], index: usize| -> u8 {
let bit = index * bits_per_sample;
let byte = row.get(bit / 8).copied().unwrap_or(0);
let offset = bit % 8;
let shift = 8 - bits_per_sample - offset;
(byte >> shift) & ((1u16 << bits_per_sample) - 1) as u8
};
for y in 0..height {
let row = raw.get(y * stride + 1..(y + 1) * stride)?;
for x in 0..width {
let di = (y * width + x) * 4;
let target = out.get_mut(di..di + 4)?;
let base = x * channels;
match header.color_type {
0 => {
let raw_g = if bits_per_sample < 8 {
packed(row, base)
} else {
sample(row, base * bytes_per_sample)
};
let g = scale_to_byte(raw_g, bits_per_sample);
let alpha = match transparency {
Some(t) if t.len() >= 2 => {
let key = u16::from_be_bytes([t[0], t[1]]);
let value = if bits_per_sample < 8 {
u16::from(raw_g)
} else {
sample_full(row, base, bytes_per_sample)
};
if value == key {
0
} else {
255
}
}
_ => 255,
};
target.copy_from_slice(&[g, g, g, alpha]);
}
2 => {
let r = scale_to_byte(sample(row, base * bytes_per_sample), bits_per_sample);
let g =
scale_to_byte(sample(row, (base + 1) * bytes_per_sample), bits_per_sample);
let b =
scale_to_byte(sample(row, (base + 2) * bytes_per_sample), bits_per_sample);
let alpha = match transparency {
Some(t) if t.len() >= 6 => {
let kr = u16::from_be_bytes([t[0], t[1]]);
let kg = u16::from_be_bytes([t[2], t[3]]);
let kb = u16::from_be_bytes([t[4], t[5]]);
let (r16, g16, b16) = (
sample_full(row, base, bytes_per_sample),
sample_full(row, base + 1, bytes_per_sample),
sample_full(row, base + 2, bytes_per_sample),
);
if (r16, g16, b16) == (kr, kg, kb) {
0
} else {
255
}
}
_ => 255,
};
target.copy_from_slice(&[r, g, b, alpha]);
}
3 => {
let index = if bits_per_sample < 8 {
packed(row, base) as usize
} else {
sample(row, base * bytes_per_sample) as usize
};
let entry = palette?.get(index * 3..index * 3 + 3)?;
let alpha = transparency.and_then(|t| t.get(index).copied()).unwrap_or(255);
target.copy_from_slice(&[entry[0], entry[1], entry[2], alpha]);
}
4 => {
let g = scale_to_byte(sample(row, base * bytes_per_sample), bits_per_sample);
let a =
scale_to_byte(sample(row, (base + 1) * bytes_per_sample), bits_per_sample);
target.copy_from_slice(&[g, g, g, a]);
}
6 => {
let r = scale_to_byte(sample(row, base * bytes_per_sample), bits_per_sample);
let g =
scale_to_byte(sample(row, (base + 1) * bytes_per_sample), bits_per_sample);
let b =
scale_to_byte(sample(row, (base + 2) * bytes_per_sample), bits_per_sample);
let a =
scale_to_byte(sample(row, (base + 3) * bytes_per_sample), bits_per_sample);
target.copy_from_slice(&[r, g, b, a]);
}
_ => return None,
}
}
}
Some(())
}
fn sample_full(row: &[u8], index: usize, bytes_per_sample: usize) -> u16 {
if bytes_per_sample == 2 {
let i = index * 2;
let hi = row.get(i).copied().unwrap_or(0);
let lo = row.get(i + 1).copied().unwrap_or(0);
u16::from_be_bytes([hi, lo])
} else {
u16::from(row.get(index).copied().unwrap_or(0))
}
}
fn scale_to_byte(value: u8, bits: usize) -> u8 {
match bits {
1 => value * 255,
2 => value * 85,
4 => value * 17,
_ => value,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn red_pixel_png() -> Vec<u8> {
let mut png = Vec::new();
png.extend_from_slice(&SIGNATURE);
let mut ihdr = Vec::new();
ihdr.extend_from_slice(&1u32.to_be_bytes());
ihdr.extend_from_slice(&1u32.to_be_bytes());
ihdr.extend_from_slice(&[8, 6, 0, 0, 0]);
push_chunk(&mut png, b"IHDR", &ihdr);
let mut raw = Vec::new();
raw.push(0); raw.extend_from_slice(&[255, 0, 0, 255]);
let mut deflate = Vec::new();
deflate.push(0x01); let len = raw.len() as u16;
deflate.extend_from_slice(&len.to_le_bytes());
deflate.extend_from_slice(&(!len).to_le_bytes());
deflate.extend_from_slice(&raw);
let mut zlib = Vec::new();
zlib.push(0x78);
zlib.push(0x01);
zlib.extend_from_slice(&deflate);
zlib.extend_from_slice(&adler32(&raw).to_be_bytes());
push_chunk(&mut png, b"IDAT", &zlib);
push_chunk(&mut png, b"IEND", &[]);
png
}
fn push_chunk(out: &mut Vec<u8>, kind: &[u8; 4], payload: &[u8]) {
out.extend_from_slice(&(payload.len() as u32).to_be_bytes());
out.extend_from_slice(kind);
out.extend_from_slice(payload);
out.extend_from_slice(&crc32(kind, payload).to_be_bytes());
}
fn palette_four_bit_png(width: u32, rows: &[&[u8]], row_bytes: usize) -> Vec<u8> {
let mut png = Vec::new();
png.extend_from_slice(&SIGNATURE);
let mut ihdr = Vec::new();
ihdr.extend_from_slice(&width.to_be_bytes());
ihdr.extend_from_slice(&(rows.len() as u32).to_be_bytes());
ihdr.extend_from_slice(&[4, 3, 0, 0, 0]);
push_chunk(&mut png, b"IHDR", &ihdr);
let mut palette = Vec::new();
palette.extend_from_slice(&[255, 0, 0]); palette.extend_from_slice(&[0, 255, 0]); palette.extend_from_slice(&[0, 0, 255]); for extra in 0..13u8 {
palette.extend_from_slice(&[extra, extra, extra]);
}
palette[15 * 3] = 255;
palette[15 * 3 + 1] = 255;
palette[15 * 3 + 2] = 255; push_chunk(&mut png, b"PLTE", &palette);
let mut trns = [255u8; 16];
trns[0] = 0;
push_chunk(&mut png, b"tRNS", &trns);
let mut raw = Vec::new();
for row in rows {
raw.push(0); raw.extend_from_slice(row);
raw.resize(raw.len() + row_bytes - row.len(), 0);
}
let mut deflate = Vec::new();
deflate.push(0x01);
let len = raw.len() as u16;
deflate.extend_from_slice(&len.to_le_bytes());
deflate.extend_from_slice(&(!len).to_le_bytes());
deflate.extend_from_slice(&raw);
let mut zlib = Vec::new();
zlib.push(0x78);
zlib.push(0x01);
zlib.extend_from_slice(&deflate);
zlib.extend_from_slice(&adler32(&raw).to_be_bytes());
push_chunk(&mut png, b"IDAT", &zlib);
push_chunk(&mut png, b"IEND", &[]);
png
}
fn adler32(data: &[u8]) -> u32 {
let (mut a, mut b) = (1u32, 0u32);
for byte in data {
a = (a + u32::from(*byte)) % 65521;
b = (b + a) % 65521;
}
(b << 16) | a
}
fn zlib_stored(raw: &[u8], final_block: bool) -> Vec<u8> {
let mut deflate = Vec::new();
deflate.push(if final_block { 0x01 } else { 0x00 });
let len = raw.len() as u16;
deflate.extend_from_slice(&len.to_le_bytes());
deflate.extend_from_slice(&(!len).to_le_bytes());
deflate.extend_from_slice(raw);
let mut zlib = vec![0x78, 0x01];
zlib.extend_from_slice(&deflate);
zlib.extend_from_slice(&adler32(raw).to_be_bytes());
zlib
}
#[test]
fn a_truecolour_alpha_pixel_decodes() {
let png = red_pixel_png();
let mut out = vec![0u8; 4];
let decoded = decode_png(&png, &mut out).expect("a valid PNG decodes");
assert_eq!((decoded.width, decoded.height), (1, 1));
assert_eq!(decoded.pixels, &[255, 0, 0, 255]);
}
#[test]
fn a_bad_crc_is_refused() {
let mut png = red_pixel_png();
let len = png.len();
png[len - 10] ^= 0xff;
let mut out = vec![0u8; 4];
assert!(decode_png(&png, &mut out).is_none(), "a corrupted chunk must be refused");
}
#[test]
fn a_wrong_signature_is_refused() {
let mut out = vec![0u8; 4];
assert!(decode_png(b"not a png at all", &mut out).is_none());
assert!(decode_png(&[], &mut out).is_none());
}
#[test]
fn a_short_buffer_is_refused() {
let png = red_pixel_png();
let mut out = vec![0u8; 3];
assert!(decode_png(&png, &mut out).is_none());
}
#[test]
fn the_paeth_predictor_is_symmetric() {
assert_eq!(paeth(10, 20, 30), 10);
assert_eq!(paeth(0, 0, 0), 0);
assert_eq!(paeth(255, 0, 0), 255);
assert_eq!(paeth(0, 0, 255), 0);
assert_eq!(paeth(255, 255, 0), 255);
assert_eq!(paeth(200, 100, 150), 150);
}
#[test]
fn colour_types_map_to_channel_counts() {
assert_eq!(channels_for(0), Some(1));
assert_eq!(channels_for(2), Some(3));
assert_eq!(channels_for(3), Some(1));
assert_eq!(channels_for(4), Some(2));
assert_eq!(channels_for(6), Some(4));
assert_eq!(channels_for(1), None, "1 is not a PNG colour type");
assert_eq!(channels_for(5), None);
}
#[test]
fn a_four_bit_palette_image_decodes() {
let png = palette_four_bit_png(2, &[&[0x0f], &[0x12]], 1);
let mut out = vec![0u8; 2 * 2 * 4];
let decoded = decode_png(&png, &mut out).expect("a 4-bit palette PNG decodes");
assert_eq!((decoded.width, decoded.height), (2, 2));
assert_eq!(
decoded.pixels,
&[
255, 0, 0, 0, 255, 255, 255, 255, 0, 255, 0, 255, 0, 0, 255, 255, ]
);
}
#[test]
fn a_packed_row_pads_to_a_whole_byte() {
let png = palette_four_bit_png(3, &[&[0x01, 0x20], &[0x21, 0x00]], 2);
let mut out = vec![0u8; 3 * 2 * 4];
let decoded = decode_png(&png, &mut out).expect("a padded row decodes");
assert_eq!(&decoded.pixels[0..4], &[255, 0, 0, 0], "row 0 pixel 0 is transparent red");
assert_eq!(&decoded.pixels[4..8], &[0, 255, 0, 255], "row 0 pixel 1 is green");
assert_eq!(&decoded.pixels[8..12], &[0, 0, 255, 255], "row 0 pixel 2 is blue");
assert_eq!(&decoded.pixels[12..16], &[0, 0, 255, 255], "row 1 pixel 0 is blue");
assert_eq!(&decoded.pixels[16..20], &[0, 255, 0, 255], "row 1 pixel 1 is green");
assert_eq!(&decoded.pixels[20..24], &[255, 0, 0, 0], "row 1 pixel 2 is transparent red");
}
#[test]
fn a_sub_byte_depth_on_truecolour_is_refused() {
let mut png = red_pixel_png();
png[16 + 8] = 4;
let mut out = vec![0u8; 4];
assert!(
decode_png(&png, &mut out).is_none(),
"4-bit truecolour is not a defined PNG encoding"
);
}
#[test]
fn a_stream_that_ends_before_the_output_is_full_is_refused() {
let zlib = zlib_stored(&[1, 2, 3, 4], true);
let mut out = vec![0u8; 8];
assert!(
inflate_into(&zlib, &mut out).is_none(),
"a stream that stops short must not pass off a zero-filled tail as data"
);
}
#[test]
fn a_stream_longer_than_the_buffer_is_refused() {
let zlib = zlib_stored(&[7u8; 128], true);
let mut out = vec![0u8; 64];
assert!(inflate_into(&zlib, &mut out).is_none(), "overflow must refuse, not truncate");
}
#[test]
fn a_stream_that_fills_the_buffer_and_continues_is_refused() {
let mut deflate = Vec::new();
deflate.push(0x00);
let len = 16u16;
deflate.extend_from_slice(&len.to_le_bytes());
deflate.extend_from_slice(&(!len).to_le_bytes());
deflate.extend_from_slice(&[3u8; 16]);
deflate.push(0x01);
deflate.extend_from_slice(&len.to_le_bytes());
deflate.extend_from_slice(&(!len).to_le_bytes());
deflate.extend_from_slice(&[4u8; 16]);
let mut adler_input = vec![3u8; 16];
adler_input.extend_from_slice(&[4u8; 16]);
let mut zlib = vec![0x78, 0x01];
zlib.extend_from_slice(&deflate);
zlib.extend_from_slice(&adler32(&adler_input).to_be_bytes());
let mut out = vec![0u8; 16];
assert!(
inflate_into(&zlib, &mut out).is_none(),
"a stream that fills the buffer and continues must refuse"
);
assert_eq!(out, vec![3u8; 16], "the first block's bytes must have been written");
}
#[test]
fn garbage_is_refused() {
let mut out = vec![0u8; 16];
assert!(inflate_into(&[], &mut out).is_none(), "an empty stream produces nothing");
assert!(
inflate_into(&[0xff; 32], &mut out).is_none(),
"a stream that is not zlib must be refused"
);
assert!(
inflate_into(&[0x78, 0x01], &mut out).is_none(),
"a zlib header with no deflate body must be refused"
);
}
#[test]
fn an_empty_output_buffer_is_refused() {
let zlib = zlib_stored(&[], true);
let mut out: Vec<u8> = Vec::new();
assert!(inflate_into(&zlib, &mut out).is_none());
}
#[test]
fn a_stream_is_inflated_across_more_than_one_call() {
let raw: Vec<u8> = (0..8832u32).map(|i| (i % 251) as u8).collect();
let zlib = zlib_stored(&raw, true);
let mut out = vec![0u8; raw.len()];
inflate_into(&zlib, &mut out).expect("a well-formed stream must inflate");
assert_eq!(out, raw, "the inflated bytes must match the input exactly");
}
}