use oxiarc_deflate::zlib::zlib_compress;
const PNG_SIGNATURE: [u8; 8] = [0x89, b'P', b'N', b'G', b'\r', b'\n', 0x1a, b'\n'];
const ZLIB_LEVEL: u8 = 9;
#[derive(Debug, thiserror::Error)]
pub enum PngError {
#[error("rgb buffer has {got} bytes, expected width*height*3 = {expected}")]
BadLength {
got: usize,
expected: usize,
},
#[error("image dimensions must be non-zero (got {width}x{height})")]
ZeroDimension {
width: usize,
height: usize,
},
#[error("dimension {value} exceeds the PNG maximum of {}", u32::MAX)]
DimensionTooLarge {
value: usize,
},
#[error("zlib compression failed: {0}")]
Compress(String),
}
pub type PngResult<T> = Result<T, PngError>;
pub fn encode_rgb8(width: usize, height: usize, rgb: &[u8]) -> PngResult<Vec<u8>> {
if width == 0 || height == 0 {
return Err(PngError::ZeroDimension { width, height });
}
if width > u32::MAX as usize {
return Err(PngError::DimensionTooLarge { value: width });
}
if height > u32::MAX as usize {
return Err(PngError::DimensionTooLarge { value: height });
}
let row_bytes = width
.checked_mul(3)
.ok_or(PngError::DimensionTooLarge { value: width })?;
let expected = row_bytes
.checked_mul(height)
.ok_or(PngError::DimensionTooLarge { value: height })?;
if rgb.len() != expected {
return Err(PngError::BadLength {
got: rgb.len(),
expected,
});
}
let mut raw = Vec::with_capacity(height * (1 + row_bytes));
for row in 0..height {
raw.push(0u8); let start = row * row_bytes;
raw.extend_from_slice(&rgb[start..start + row_bytes]);
}
let idat = zlib_compress(&raw, ZLIB_LEVEL).map_err(|e| PngError::Compress(e.to_string()))?;
let mut ihdr = Vec::with_capacity(13);
ihdr.extend_from_slice(&(width as u32).to_be_bytes());
ihdr.extend_from_slice(&(height as u32).to_be_bytes());
ihdr.push(8); ihdr.push(2); ihdr.push(0); ihdr.push(0); ihdr.push(0);
let mut out = Vec::with_capacity(PNG_SIGNATURE.len() + 12 * 3 + ihdr.len() + idat.len());
out.extend_from_slice(&PNG_SIGNATURE);
write_chunk(&mut out, b"IHDR", &ihdr);
write_chunk(&mut out, b"IDAT", &idat);
write_chunk(&mut out, b"IEND", &[]);
Ok(out)
}
fn write_chunk(out: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) {
let len = data.len() as u32;
out.extend_from_slice(&len.to_be_bytes());
out.extend_from_slice(chunk_type);
out.extend_from_slice(data);
let mut hasher = crc32fast::Hasher::new();
hasher.update(chunk_type);
hasher.update(data);
let crc = hasher.finalize();
out.extend_from_slice(&crc.to_be_bytes());
}
#[cfg(test)]
mod tests {
use super::*;
use oxiarc_deflate::zlib::zlib_decompress;
fn raw_scanlines(width: usize, height: usize, rgb: &[u8]) -> Vec<u8> {
let row_bytes = width * 3;
let mut raw = Vec::with_capacity(height * (1 + row_bytes));
for row in 0..height {
raw.push(0u8);
let start = row * row_bytes;
raw.extend_from_slice(&rgb[start..start + row_bytes]);
}
raw
}
struct BitReader<'a> {
data: &'a [u8],
byte: usize,
bit: u32,
}
impl<'a> BitReader<'a> {
fn new(data: &'a [u8]) -> Self {
Self {
data,
byte: 0,
bit: 0,
}
}
fn bit(&mut self) -> Result<u32, &'static str> {
let b = *self.data.get(self.byte).ok_or("deflate: out of bits")?;
let v = (b >> self.bit) & 1;
self.bit += 1;
if self.bit == 8 {
self.bit = 0;
self.byte += 1;
}
Ok(v as u32)
}
fn bits(&mut self, n: u32) -> Result<u32, &'static str> {
let mut v = 0u32;
for i in 0..n {
v |= self.bit()? << i;
}
Ok(v)
}
fn align(&mut self) {
if self.bit != 0 {
self.bit = 0;
self.byte += 1;
}
}
}
struct Huffman {
counts: [u16; 16],
symbols: Vec<u16>,
}
impl Huffman {
fn new(lengths: &[u8]) -> Self {
let mut counts = [0u16; 16];
for &l in lengths {
counts[l as usize] += 1;
}
counts[0] = 0;
let mut offsets = [0u16; 16];
let mut sum = 0u16;
for len in 1..16 {
offsets[len] = sum;
sum += counts[len];
}
let mut symbols = vec![0u16; lengths.len()];
for (sym, &l) in lengths.iter().enumerate() {
if l != 0 {
symbols[offsets[l as usize] as usize] = sym as u16;
offsets[l as usize] += 1;
}
}
Self { counts, symbols }
}
fn decode(&self, br: &mut BitReader) -> Result<u16, &'static str> {
let mut code = 0i32;
let mut first = 0i32;
let mut index = 0i32;
for len in 1..16 {
code |= br.bit()? as i32;
let count = self.counts[len] as i32;
if code - first < count {
return Ok(self.symbols[(index + (code - first)) as usize]);
}
index += count;
first += count;
first <<= 1;
code <<= 1;
}
Err("deflate: bad symbol")
}
}
fn inflate_raw(data: &[u8]) -> Result<Vec<u8>, &'static str> {
const LEN_BASE: [u16; 29] = [
3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99,
115, 131, 163, 195, 227, 258,
];
const LEN_EXTRA: [u8; 29] = [
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0,
];
const DIST_BASE: [u16; 30] = [
1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025,
1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577,
];
const DIST_EXTRA: [u8; 30] = [
0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12,
12, 13, 13,
];
const CL_ORDER: [usize; 19] = [
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15,
];
let mut br = BitReader::new(data);
let mut out = Vec::new();
loop {
let final_block = br.bit()?;
let btype = br.bits(2)?;
match btype {
0 => {
br.align();
let len = u16::from_le_bytes([
*data.get(br.byte).ok_or("stored len")?,
*data.get(br.byte + 1).ok_or("stored len")?,
]) as usize;
br.byte += 4; let chunk = data.get(br.byte..br.byte + len).ok_or("stored payload")?;
out.extend_from_slice(chunk);
br.byte += len;
}
1 | 2 => {
let (lit, dist) = if btype == 1 {
let mut ll = [0u8; 288];
for (i, slot) in ll.iter_mut().enumerate() {
*slot = if i < 144 {
8
} else if i < 256 {
9
} else if i < 280 {
7
} else {
8
};
}
let dd = [5u8; 30];
(Huffman::new(&ll), Huffman::new(&dd))
} else {
let hlit = br.bits(5)? as usize + 257;
let hdist = br.bits(5)? as usize + 1;
let hclen = br.bits(4)? as usize + 4;
let mut cl_lengths = [0u8; 19];
for &idx in CL_ORDER.iter().take(hclen) {
cl_lengths[idx] = br.bits(3)? as u8;
}
let cl = Huffman::new(&cl_lengths);
let mut lengths = Vec::with_capacity(hlit + hdist);
while lengths.len() < hlit + hdist {
let sym = cl.decode(&mut br)?;
match sym {
0..=15 => lengths.push(sym as u8),
16 => {
let prev = *lengths.last().ok_or("repeat with no prev")?;
let n = br.bits(2)? + 3;
for _ in 0..n {
lengths.push(prev);
}
}
17 => {
let n = br.bits(3)? as usize + 3;
lengths.resize(lengths.len() + n, 0);
}
18 => {
let n = br.bits(7)? as usize + 11;
lengths.resize(lengths.len() + n, 0);
}
_ => return Err("bad cl symbol"),
}
}
if lengths.len() != hlit + hdist {
return Err("length set overrun");
}
let (ll, dd) = lengths.split_at(hlit);
(Huffman::new(ll), Huffman::new(dd))
};
loop {
let sym = lit.decode(&mut br)?;
if sym == 256 {
break;
} else if sym < 256 {
out.push(sym as u8);
} else {
let li = (sym - 257) as usize;
if li >= LEN_BASE.len() {
return Err("bad length symbol");
}
let length =
LEN_BASE[li] as usize + br.bits(LEN_EXTRA[li] as u32)? as usize;
let dsym = dist.decode(&mut br)? as usize;
if dsym >= DIST_BASE.len() {
return Err("bad dist symbol");
}
let distance = DIST_BASE[dsym] as usize
+ br.bits(DIST_EXTRA[dsym] as u32)? as usize;
if distance == 0 || distance > out.len() {
return Err("bad back-reference");
}
let start = out.len() - distance;
for k in 0..length {
out.push(out[start + k]);
}
}
}
}
_ => return Err("reserved block type"),
}
if final_block == 1 {
break;
}
}
Ok(out)
}
fn zlib_inflate_independent(data: &[u8]) -> Result<Vec<u8>, &'static str> {
if data.len() < 6 {
return Err("zlib too short");
}
if data[0] & 0x0f != 8 {
return Err("zlib: not DEFLATE");
}
inflate_raw(&data[2..data.len() - 4])
}
fn find_chunk<'a>(png: &'a [u8], chunk_type: &[u8; 4]) -> Option<&'a [u8]> {
let mut pos = 8usize; while pos + 8 <= png.len() {
let len =
u32::from_be_bytes([png[pos], png[pos + 1], png[pos + 2], png[pos + 3]]) as usize;
let ty = &png[pos + 4..pos + 8];
let data_start = pos + 8;
let data_end = data_start + len;
if data_end + 4 > png.len() {
return None;
}
if ty == chunk_type {
return Some(&png[data_start..data_end]);
}
pos = data_end + 4; }
None
}
#[test]
fn signature_and_structure() {
let width = 3;
let height = 2;
let rgb: Vec<u8> = vec![
0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 0, 0, 0, 255, 128, 64, 32, ];
let png = encode_rgb8(width, height, &rgb).expect("encode");
assert_eq!(&png[..8], b"\x89PNG\r\n\x1a\n");
let ihdr = find_chunk(&png, b"IHDR").expect("IHDR");
assert_eq!(ihdr.len(), 13);
assert_eq!(
u32::from_be_bytes([ihdr[0], ihdr[1], ihdr[2], ihdr[3]]),
width as u32
);
assert_eq!(
u32::from_be_bytes([ihdr[4], ihdr[5], ihdr[6], ihdr[7]]),
height as u32
);
assert_eq!(ihdr[8], 8, "bit depth");
assert_eq!(ihdr[9], 2, "color type RGB");
assert_eq!(ihdr[10], 0, "compression");
assert_eq!(ihdr[11], 0, "filter");
assert_eq!(ihdr[12], 0, "interlace");
let iend = find_chunk(&png, b"IEND").expect("IEND");
assert!(iend.is_empty());
}
#[test]
fn idat_roundtrips_to_scanlines() {
let width = 4;
let height = 3;
let mut rgb = Vec::with_capacity(width * height * 3);
for y in 0..height {
for x in 0..width {
rgb.push((x * 60) as u8);
rgb.push((y * 80) as u8);
rgb.push(((x + y) * 30) as u8);
}
}
let png = encode_rgb8(width, height, &rgb).expect("encode");
let idat = find_chunk(&png, b"IDAT").expect("IDAT");
let inflated = zlib_decompress(idat).expect("inflate IDAT");
let expected = raw_scanlines(width, height, &rgb);
assert_eq!(
inflated, expected,
"re-inflated IDAT must match the raw scanlines"
);
let row_bytes = width * 3;
let mut recovered = Vec::with_capacity(width * height * 3);
for row in 0..height {
let start = row * (1 + row_bytes);
assert_eq!(inflated[start], 0, "filter byte must be 0 (None)");
recovered.extend_from_slice(&inflated[start + 1..start + 1 + row_bytes]);
}
assert_eq!(recovered, rgb);
}
#[test]
fn idat_decodes_with_independent_inflater() {
let width = 96;
let height = 64;
let mut rgb = Vec::with_capacity(width * height * 3);
let mut state = 0x1234_5678u32;
for _ in 0..width * height {
for _ in 0..3 {
state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
rgb.push((state >> 24) as u8);
}
}
let png = encode_rgb8(width, height, &rgb).expect("encode");
let idat = find_chunk(&png, b"IDAT").expect("IDAT");
let inflated =
zlib_inflate_independent(idat).expect("IDAT must decode with an independent inflater");
let expected = raw_scanlines(width, height, &rgb);
assert_eq!(
inflated, expected,
"independent inflate of IDAT must match the raw scanlines"
);
}
#[test]
fn crc_is_correct_for_iend() {
let png = encode_rgb8(1, 1, &[10, 20, 30]).expect("encode");
let n = png.len();
let crc = u32::from_be_bytes([png[n - 4], png[n - 3], png[n - 2], png[n - 1]]);
assert_eq!(crc, 0xAE42_6082, "IEND CRC");
}
#[test]
fn rejects_bad_length() {
let err = encode_rgb8(2, 2, &[0, 0, 0]).expect_err("should reject short buffer");
match err {
PngError::BadLength { got, expected } => {
assert_eq!(got, 3);
assert_eq!(expected, 12);
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn rejects_zero_dimension() {
let err = encode_rgb8(0, 5, &[]).expect_err("should reject zero width");
assert!(matches!(err, PngError::ZeroDimension { .. }));
}
}