feat(renderer): add PNG decoder with filter reconstruction
Parse PNG files (RGB and RGBA, 8-bit) with full filter reconstruction (None, Sub, Up, Average, Paeth). Uses the self-contained deflate decompressor for IDAT chunk decompression. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
pub mod deflate;
|
||||
pub mod png;
|
||||
pub mod gpu;
|
||||
pub mod light;
|
||||
pub mod obj;
|
||||
@@ -51,3 +53,4 @@ pub use rt_shadow::{RtShadowResources, RtShadowUniform, RT_SHADOW_FORMAT};
|
||||
pub use hdr::{HdrTarget, HDR_FORMAT};
|
||||
pub use bloom::{BloomResources, BloomUniform, mip_sizes, BLOOM_MIP_COUNT};
|
||||
pub use tonemap::{TonemapUniform, aces_tonemap};
|
||||
pub use png::parse_png;
|
||||
|
||||
450
crates/voltex_renderer/src/png.rs
Normal file
450
crates/voltex_renderer/src/png.rs
Normal file
@@ -0,0 +1,450 @@
|
||||
/// PNG decoder that uses the self-contained deflate decompressor.
|
||||
/// Supports 8-bit RGB (color_type=2) and RGBA (color_type=6) images.
|
||||
|
||||
use crate::deflate;
|
||||
|
||||
pub fn parse_png(data: &[u8]) -> Result<(Vec<u8>, u32, u32), String> {
|
||||
// 1. Verify PNG signature
|
||||
let signature: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10];
|
||||
if data.len() < 8 || data[..8] != signature {
|
||||
return Err("Invalid PNG signature".into());
|
||||
}
|
||||
|
||||
let mut pos = 8;
|
||||
let mut width: u32 = 0;
|
||||
let mut height: u32 = 0;
|
||||
let mut bit_depth: u8;
|
||||
let mut color_type: u8 = 0;
|
||||
let mut idat_data = Vec::new();
|
||||
let mut got_ihdr = false;
|
||||
|
||||
// 2. Parse chunks
|
||||
while pos + 8 <= data.len() {
|
||||
let chunk_len = u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
|
||||
let chunk_type = &data[pos + 4..pos + 8];
|
||||
pos += 8;
|
||||
|
||||
if pos + chunk_len > data.len() {
|
||||
return Err("Chunk extends past end of data".into());
|
||||
}
|
||||
let chunk_data = &data[pos..pos + chunk_len];
|
||||
pos += chunk_len;
|
||||
|
||||
// Skip CRC (4 bytes)
|
||||
if pos + 4 > data.len() {
|
||||
return Err("Missing chunk CRC".into());
|
||||
}
|
||||
pos += 4;
|
||||
|
||||
match chunk_type {
|
||||
b"IHDR" => {
|
||||
if chunk_len < 13 {
|
||||
return Err("IHDR chunk too small".into());
|
||||
}
|
||||
width = u32::from_be_bytes([chunk_data[0], chunk_data[1], chunk_data[2], chunk_data[3]]);
|
||||
height = u32::from_be_bytes([chunk_data[4], chunk_data[5], chunk_data[6], chunk_data[7]]);
|
||||
bit_depth = chunk_data[8];
|
||||
color_type = chunk_data[9];
|
||||
let compression = chunk_data[10];
|
||||
let _filter = chunk_data[11];
|
||||
let interlace = chunk_data[12];
|
||||
|
||||
if bit_depth != 8 {
|
||||
return Err(format!("Unsupported bit depth: {} (only 8 supported)", bit_depth));
|
||||
}
|
||||
if color_type != 2 && color_type != 6 {
|
||||
return Err(format!(
|
||||
"Unsupported color type: {} (only RGB=2 and RGBA=6 supported)",
|
||||
color_type
|
||||
));
|
||||
}
|
||||
if compression != 0 {
|
||||
return Err("Unsupported compression method".into());
|
||||
}
|
||||
if interlace != 0 {
|
||||
return Err("Interlaced PNGs not supported".into());
|
||||
}
|
||||
got_ihdr = true;
|
||||
}
|
||||
b"IDAT" => {
|
||||
idat_data.extend_from_slice(chunk_data);
|
||||
}
|
||||
b"IEND" => {
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
// Skip unknown chunks
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !got_ihdr {
|
||||
return Err("Missing IHDR chunk".into());
|
||||
}
|
||||
if idat_data.is_empty() {
|
||||
return Err("No IDAT chunks found".into());
|
||||
}
|
||||
|
||||
// 3. Decompress IDAT data
|
||||
let decompressed = deflate::inflate(&idat_data)?;
|
||||
|
||||
// 4. Apply PNG filters
|
||||
let channels: usize = match color_type {
|
||||
2 => 3, // RGB
|
||||
6 => 4, // RGBA
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let bpp = channels; // bytes per pixel (bit_depth=8)
|
||||
let stride = width as usize * channels;
|
||||
let expected_size = height as usize * (1 + stride); // 1 filter byte per row
|
||||
|
||||
if decompressed.len() < expected_size {
|
||||
return Err(format!(
|
||||
"Decompressed data too small: got {} expected {}",
|
||||
decompressed.len(),
|
||||
expected_size
|
||||
));
|
||||
}
|
||||
|
||||
let mut image_data = vec![0u8; height as usize * stride];
|
||||
let mut prev_row = vec![0u8; stride];
|
||||
|
||||
for y in 0..height as usize {
|
||||
let row_start = y * (1 + stride);
|
||||
let filter_type = decompressed[row_start];
|
||||
let src = &decompressed[row_start + 1..row_start + 1 + stride];
|
||||
|
||||
let dst_start = y * stride;
|
||||
let dst_end = dst_start + stride;
|
||||
image_data[dst_start..dst_end].copy_from_slice(src);
|
||||
|
||||
let (before, current) = image_data.split_at_mut(dst_start);
|
||||
let current = &mut current[..stride];
|
||||
let previous = if y == 0 { &prev_row[..] } else { &before[dst_start - stride..dst_start] };
|
||||
|
||||
reconstruct_row(filter_type, current, previous, bpp)?;
|
||||
|
||||
if y == 0 {
|
||||
prev_row.copy_from_slice(current);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Convert to RGBA if needed
|
||||
let rgba = if color_type == 2 {
|
||||
// RGB -> RGBA
|
||||
let mut rgba = Vec::with_capacity(width as usize * height as usize * 4);
|
||||
for pixel in image_data.chunks_exact(3) {
|
||||
rgba.push(pixel[0]);
|
||||
rgba.push(pixel[1]);
|
||||
rgba.push(pixel[2]);
|
||||
rgba.push(255);
|
||||
}
|
||||
rgba
|
||||
} else {
|
||||
// Already RGBA
|
||||
image_data
|
||||
};
|
||||
|
||||
Ok((rgba, width, height))
|
||||
}
|
||||
|
||||
fn reconstruct_row(filter_type: u8, current: &mut [u8], previous: &[u8], bpp: usize) -> Result<(), String> {
|
||||
match filter_type {
|
||||
0 => {} // None
|
||||
1 => {
|
||||
// Sub
|
||||
for i in bpp..current.len() {
|
||||
current[i] = current[i].wrapping_add(current[i - bpp]);
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
// Up
|
||||
for i in 0..current.len() {
|
||||
current[i] = current[i].wrapping_add(previous[i]);
|
||||
}
|
||||
}
|
||||
3 => {
|
||||
// Average
|
||||
for i in 0..current.len() {
|
||||
let a = if i >= bpp { current[i - bpp] as u16 } else { 0 };
|
||||
let b = previous[i] as u16;
|
||||
current[i] = current[i].wrapping_add(((a + b) / 2) as u8);
|
||||
}
|
||||
}
|
||||
4 => {
|
||||
// Paeth
|
||||
for i in 0..current.len() {
|
||||
let a = if i >= bpp { current[i - bpp] as i32 } else { 0 };
|
||||
let b = previous[i] as i32;
|
||||
let c = if i >= bpp { previous[i - bpp] as i32 } else { 0 };
|
||||
current[i] = current[i].wrapping_add(paeth_predictor(a, b, c) as u8);
|
||||
}
|
||||
}
|
||||
_ => return Err(format!("Unknown PNG filter type: {}", filter_type)),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn paeth_predictor(a: i32, b: i32, c: i32) -> i32 {
|
||||
let p = a + b - c;
|
||||
let pa = (p - a).abs();
|
||||
let pb = (p - b).abs();
|
||||
let pc = (p - c).abs();
|
||||
if pa <= pb && pa <= pc {
|
||||
a
|
||||
} else if pb <= pc {
|
||||
b
|
||||
} else {
|
||||
c
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn crc32(chunk_type: &[u8], data: &[u8]) -> u32 {
|
||||
let mut crc: u32 = 0xFFFFFFFF;
|
||||
for &b in chunk_type.iter().chain(data.iter()) {
|
||||
crc ^= b as u32;
|
||||
for _ in 0..8 {
|
||||
if crc & 1 != 0 {
|
||||
crc = (crc >> 1) ^ 0xEDB88320;
|
||||
} else {
|
||||
crc >>= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
crc ^ 0xFFFFFFFF
|
||||
}
|
||||
|
||||
fn write_chunk(out: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) {
|
||||
out.extend_from_slice(&(data.len() as u32).to_be_bytes());
|
||||
out.extend_from_slice(chunk_type);
|
||||
out.extend_from_slice(data);
|
||||
let crc = crc32(chunk_type, data);
|
||||
out.extend_from_slice(&crc.to_be_bytes());
|
||||
}
|
||||
|
||||
fn adler32(data: &[u8]) -> u32 {
|
||||
let mut a: u32 = 1;
|
||||
let mut b: u32 = 0;
|
||||
for &byte in data {
|
||||
a = (a + byte as u32) % 65521;
|
||||
b = (b + a) % 65521;
|
||||
}
|
||||
(b << 16) | a
|
||||
}
|
||||
|
||||
fn deflate_stored(data: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
out.push(0x78); // CMF
|
||||
out.push(0x01); // FLG
|
||||
|
||||
let chunks: Vec<&[u8]> = data.chunks(65535).collect();
|
||||
if chunks.is_empty() {
|
||||
out.push(0x01);
|
||||
out.extend_from_slice(&0u16.to_le_bytes());
|
||||
out.extend_from_slice(&(!0u16).to_le_bytes());
|
||||
} else {
|
||||
for (i, chunk) in chunks.iter().enumerate() {
|
||||
let bfinal = if i == chunks.len() - 1 { 1u8 } else { 0u8 };
|
||||
out.push(bfinal);
|
||||
let len = chunk.len() as u16;
|
||||
out.extend_from_slice(&len.to_le_bytes());
|
||||
out.extend_from_slice(&(!len).to_le_bytes());
|
||||
out.extend_from_slice(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
let adler = adler32(data);
|
||||
out.extend_from_slice(&adler.to_be_bytes());
|
||||
out
|
||||
}
|
||||
|
||||
fn create_test_png(width: u32, height: u32, rgba_pixels: &[u8]) -> Vec<u8> {
|
||||
let stride = (width as usize) * 4;
|
||||
let mut raw = Vec::new();
|
||||
for y in 0..height as usize {
|
||||
raw.push(0); // filter type None
|
||||
raw.extend_from_slice(&rgba_pixels[y * stride..(y + 1) * stride]);
|
||||
}
|
||||
|
||||
let compressed = deflate_stored(&raw);
|
||||
|
||||
let mut png = Vec::new();
|
||||
png.extend_from_slice(&[137, 80, 78, 71, 13, 10, 26, 10]);
|
||||
|
||||
// IHDR
|
||||
let mut ihdr = Vec::new();
|
||||
ihdr.extend_from_slice(&width.to_be_bytes());
|
||||
ihdr.extend_from_slice(&height.to_be_bytes());
|
||||
ihdr.push(8); // bit depth
|
||||
ihdr.push(6); // color type RGBA
|
||||
ihdr.push(0); // compression
|
||||
ihdr.push(0); // filter
|
||||
ihdr.push(0); // interlace
|
||||
write_chunk(&mut png, b"IHDR", &ihdr);
|
||||
|
||||
// IDAT
|
||||
write_chunk(&mut png, b"IDAT", &compressed);
|
||||
|
||||
// IEND
|
||||
write_chunk(&mut png, b"IEND", &[]);
|
||||
|
||||
png
|
||||
}
|
||||
|
||||
fn create_test_png_rgb(width: u32, height: u32, rgb_pixels: &[u8]) -> Vec<u8> {
|
||||
let stride = (width as usize) * 3;
|
||||
let mut raw = Vec::new();
|
||||
for y in 0..height as usize {
|
||||
raw.push(0); // filter type None
|
||||
raw.extend_from_slice(&rgb_pixels[y * stride..(y + 1) * stride]);
|
||||
}
|
||||
|
||||
let compressed = deflate_stored(&raw);
|
||||
|
||||
let mut png = Vec::new();
|
||||
png.extend_from_slice(&[137, 80, 78, 71, 13, 10, 26, 10]);
|
||||
|
||||
let mut ihdr = Vec::new();
|
||||
ihdr.extend_from_slice(&width.to_be_bytes());
|
||||
ihdr.extend_from_slice(&height.to_be_bytes());
|
||||
ihdr.push(8); // bit depth
|
||||
ihdr.push(2); // color type RGB
|
||||
ihdr.push(0);
|
||||
ihdr.push(0);
|
||||
ihdr.push(0);
|
||||
write_chunk(&mut png, b"IHDR", &ihdr);
|
||||
|
||||
write_chunk(&mut png, b"IDAT", &compressed);
|
||||
write_chunk(&mut png, b"IEND", &[]);
|
||||
|
||||
png
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_small_png_rgba() {
|
||||
let pixels = [
|
||||
255, 0, 0, 255, 0, 255, 0, 255, // row 0: red, green
|
||||
0, 0, 255, 255, 255, 255, 0, 255, // row 1: blue, yellow
|
||||
];
|
||||
let png_data = create_test_png(2, 2, &pixels);
|
||||
let (result, w, h) = parse_png(&png_data).unwrap();
|
||||
assert_eq!(w, 2);
|
||||
assert_eq!(h, 2);
|
||||
assert_eq!(result.len(), 16);
|
||||
// Red pixel
|
||||
assert_eq!(result[0], 255);
|
||||
assert_eq!(result[1], 0);
|
||||
assert_eq!(result[2], 0);
|
||||
assert_eq!(result[3], 255);
|
||||
// Green pixel
|
||||
assert_eq!(result[4], 0);
|
||||
assert_eq!(result[5], 255);
|
||||
assert_eq!(result[6], 0);
|
||||
assert_eq!(result[7], 255);
|
||||
// Blue pixel
|
||||
assert_eq!(result[8], 0);
|
||||
assert_eq!(result[9], 0);
|
||||
assert_eq!(result[10], 255);
|
||||
assert_eq!(result[11], 255);
|
||||
// Yellow pixel
|
||||
assert_eq!(result[12], 255);
|
||||
assert_eq!(result[13], 255);
|
||||
assert_eq!(result[14], 0);
|
||||
assert_eq!(result[15], 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_small_png_rgb() {
|
||||
let pixels = [
|
||||
255, 0, 0, 0, 255, 0, // row 0: red, green
|
||||
0, 0, 255, 255, 255, 0, // row 1: blue, yellow
|
||||
];
|
||||
let png_data = create_test_png_rgb(2, 2, &pixels);
|
||||
let (result, w, h) = parse_png(&png_data).unwrap();
|
||||
assert_eq!(w, 2);
|
||||
assert_eq!(h, 2);
|
||||
assert_eq!(result.len(), 16); // converted to RGBA
|
||||
// Red pixel with alpha=255
|
||||
assert_eq!(&result[0..4], &[255, 0, 0, 255]);
|
||||
// Green pixel
|
||||
assert_eq!(&result[4..8], &[0, 255, 0, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_1x1_png() {
|
||||
let pixels = [128, 64, 32, 200];
|
||||
let png_data = create_test_png(1, 1, &pixels);
|
||||
let (result, w, h) = parse_png(&png_data).unwrap();
|
||||
assert_eq!(w, 1);
|
||||
assert_eq!(h, 1);
|
||||
assert_eq!(result, vec![128, 64, 32, 200]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_signature() {
|
||||
let data = [0u8; 100];
|
||||
assert!(parse_png(&data).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_png_filter_sub() {
|
||||
// Test Sub filter reconstruction
|
||||
let mut row = vec![10, 20, 30, 40, 5, 10, 15, 20];
|
||||
let prev = vec![0u8; 8];
|
||||
reconstruct_row(1, &mut row, &prev, 4).unwrap();
|
||||
// After Sub: bytes 4..8 get previous pixel added
|
||||
assert_eq!(row[4], 5u8.wrapping_add(10)); // 15
|
||||
assert_eq!(row[5], 10u8.wrapping_add(20)); // 30
|
||||
assert_eq!(row[6], 15u8.wrapping_add(30)); // 45
|
||||
assert_eq!(row[7], 20u8.wrapping_add(40)); // 60
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_png_filter_up() {
|
||||
let mut row = vec![10, 20, 30, 40];
|
||||
let prev = vec![5, 10, 15, 20];
|
||||
reconstruct_row(2, &mut row, &prev, 4).unwrap();
|
||||
assert_eq!(row, vec![15, 30, 45, 60]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_png_filter_average() {
|
||||
let mut row = vec![10, 20, 30, 40, 5, 10, 15, 20];
|
||||
let prev = vec![0, 0, 0, 0, 0, 0, 0, 0];
|
||||
reconstruct_row(3, &mut row, &prev, 4).unwrap();
|
||||
// First pixel: avg(0, 0) = 0, so unchanged
|
||||
assert_eq!(row[0], 10);
|
||||
// Second pixel (i=4): avg(row[0]=10, prev[4]=0) = 5, so 5+5=10
|
||||
assert_eq!(row[4], 5u8.wrapping_add(((10u16 + 0u16) / 2) as u8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paeth_predictor() {
|
||||
assert_eq!(paeth_predictor(10, 20, 5), 20);
|
||||
assert_eq!(paeth_predictor(0, 0, 0), 0);
|
||||
assert_eq!(paeth_predictor(100, 100, 100), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_larger_png() {
|
||||
// 8x8 gradient image
|
||||
let mut pixels = Vec::new();
|
||||
for y in 0..8u32 {
|
||||
for x in 0..8u32 {
|
||||
pixels.push((x * 32) as u8);
|
||||
pixels.push((y * 32) as u8);
|
||||
pixels.push(128);
|
||||
pixels.push(255);
|
||||
}
|
||||
}
|
||||
let png_data = create_test_png(8, 8, &pixels);
|
||||
let (result, w, h) = parse_png(&png_data).unwrap();
|
||||
assert_eq!(w, 8);
|
||||
assert_eq!(h, 8);
|
||||
assert_eq!(result, pixels);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user