we (web engine): Experimental web browser project to understand the limits of Claude

Implement PNG decoder (RFC 2083)

Pure Rust PNG decoder supporting all color types (grayscale, RGB,
indexed, grayscale+alpha, RGBA), bit depths 1-16, scanline filtering
(None/Sub/Up/Average/Paeth), Adam7 interlacing, tRNS transparency,
CRC-32 validation, and multiple IDAT chunk concatenation.

40+ unit tests covering all color types, bit depths, filter types,
interlacing, transparency, error cases, and edge cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+1545
+1
crates/image/src/lib.rs
··· 2 2 3 3 pub mod deflate; 4 4 pub mod pixel; 5 + pub mod png; 5 6 pub mod zlib;
+1544
crates/image/src/png.rs
··· 1 + //! PNG decoder (RFC 2083). 2 + //! 3 + //! Decodes PNG images into RGBA8 pixel data. Supports all standard color types 4 + //! (grayscale, RGB, indexed, grayscale+alpha, RGBA), bit depths 1–16, 5 + //! scanline filtering, and Adam7 interlacing. 6 + 7 + use crate::pixel::{self, Image, ImageError}; 8 + use crate::zlib; 9 + 10 + // --------------------------------------------------------------------------- 11 + // CRC-32 12 + // --------------------------------------------------------------------------- 13 + 14 + /// CRC-32 lookup table (polynomial 0xEDB88320, reflected). 15 + const CRC32_TABLE: [u32; 256] = { 16 + let mut table = [0u32; 256]; 17 + let mut i = 0u32; 18 + while i < 256 { 19 + let mut crc = i; 20 + let mut j = 0; 21 + while j < 8 { 22 + if crc & 1 != 0 { 23 + crc = (crc >> 1) ^ 0xEDB8_8320; 24 + } else { 25 + crc >>= 1; 26 + } 27 + j += 1; 28 + } 29 + table[i as usize] = crc; 30 + i += 1; 31 + } 32 + table 33 + }; 34 + 35 + /// Compute CRC-32 over a byte slice. 36 + fn crc32(data: &[u8]) -> u32 { 37 + let mut crc = 0xFFFF_FFFFu32; 38 + for &b in data { 39 + let idx = ((crc ^ b as u32) & 0xFF) as usize; 40 + crc = CRC32_TABLE[idx] ^ (crc >> 8); 41 + } 42 + crc ^ 0xFFFF_FFFF 43 + } 44 + 45 + // --------------------------------------------------------------------------- 46 + // PNG constants 47 + // --------------------------------------------------------------------------- 48 + 49 + /// 8-byte PNG file signature. 50 + const PNG_SIGNATURE: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10]; 51 + 52 + // Chunk type tags (as big-endian u32). 53 + const CHUNK_IHDR: u32 = u32::from_be_bytes(*b"IHDR"); 54 + const CHUNK_PLTE: u32 = u32::from_be_bytes(*b"PLTE"); 55 + const CHUNK_IDAT: u32 = u32::from_be_bytes(*b"IDAT"); 56 + const CHUNK_IEND: u32 = u32::from_be_bytes(*b"IEND"); 57 + const CHUNK_TRNS: u32 = u32::from_be_bytes(*b"tRNS"); 58 + 59 + // Color type flags. 60 + const COLOR_GRAYSCALE: u8 = 0; 61 + const COLOR_RGB: u8 = 2; 62 + const COLOR_INDEXED: u8 = 3; 63 + const COLOR_GRAYSCALE_ALPHA: u8 = 4; 64 + const COLOR_RGBA: u8 = 6; 65 + 66 + // Filter types. 67 + const FILTER_NONE: u8 = 0; 68 + const FILTER_SUB: u8 = 1; 69 + const FILTER_UP: u8 = 2; 70 + const FILTER_AVERAGE: u8 = 3; 71 + const FILTER_PAETH: u8 = 4; 72 + 73 + // Adam7 interlace pass parameters: (x_start, y_start, x_step, y_step) 74 + const ADAM7_PASSES: [(usize, usize, usize, usize); 7] = [ 75 + (0, 0, 8, 8), 76 + (4, 0, 8, 8), 77 + (0, 4, 4, 8), 78 + (2, 0, 4, 4), 79 + (0, 2, 2, 4), 80 + (1, 0, 2, 2), 81 + (0, 1, 1, 2), 82 + ]; 83 + 84 + // --------------------------------------------------------------------------- 85 + // IHDR 86 + // --------------------------------------------------------------------------- 87 + 88 + #[derive(Debug, Clone)] 89 + struct Ihdr { 90 + width: u32, 91 + height: u32, 92 + bit_depth: u8, 93 + color_type: u8, 94 + interlace: u8, 95 + } 96 + 97 + impl Ihdr { 98 + fn parse(data: &[u8]) -> Result<Self, ImageError> { 99 + if data.len() != 13 { 100 + return Err(decode_err("IHDR chunk must be 13 bytes")); 101 + } 102 + let width = read_u32_be(data, 0); 103 + let height = read_u32_be(data, 4); 104 + let bit_depth = data[8]; 105 + let color_type = data[9]; 106 + let compression = data[10]; 107 + let filter = data[11]; 108 + let interlace = data[12]; 109 + 110 + if width == 0 || height == 0 { 111 + return Err(ImageError::ZeroDimension { width, height }); 112 + } 113 + if compression != 0 { 114 + return Err(decode_err("unsupported compression method")); 115 + } 116 + if filter != 0 { 117 + return Err(decode_err("unsupported filter method")); 118 + } 119 + if interlace > 1 { 120 + return Err(decode_err("unsupported interlace method")); 121 + } 122 + 123 + // Validate bit depth for each color type per PNG spec. 124 + let valid = match color_type { 125 + COLOR_GRAYSCALE => matches!(bit_depth, 1 | 2 | 4 | 8 | 16), 126 + COLOR_RGB => matches!(bit_depth, 8 | 16), 127 + COLOR_INDEXED => matches!(bit_depth, 1 | 2 | 4 | 8), 128 + COLOR_GRAYSCALE_ALPHA => matches!(bit_depth, 8 | 16), 129 + COLOR_RGBA => matches!(bit_depth, 8 | 16), 130 + _ => false, 131 + }; 132 + if !valid { 133 + return Err(decode_err(&format!( 134 + "invalid bit_depth={bit_depth} for color_type={color_type}" 135 + ))); 136 + } 137 + 138 + Ok(Self { 139 + width, 140 + height, 141 + bit_depth, 142 + color_type, 143 + interlace, 144 + }) 145 + } 146 + 147 + /// Number of channels for this color type. 148 + fn channels(&self) -> usize { 149 + match self.color_type { 150 + COLOR_GRAYSCALE => 1, 151 + COLOR_RGB => 3, 152 + COLOR_INDEXED => 1, 153 + COLOR_GRAYSCALE_ALPHA => 2, 154 + COLOR_RGBA => 4, 155 + _ => 1, 156 + } 157 + } 158 + 159 + /// Bytes per pixel (for filter byte offset), minimum 1. 160 + fn bytes_per_pixel(&self) -> usize { 161 + let bits = self.channels() * self.bit_depth as usize; 162 + std::cmp::max(1, bits / 8) 163 + } 164 + 165 + /// Bytes per scanline row (excluding the filter byte), for given width. 166 + fn row_bytes(&self, width: u32) -> usize { 167 + let bits_per_pixel = self.channels() * self.bit_depth as usize; 168 + let total_bits = bits_per_pixel * width as usize; 169 + total_bits.div_ceil(8) 170 + } 171 + } 172 + 173 + // --------------------------------------------------------------------------- 174 + // Transparency info 175 + // --------------------------------------------------------------------------- 176 + 177 + #[derive(Debug, Clone)] 178 + enum Transparency { 179 + /// Grayscale transparent value (color type 0). 180 + Gray(u16), 181 + /// RGB transparent value (color type 2). 182 + Rgb(u16, u16, u16), 183 + /// Per-palette-entry alpha values (color type 3). 184 + Palette(Vec<u8>), 185 + } 186 + 187 + // --------------------------------------------------------------------------- 188 + // Chunk reader 189 + // --------------------------------------------------------------------------- 190 + 191 + struct ChunkReader<'a> { 192 + data: &'a [u8], 193 + pos: usize, 194 + } 195 + 196 + struct Chunk<'a> { 197 + chunk_type: u32, 198 + data: &'a [u8], 199 + } 200 + 201 + impl<'a> ChunkReader<'a> { 202 + fn new(data: &'a [u8]) -> Result<Self, ImageError> { 203 + if data.len() < 8 || data[..8] != PNG_SIGNATURE { 204 + return Err(decode_err("invalid PNG signature")); 205 + } 206 + Ok(Self { data, pos: 8 }) 207 + } 208 + 209 + fn next_chunk(&mut self) -> Result<Chunk<'a>, ImageError> { 210 + if self.pos + 12 > self.data.len() { 211 + return Err(decode_err("unexpected end of PNG data")); 212 + } 213 + let length = read_u32_be(self.data, self.pos) as usize; 214 + let chunk_type_bytes = &self.data[self.pos + 4..self.pos + 8]; 215 + let chunk_type = u32::from_be_bytes([ 216 + chunk_type_bytes[0], 217 + chunk_type_bytes[1], 218 + chunk_type_bytes[2], 219 + chunk_type_bytes[3], 220 + ]); 221 + 222 + let data_start = self.pos + 8; 223 + let data_end = data_start + length; 224 + let crc_end = data_end + 4; 225 + 226 + if crc_end > self.data.len() { 227 + return Err(decode_err("chunk extends beyond PNG data")); 228 + } 229 + 230 + let chunk_data = &self.data[data_start..data_end]; 231 + 232 + // CRC covers chunk type + chunk data. 233 + let crc_input = &self.data[self.pos + 4..data_end]; 234 + let stored_crc = read_u32_be(self.data, data_end); 235 + let computed_crc = crc32(crc_input); 236 + if stored_crc != computed_crc { 237 + return Err(decode_err(&format!( 238 + "CRC mismatch in chunk {:?}: stored={stored_crc:#010x}, computed={computed_crc:#010x}", 239 + std::str::from_utf8(chunk_type_bytes).unwrap_or("????") 240 + ))); 241 + } 242 + 243 + self.pos = crc_end; 244 + 245 + Ok(Chunk { 246 + chunk_type, 247 + data: chunk_data, 248 + }) 249 + } 250 + } 251 + 252 + // --------------------------------------------------------------------------- 253 + // Scanline filter reconstruction 254 + // --------------------------------------------------------------------------- 255 + 256 + /// Paeth predictor function (PNG spec). 257 + fn paeth_predictor(a: u8, b: u8, c: u8) -> u8 { 258 + let a = a as i16; 259 + let b = b as i16; 260 + let c = c as i16; 261 + let p = a + b - c; 262 + let pa = (p - a).abs(); 263 + let pb = (p - b).abs(); 264 + let pc = (p - c).abs(); 265 + if pa <= pb && pa <= pc { 266 + a as u8 267 + } else if pb <= pc { 268 + b as u8 269 + } else { 270 + c as u8 271 + } 272 + } 273 + 274 + /// Reconstruct filtered scanline data in-place. 275 + /// 276 + /// `data` contains all scanlines concatenated, each prefixed by a filter byte. 277 + /// `row_bytes` is the number of data bytes per row (excluding filter byte). 278 + /// `bpp` is the bytes-per-pixel (minimum 1). 279 + fn unfilter( 280 + data: &mut [u8], 281 + row_bytes: usize, 282 + height: usize, 283 + bpp: usize, 284 + ) -> Result<(), ImageError> { 285 + let stride = row_bytes + 1; // filter byte + row data 286 + if data.len() != stride * height { 287 + return Err(decode_err("decompressed data size mismatch")); 288 + } 289 + 290 + for y in 0..height { 291 + let row_start = y * stride; 292 + let filter_type = data[row_start]; 293 + // Work on a copy of the previous row for the Up/Average/Paeth filters. 294 + // We reconstruct in-place, so we need prior row data before modification. 295 + // Since rows are processed top-to-bottom, the previous row (y-1) is already 296 + // reconstructed at this point. 297 + 298 + match filter_type { 299 + FILTER_NONE => {} 300 + FILTER_SUB => { 301 + for i in bpp..row_bytes { 302 + let cur = row_start + 1 + i; 303 + let left = data[cur - bpp]; 304 + data[cur] = data[cur].wrapping_add(left); 305 + } 306 + } 307 + FILTER_UP => { 308 + if y > 0 { 309 + let prev_start = (y - 1) * stride + 1; 310 + for i in 0..row_bytes { 311 + let cur = row_start + 1 + i; 312 + let up = data[prev_start + i]; 313 + data[cur] = data[cur].wrapping_add(up); 314 + } 315 + } 316 + } 317 + FILTER_AVERAGE => { 318 + for i in 0..row_bytes { 319 + let cur = row_start + 1 + i; 320 + let left = if i >= bpp { data[cur - bpp] } else { 0 }; 321 + let up = if y > 0 { 322 + data[(y - 1) * stride + 1 + i] 323 + } else { 324 + 0 325 + }; 326 + let avg = ((left as u16 + up as u16) / 2) as u8; 327 + data[cur] = data[cur].wrapping_add(avg); 328 + } 329 + } 330 + FILTER_PAETH => { 331 + for i in 0..row_bytes { 332 + let cur = row_start + 1 + i; 333 + let left = if i >= bpp { data[cur - bpp] } else { 0 }; 334 + let up = if y > 0 { 335 + data[(y - 1) * stride + 1 + i] 336 + } else { 337 + 0 338 + }; 339 + let upper_left = if y > 0 && i >= bpp { 340 + data[(y - 1) * stride + 1 + i - bpp] 341 + } else { 342 + 0 343 + }; 344 + data[cur] = data[cur].wrapping_add(paeth_predictor(left, up, upper_left)); 345 + } 346 + } 347 + _ => return Err(decode_err(&format!("unknown filter type: {filter_type}"))), 348 + } 349 + } 350 + 351 + Ok(()) 352 + } 353 + 354 + /// Extract raw pixel bytes from unfiltered data (strip filter bytes). 355 + fn strip_filter_bytes(data: &[u8], row_bytes: usize, height: usize) -> Vec<u8> { 356 + let stride = row_bytes + 1; 357 + let mut out = Vec::with_capacity(row_bytes * height); 358 + for y in 0..height { 359 + let row_start = y * stride + 1; // skip filter byte 360 + out.extend_from_slice(&data[row_start..row_start + row_bytes]); 361 + } 362 + out 363 + } 364 + 365 + // --------------------------------------------------------------------------- 366 + // Bit depth expansion 367 + // --------------------------------------------------------------------------- 368 + 369 + /// Expand sub-byte samples (1, 2, 4 bits) to 8-bit values for a row of `width` samples. 370 + fn expand_sub_byte(row: &[u8], bit_depth: u8, width: usize) -> Vec<u8> { 371 + let mut out = Vec::with_capacity(width); 372 + let mask = (1u8 << bit_depth) - 1; 373 + let scale = 255 / mask; 374 + let samples_per_byte = 8 / bit_depth as usize; 375 + 376 + let mut sample_idx = 0; 377 + for &byte in row { 378 + for shift_idx in 0..samples_per_byte { 379 + if sample_idx >= width { 380 + break; 381 + } 382 + let shift = 8 - bit_depth * (shift_idx as u8 + 1); 383 + let val = (byte >> shift) & mask; 384 + out.push(val * scale); 385 + sample_idx += 1; 386 + } 387 + } 388 + out 389 + } 390 + 391 + /// Expand 16-bit samples to 8-bit by taking the high byte. 392 + fn downconvert_16_to_8(data: &[u8]) -> Vec<u8> { 393 + let mut out = Vec::with_capacity(data.len() / 2); 394 + for pair in data.chunks_exact(2) { 395 + out.push(pair[0]); // high byte 396 + } 397 + out 398 + } 399 + 400 + // --------------------------------------------------------------------------- 401 + // Color conversion to RGBA8 402 + // --------------------------------------------------------------------------- 403 + 404 + /// Convert raw pixel data to RGBA8, handling all color types and bit depths. 405 + fn to_rgba8( 406 + ihdr: &Ihdr, 407 + raw: &[u8], 408 + width: u32, 409 + height: u32, 410 + palette: Option<&[u8]>, 411 + transparency: Option<&Transparency>, 412 + ) -> Result<Vec<u8>, ImageError> { 413 + let pixel_count = width as usize * height as usize; 414 + 415 + match ihdr.color_type { 416 + COLOR_GRAYSCALE => { 417 + let samples = if ihdr.bit_depth < 8 { 418 + expand_sub_byte(raw, ihdr.bit_depth, pixel_count) 419 + } else if ihdr.bit_depth == 16 { 420 + downconvert_16_to_8(raw) 421 + } else { 422 + raw.to_vec() 423 + }; 424 + 425 + // Apply transparency if present. 426 + if let Some(Transparency::Gray(trans_val)) = transparency { 427 + let tv = if ihdr.bit_depth == 16 { 428 + (*trans_val >> 8) as u8 429 + } else if ihdr.bit_depth < 8 { 430 + let mask = (1u16 << ihdr.bit_depth) - 1; 431 + let scale = 255 / mask; 432 + (*trans_val & (mask * scale)) as u8 433 + } else { 434 + *trans_val as u8 435 + }; 436 + let mut rgba = Vec::with_capacity(pixel_count * 4); 437 + for &g in &samples { 438 + rgba.push(g); 439 + rgba.push(g); 440 + rgba.push(g); 441 + rgba.push(if g == tv { 0 } else { 255 }); 442 + } 443 + return Ok(rgba); 444 + } 445 + 446 + let img = pixel::from_grayscale(width, height, &samples)?; 447 + Ok(img.data) 448 + } 449 + 450 + COLOR_RGB => { 451 + let samples = if ihdr.bit_depth == 16 { 452 + downconvert_16_to_8(raw) 453 + } else { 454 + raw.to_vec() 455 + }; 456 + 457 + if let Some(Transparency::Rgb(tr, tg, tb)) = transparency { 458 + let (tvr, tvg, tvb) = if ihdr.bit_depth == 16 { 459 + ((*tr >> 8) as u8, (*tg >> 8) as u8, (*tb >> 8) as u8) 460 + } else { 461 + (*tr as u8, *tg as u8, *tb as u8) 462 + }; 463 + let mut rgba = Vec::with_capacity(pixel_count * 4); 464 + for triple in samples.chunks_exact(3) { 465 + let r = triple[0]; 466 + let g = triple[1]; 467 + let b = triple[2]; 468 + rgba.push(r); 469 + rgba.push(g); 470 + rgba.push(b); 471 + rgba.push(if r == tvr && g == tvg && b == tvb { 472 + 0 473 + } else { 474 + 255 475 + }); 476 + } 477 + return Ok(rgba); 478 + } 479 + 480 + let img = pixel::from_rgb(width, height, &samples)?; 481 + Ok(img.data) 482 + } 483 + 484 + COLOR_INDEXED => { 485 + let pal = palette.ok_or_else(|| decode_err("missing PLTE chunk for indexed image"))?; 486 + 487 + let indices = if ihdr.bit_depth < 8 { 488 + // For indexed images, don't scale — we want raw index values. 489 + let mask = (1u8 << ihdr.bit_depth) - 1; 490 + let samples_per_byte = 8 / ihdr.bit_depth as usize; 491 + let mut out = Vec::with_capacity(pixel_count); 492 + let row_bytes = ihdr.row_bytes(width); 493 + for y in 0..height as usize { 494 + let row = &raw[y * row_bytes..(y + 1) * row_bytes]; 495 + let mut sample_idx = 0; 496 + for &byte in row { 497 + for shift_idx in 0..samples_per_byte { 498 + if sample_idx >= width as usize { 499 + break; 500 + } 501 + let shift = 8 - ihdr.bit_depth * (shift_idx as u8 + 1); 502 + let val = (byte >> shift) & mask; 503 + out.push(val); 504 + sample_idx += 1; 505 + } 506 + } 507 + } 508 + out 509 + } else { 510 + raw.to_vec() 511 + }; 512 + 513 + if let Some(Transparency::Palette(alpha)) = transparency { 514 + let img = pixel::from_indexed_alpha(width, height, pal, alpha, &indices)?; 515 + Ok(img.data) 516 + } else { 517 + let img = pixel::from_indexed(width, height, pal, &indices)?; 518 + Ok(img.data) 519 + } 520 + } 521 + 522 + COLOR_GRAYSCALE_ALPHA => { 523 + let samples = if ihdr.bit_depth == 16 { 524 + downconvert_16_to_8(raw) 525 + } else { 526 + raw.to_vec() 527 + }; 528 + let img = pixel::from_grayscale_alpha(width, height, &samples)?; 529 + Ok(img.data) 530 + } 531 + 532 + COLOR_RGBA => { 533 + let samples = if ihdr.bit_depth == 16 { 534 + downconvert_16_to_8(raw) 535 + } else { 536 + raw.to_vec() 537 + }; 538 + let img = pixel::from_rgba(width, height, samples)?; 539 + Ok(img.data) 540 + } 541 + 542 + _ => Err(decode_err(&format!( 543 + "unsupported color type: {}", 544 + ihdr.color_type 545 + ))), 546 + } 547 + } 548 + 549 + // --------------------------------------------------------------------------- 550 + // Adam7 interlacing 551 + // --------------------------------------------------------------------------- 552 + 553 + /// Decode Adam7-interlaced image data. 554 + fn decode_adam7( 555 + decompressed: &[u8], 556 + ihdr: &Ihdr, 557 + palette: Option<&[u8]>, 558 + transparency: Option<&Transparency>, 559 + ) -> Result<Vec<u8>, ImageError> { 560 + let full_width = ihdr.width as usize; 561 + let full_height = ihdr.height as usize; 562 + let mut final_rgba = vec![0u8; full_width * full_height * 4]; 563 + 564 + let mut offset = 0; 565 + 566 + for &(x_start, y_start, x_step, y_step) in &ADAM7_PASSES { 567 + // Calculate pass dimensions. 568 + let pass_width = if x_start >= full_width { 569 + 0 570 + } else { 571 + (full_width - x_start).div_ceil(x_step) 572 + }; 573 + let pass_height = if y_start >= full_height { 574 + 0 575 + } else { 576 + (full_height - y_start).div_ceil(y_step) 577 + }; 578 + 579 + if pass_width == 0 || pass_height == 0 { 580 + continue; 581 + } 582 + 583 + let row_bytes = ihdr.row_bytes(pass_width as u32); 584 + let stride = row_bytes + 1; // filter byte + data 585 + let pass_data_len = stride * pass_height; 586 + 587 + if offset + pass_data_len > decompressed.len() { 588 + return Err(decode_err("interlaced data too short")); 589 + } 590 + 591 + let mut pass_data = decompressed[offset..offset + pass_data_len].to_vec(); 592 + offset += pass_data_len; 593 + 594 + unfilter( 595 + &mut pass_data, 596 + row_bytes, 597 + pass_height, 598 + ihdr.bytes_per_pixel(), 599 + )?; 600 + let raw = strip_filter_bytes(&pass_data, row_bytes, pass_height); 601 + 602 + let pass_rgba = to_rgba8( 603 + ihdr, 604 + &raw, 605 + pass_width as u32, 606 + pass_height as u32, 607 + palette, 608 + transparency, 609 + )?; 610 + 611 + // Place pass pixels into the final image. 612 + for py in 0..pass_height { 613 + for px in 0..pass_width { 614 + let fx = x_start + px * x_step; 615 + let fy = y_start + py * y_step; 616 + if fx < full_width && fy < full_height { 617 + let src = (py * pass_width + px) * 4; 618 + let dst = (fy * full_width + fx) * 4; 619 + final_rgba[dst..dst + 4].copy_from_slice(&pass_rgba[src..src + 4]); 620 + } 621 + } 622 + } 623 + } 624 + 625 + Ok(final_rgba) 626 + } 627 + 628 + // --------------------------------------------------------------------------- 629 + // Public API 630 + // --------------------------------------------------------------------------- 631 + 632 + /// Decode a PNG image from raw bytes. 633 + /// 634 + /// Returns an `Image` with RGBA8 pixel data. 635 + pub fn decode_png(data: &[u8]) -> Result<Image, ImageError> { 636 + let mut reader = ChunkReader::new(data)?; 637 + 638 + // First chunk must be IHDR. 639 + let ihdr_chunk = reader.next_chunk()?; 640 + if ihdr_chunk.chunk_type != CHUNK_IHDR { 641 + return Err(decode_err("first chunk must be IHDR")); 642 + } 643 + let ihdr = Ihdr::parse(ihdr_chunk.data)?; 644 + 645 + let mut palette: Option<Vec<u8>> = None; 646 + let mut transparency: Option<Transparency> = None; 647 + let mut idat_data: Vec<u8> = Vec::new(); 648 + 649 + // Read remaining chunks. 650 + loop { 651 + let chunk = reader.next_chunk()?; 652 + 653 + match chunk.chunk_type { 654 + CHUNK_PLTE => { 655 + if chunk.data.len() % 3 != 0 || chunk.data.is_empty() { 656 + return Err(decode_err("invalid PLTE chunk length")); 657 + } 658 + palette = Some(chunk.data.to_vec()); 659 + } 660 + CHUNK_TRNS => { 661 + let trans = match ihdr.color_type { 662 + COLOR_GRAYSCALE => { 663 + if chunk.data.len() != 2 { 664 + return Err(decode_err("invalid tRNS length for grayscale")); 665 + } 666 + Transparency::Gray(read_u16_be(chunk.data, 0)) 667 + } 668 + COLOR_RGB => { 669 + if chunk.data.len() != 6 { 670 + return Err(decode_err("invalid tRNS length for RGB")); 671 + } 672 + Transparency::Rgb( 673 + read_u16_be(chunk.data, 0), 674 + read_u16_be(chunk.data, 2), 675 + read_u16_be(chunk.data, 4), 676 + ) 677 + } 678 + COLOR_INDEXED => Transparency::Palette(chunk.data.to_vec()), 679 + _ => { 680 + return Err(decode_err("tRNS not allowed for this color type")); 681 + } 682 + }; 683 + transparency = Some(trans); 684 + } 685 + CHUNK_IDAT => { 686 + idat_data.extend_from_slice(chunk.data); 687 + } 688 + CHUNK_IEND => break, 689 + _ => { 690 + // Unknown or ancillary chunk — skip. 691 + // If it's critical (uppercase first letter), that's an error. 692 + let first_byte = (chunk.chunk_type >> 24) as u8; 693 + if first_byte.is_ascii_uppercase() { 694 + return Err(decode_err(&format!( 695 + "unknown critical chunk: {:?}", 696 + std::str::from_utf8(&chunk.chunk_type.to_be_bytes()).unwrap_or("????") 697 + ))); 698 + } 699 + } 700 + } 701 + } 702 + 703 + if idat_data.is_empty() { 704 + return Err(decode_err("no IDAT chunks found")); 705 + } 706 + 707 + // Validate palette requirement for indexed images. 708 + if ihdr.color_type == COLOR_INDEXED && palette.is_none() { 709 + return Err(decode_err("missing PLTE chunk for indexed image")); 710 + } 711 + 712 + // Decompress IDAT data (zlib). 713 + let decompressed = 714 + zlib::zlib_decompress(&idat_data).map_err(|e| ImageError::Decode(format!("zlib: {e}")))?; 715 + 716 + let pal_ref = palette.as_deref(); 717 + let trans_ref = transparency.as_ref(); 718 + 719 + if ihdr.interlace == 1 { 720 + // Adam7 interlaced. 721 + let rgba = decode_adam7(&decompressed, &ihdr, pal_ref, trans_ref)?; 722 + Image::new(ihdr.width, ihdr.height, rgba) 723 + } else { 724 + // Non-interlaced. 725 + let row_bytes = ihdr.row_bytes(ihdr.width); 726 + let mut data_buf = decompressed; 727 + unfilter( 728 + &mut data_buf, 729 + row_bytes, 730 + ihdr.height as usize, 731 + ihdr.bytes_per_pixel(), 732 + )?; 733 + let raw = strip_filter_bytes(&data_buf, row_bytes, ihdr.height as usize); 734 + let rgba = to_rgba8(&ihdr, &raw, ihdr.width, ihdr.height, pal_ref, trans_ref)?; 735 + Image::new(ihdr.width, ihdr.height, rgba) 736 + } 737 + } 738 + 739 + // --------------------------------------------------------------------------- 740 + // Helpers 741 + // --------------------------------------------------------------------------- 742 + 743 + fn read_u32_be(data: &[u8], offset: usize) -> u32 { 744 + u32::from_be_bytes([ 745 + data[offset], 746 + data[offset + 1], 747 + data[offset + 2], 748 + data[offset + 3], 749 + ]) 750 + } 751 + 752 + fn read_u16_be(data: &[u8], offset: usize) -> u16 { 753 + u16::from_be_bytes([data[offset], data[offset + 1]]) 754 + } 755 + 756 + fn decode_err(msg: &str) -> ImageError { 757 + ImageError::Decode(msg.to_string()) 758 + } 759 + 760 + // --------------------------------------------------------------------------- 761 + // Tests 762 + // --------------------------------------------------------------------------- 763 + 764 + #[cfg(test)] 765 + mod tests { 766 + use super::*; 767 + 768 + // -- CRC-32 tests -- 769 + 770 + #[test] 771 + fn crc32_empty() { 772 + assert_eq!(crc32(&[]), 0x0000_0000); 773 + } 774 + 775 + #[test] 776 + fn crc32_known_value() { 777 + // CRC-32 of "IEND" (the IEND chunk type bytes) 778 + assert_eq!(crc32(b"IEND"), 0xAE42_6082); 779 + } 780 + 781 + #[test] 782 + fn crc32_abc() { 783 + // Known CRC-32 of "abc" = 0x352441C2 784 + assert_eq!(crc32(b"abc"), 0x352441C2); 785 + } 786 + 787 + #[test] 788 + fn crc32_check_value() { 789 + // The CRC-32 check value: CRC of "123456789" = 0xCBF43926 790 + assert_eq!(crc32(b"123456789"), 0xCBF43926); 791 + } 792 + 793 + // -- PNG signature tests -- 794 + 795 + #[test] 796 + fn invalid_signature() { 797 + let data = [0u8; 8]; 798 + assert!(decode_png(&data).is_err()); 799 + } 800 + 801 + #[test] 802 + fn too_short() { 803 + assert!(decode_png(&[]).is_err()); 804 + assert!(decode_png(&[137, 80, 78, 71]).is_err()); 805 + } 806 + 807 + // -- Paeth predictor tests -- 808 + 809 + #[test] 810 + fn paeth_predictor_basic() { 811 + // When a=b=c=0, should return 0 812 + assert_eq!(paeth_predictor(0, 0, 0), 0); 813 + // When a=1, b=0, c=0: p=1, pa=0, pb=1, pc=1 → a=1 814 + assert_eq!(paeth_predictor(1, 0, 0), 1); 815 + // When a=0, b=1, c=0: p=1, pa=1, pb=0, pc=1 → b=1 816 + assert_eq!(paeth_predictor(0, 1, 0), 1); 817 + // When a=0, b=0, c=1: p=-1, pa=1, pb=1, pc=2 → a=0 818 + assert_eq!(paeth_predictor(0, 0, 1), 0); 819 + } 820 + 821 + // -- Sub-byte expansion tests -- 822 + 823 + #[test] 824 + fn expand_1bit() { 825 + // 0b10110000 with 4 samples → 255, 0, 255, 255 826 + let row = [0b1011_0000]; 827 + let expanded = expand_sub_byte(&row, 1, 4); 828 + assert_eq!(expanded, vec![255, 0, 255, 255]); 829 + } 830 + 831 + #[test] 832 + fn expand_2bit() { 833 + // 0b11_10_01_00 → 255, 170, 85, 0 834 + let row = [0b1110_0100]; 835 + let expanded = expand_sub_byte(&row, 2, 4); 836 + assert_eq!(expanded, vec![255, 170, 85, 0]); 837 + } 838 + 839 + #[test] 840 + fn expand_4bit() { 841 + // 0xF0 → high nibble=15→255, low nibble=0→0 842 + let row = [0xF0]; 843 + let expanded = expand_sub_byte(&row, 4, 2); 844 + assert_eq!(expanded, vec![255, 0]); 845 + } 846 + 847 + // -- IHDR validation tests -- 848 + 849 + #[test] 850 + fn ihdr_valid_rgb8() { 851 + let mut data = [0u8; 13]; 852 + // width=1, height=1 853 + data[0..4].copy_from_slice(&1u32.to_be_bytes()); 854 + data[4..8].copy_from_slice(&1u32.to_be_bytes()); 855 + data[8] = 8; // bit depth 856 + data[9] = 2; // color type RGB 857 + let ihdr = Ihdr::parse(&data).unwrap(); 858 + assert_eq!(ihdr.channels(), 3); 859 + assert_eq!(ihdr.bytes_per_pixel(), 3); 860 + assert_eq!(ihdr.row_bytes(1), 3); 861 + } 862 + 863 + #[test] 864 + fn ihdr_invalid_bit_depth() { 865 + let mut data = [0u8; 13]; 866 + data[0..4].copy_from_slice(&1u32.to_be_bytes()); 867 + data[4..8].copy_from_slice(&1u32.to_be_bytes()); 868 + data[8] = 3; // invalid bit depth for RGB 869 + data[9] = 2; // color type RGB 870 + assert!(Ihdr::parse(&data).is_err()); 871 + } 872 + 873 + #[test] 874 + fn ihdr_zero_dimensions() { 875 + let mut data = [0u8; 13]; 876 + data[0..4].copy_from_slice(&0u32.to_be_bytes()); 877 + data[4..8].copy_from_slice(&1u32.to_be_bytes()); 878 + data[8] = 8; 879 + data[9] = 2; 880 + assert!(matches!( 881 + Ihdr::parse(&data), 882 + Err(ImageError::ZeroDimension { .. }) 883 + )); 884 + } 885 + 886 + // -- Unfilter tests -- 887 + 888 + #[test] 889 + fn unfilter_none() { 890 + // 2x1 image, 3 bytes per pixel (RGB), filter=0 891 + let mut data = vec![FILTER_NONE, 10, 20, 30, 40, 50, 60]; 892 + unfilter(&mut data, 6, 1, 3).unwrap(); 893 + assert_eq!(data, vec![FILTER_NONE, 10, 20, 30, 40, 50, 60]); 894 + } 895 + 896 + #[test] 897 + fn unfilter_sub() { 898 + // 2x1 RGB: filter=1, first pixel=(10,20,30), second pixel delta=(5,5,5) 899 + let mut data = vec![FILTER_SUB, 10, 20, 30, 5, 5, 5]; 900 + unfilter(&mut data, 6, 1, 3).unwrap(); 901 + // After Sub: second pixel = (5+10, 5+20, 5+30) = (15, 25, 35) 902 + assert_eq!(data, vec![FILTER_SUB, 10, 20, 30, 15, 25, 35]); 903 + } 904 + 905 + #[test] 906 + fn unfilter_up() { 907 + // 1x2 RGB: row 0 filter=0 (10,20,30), row 1 filter=2 (5,5,5) 908 + let mut data = vec![FILTER_NONE, 10, 20, 30, FILTER_UP, 5, 5, 5]; 909 + unfilter(&mut data, 3, 2, 3).unwrap(); 910 + // After Up: row1 = (5+10, 5+20, 5+30) = (15, 25, 35) 911 + assert_eq!(data, vec![FILTER_NONE, 10, 20, 30, FILTER_UP, 15, 25, 35,]); 912 + } 913 + 914 + #[test] 915 + fn unfilter_average() { 916 + // 2x1 grayscale: filter=3, values [100, 80] 917 + // bpp=1, left(0)=0, up(0)=0 → avg=0 → 100+0=100 918 + // left(1)=100, up(1)=0 → avg=50 → 80+50=130 919 + let mut data = vec![FILTER_AVERAGE, 100, 80]; 920 + unfilter(&mut data, 2, 1, 1).unwrap(); 921 + assert_eq!(data, vec![FILTER_AVERAGE, 100, 130]); 922 + } 923 + 924 + #[test] 925 + fn unfilter_paeth() { 926 + // 2x1 grayscale: filter=4, values [100, 10] 927 + // bpp=1 928 + // Pixel 0: a=0, b=0, c=0, paeth=0 → 100+0=100 929 + // Pixel 1: a=100, b=0, c=0, paeth=paeth(100,0,0) 930 + // p=100, pa=0, pb=100, pc=100 → a=100 → 10+100=110 931 + let mut data = vec![FILTER_PAETH, 100, 10]; 932 + unfilter(&mut data, 2, 1, 1).unwrap(); 933 + assert_eq!(data, vec![FILTER_PAETH, 100, 110]); 934 + } 935 + 936 + // -- Minimal valid PNG construction helpers -- 937 + 938 + /// Build a minimal valid PNG file from components. 939 + fn build_png( 940 + ihdr: &Ihdr, 941 + palette: Option<&[u8]>, 942 + trns: Option<&[u8]>, 943 + image_data: &[u8], 944 + ) -> Vec<u8> { 945 + let mut png = Vec::new(); 946 + png.extend_from_slice(&PNG_SIGNATURE); 947 + 948 + // IHDR chunk 949 + let mut ihdr_data = Vec::with_capacity(13); 950 + ihdr_data.extend_from_slice(&ihdr.width.to_be_bytes()); 951 + ihdr_data.extend_from_slice(&ihdr.height.to_be_bytes()); 952 + ihdr_data.push(ihdr.bit_depth); 953 + ihdr_data.push(ihdr.color_type); 954 + ihdr_data.push(0); // compression 955 + ihdr_data.push(0); // filter 956 + ihdr_data.push(ihdr.interlace); 957 + write_chunk(&mut png, b"IHDR", &ihdr_data); 958 + 959 + // PLTE 960 + if let Some(pal) = palette { 961 + write_chunk(&mut png, b"PLTE", pal); 962 + } 963 + 964 + // tRNS 965 + if let Some(t) = trns { 966 + write_chunk(&mut png, b"tRNS", t); 967 + } 968 + 969 + // IDAT: compress image_data with zlib 970 + let compressed = zlib_compress(image_data); 971 + write_chunk(&mut png, b"IDAT", &compressed); 972 + 973 + // IEND 974 + write_chunk(&mut png, b"IEND", &[]); 975 + 976 + png 977 + } 978 + 979 + fn write_chunk(out: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) { 980 + out.extend_from_slice(&(data.len() as u32).to_be_bytes()); 981 + out.extend_from_slice(chunk_type); 982 + out.extend_from_slice(data); 983 + // CRC over type + data 984 + let mut crc_buf = Vec::with_capacity(4 + data.len()); 985 + crc_buf.extend_from_slice(chunk_type); 986 + crc_buf.extend_from_slice(data); 987 + let crc = crc32(&crc_buf); 988 + out.extend_from_slice(&crc.to_be_bytes()); 989 + } 990 + 991 + /// Minimal zlib compression: store block (no compression). 992 + fn zlib_compress(data: &[u8]) -> Vec<u8> { 993 + let mut out = Vec::new(); 994 + // zlib header: CMF=0x78 (deflate, window=32KB), FLG=0x01 995 + out.push(0x78); 996 + out.push(0x01); 997 + 998 + // DEFLATE: split into non-compressed blocks of max 65535 bytes. 999 + let chunks: Vec<&[u8]> = if data.is_empty() { 1000 + vec![&[]] 1001 + } else { 1002 + data.chunks(65535).collect() 1003 + }; 1004 + for (i, chunk) in chunks.iter().enumerate() { 1005 + let is_final = i == chunks.len() - 1; 1006 + out.push(if is_final { 0x01 } else { 0x00 }); // BFINAL + BTYPE=00 1007 + let len = chunk.len() as u16; 1008 + out.push(len as u8); 1009 + out.push((len >> 8) as u8); 1010 + let nlen = !len; 1011 + out.push(nlen as u8); 1012 + out.push((nlen >> 8) as u8); 1013 + out.extend_from_slice(chunk); 1014 + } 1015 + 1016 + // Adler-32 trailer 1017 + let adler = adler32_compute(data); 1018 + out.push((adler >> 24) as u8); 1019 + out.push((adler >> 16) as u8); 1020 + out.push((adler >> 8) as u8); 1021 + out.push(adler as u8); 1022 + out 1023 + } 1024 + 1025 + fn adler32_compute(data: &[u8]) -> u32 { 1026 + const MOD: u32 = 65521; 1027 + let mut a: u32 = 1; 1028 + let mut b: u32 = 0; 1029 + for &byte in data { 1030 + a = (a + byte as u32) % MOD; 1031 + b = (b + a) % MOD; 1032 + } 1033 + (b << 16) | a 1034 + } 1035 + 1036 + /// Build raw scanline data with filter byte prepended to each row. 1037 + fn make_filtered_rows(rows: &[Vec<u8>]) -> Vec<u8> { 1038 + let mut out = Vec::new(); 1039 + for row in rows { 1040 + out.push(FILTER_NONE); // no filter 1041 + out.extend_from_slice(row); 1042 + } 1043 + out 1044 + } 1045 + 1046 + fn make_ihdr(width: u32, height: u32, bit_depth: u8, color_type: u8) -> Ihdr { 1047 + Ihdr { 1048 + width, 1049 + height, 1050 + bit_depth, 1051 + color_type, 1052 + interlace: 0, 1053 + } 1054 + } 1055 + 1056 + // -- Decoding tests -- 1057 + 1058 + #[test] 1059 + fn decode_1x1_rgb() { 1060 + let ihdr = make_ihdr(1, 1, 8, COLOR_RGB); 1061 + let raw = make_filtered_rows(&[vec![255, 0, 0]]); // red pixel 1062 + let png = build_png(&ihdr, None, None, &raw); 1063 + let img = decode_png(&png).unwrap(); 1064 + assert_eq!(img.width, 1); 1065 + assert_eq!(img.height, 1); 1066 + assert_eq!(img.data, vec![255, 0, 0, 255]); 1067 + } 1068 + 1069 + #[test] 1070 + fn decode_1x1_rgba() { 1071 + let ihdr = make_ihdr(1, 1, 8, COLOR_RGBA); 1072 + let raw = make_filtered_rows(&[vec![10, 20, 30, 128]]); 1073 + let png = build_png(&ihdr, None, None, &raw); 1074 + let img = decode_png(&png).unwrap(); 1075 + assert_eq!(img.data, vec![10, 20, 30, 128]); 1076 + } 1077 + 1078 + #[test] 1079 + fn decode_1x1_grayscale() { 1080 + let ihdr = make_ihdr(1, 1, 8, COLOR_GRAYSCALE); 1081 + let raw = make_filtered_rows(&[vec![128]]); 1082 + let png = build_png(&ihdr, None, None, &raw); 1083 + let img = decode_png(&png).unwrap(); 1084 + assert_eq!(img.data, vec![128, 128, 128, 255]); 1085 + } 1086 + 1087 + #[test] 1088 + fn decode_1x1_grayscale_alpha() { 1089 + let ihdr = make_ihdr(1, 1, 8, COLOR_GRAYSCALE_ALPHA); 1090 + let raw = make_filtered_rows(&[vec![200, 100]]); 1091 + let png = build_png(&ihdr, None, None, &raw); 1092 + let img = decode_png(&png).unwrap(); 1093 + assert_eq!(img.data, vec![200, 200, 200, 100]); 1094 + } 1095 + 1096 + #[test] 1097 + fn decode_indexed() { 1098 + let ihdr = make_ihdr(2, 1, 8, COLOR_INDEXED); 1099 + let palette = vec![255, 0, 0, 0, 255, 0]; // red, green 1100 + let raw = make_filtered_rows(&[vec![0, 1]]); // index 0, 1 1101 + let png = build_png(&ihdr, Some(&palette), None, &raw); 1102 + let img = decode_png(&png).unwrap(); 1103 + assert_eq!(img.data, vec![255, 0, 0, 255, 0, 255, 0, 255]); 1104 + } 1105 + 1106 + #[test] 1107 + fn decode_indexed_with_trns() { 1108 + let ihdr = make_ihdr(2, 1, 8, COLOR_INDEXED); 1109 + let palette = vec![255, 0, 0, 0, 255, 0]; 1110 + let trns = vec![128, 64]; // alpha for entries 0 and 1 1111 + let raw = make_filtered_rows(&[vec![0, 1]]); 1112 + let png = build_png(&ihdr, Some(&palette), Some(&trns), &raw); 1113 + let img = decode_png(&png).unwrap(); 1114 + assert_eq!(img.data, vec![255, 0, 0, 128, 0, 255, 0, 64]); 1115 + } 1116 + 1117 + #[test] 1118 + fn decode_2x2_rgb() { 1119 + let ihdr = make_ihdr(2, 2, 8, COLOR_RGB); 1120 + let raw = make_filtered_rows(&[ 1121 + vec![255, 0, 0, 0, 255, 0], // red, green 1122 + vec![0, 0, 255, 255, 255, 0], // blue, yellow 1123 + ]); 1124 + let png = build_png(&ihdr, None, None, &raw); 1125 + let img = decode_png(&png).unwrap(); 1126 + assert_eq!(img.width, 2); 1127 + assert_eq!(img.height, 2); 1128 + assert_eq!( 1129 + img.data, 1130 + vec![ 1131 + 255, 0, 0, 255, // red 1132 + 0, 255, 0, 255, // green 1133 + 0, 0, 255, 255, // blue 1134 + 255, 255, 0, 255, // yellow 1135 + ] 1136 + ); 1137 + } 1138 + 1139 + #[test] 1140 + fn decode_grayscale_with_trns() { 1141 + let ihdr = make_ihdr(2, 1, 8, COLOR_GRAYSCALE); 1142 + let raw = make_filtered_rows(&[vec![100, 200]]); 1143 + let trns = 100u16.to_be_bytes().to_vec(); // gray value 100 is transparent 1144 + let png = build_png(&ihdr, None, Some(&trns), &raw); 1145 + let img = decode_png(&png).unwrap(); 1146 + // pixel 0: gray=100 matches trns → alpha=0 1147 + // pixel 1: gray=200 no match → alpha=255 1148 + assert_eq!(img.data, vec![100, 100, 100, 0, 200, 200, 200, 255]); 1149 + } 1150 + 1151 + #[test] 1152 + fn decode_rgb_with_trns() { 1153 + let ihdr = make_ihdr(2, 1, 8, COLOR_RGB); 1154 + let raw = make_filtered_rows(&[vec![255, 0, 0, 0, 255, 0]]); 1155 + // tRNS: transparent color is (255, 0, 0) = red 1156 + let mut trns = Vec::new(); 1157 + trns.extend_from_slice(&255u16.to_be_bytes()); 1158 + trns.extend_from_slice(&0u16.to_be_bytes()); 1159 + trns.extend_from_slice(&0u16.to_be_bytes()); 1160 + let png = build_png(&ihdr, None, Some(&trns), &raw); 1161 + let img = decode_png(&png).unwrap(); 1162 + assert_eq!( 1163 + img.data, 1164 + vec![ 1165 + 255, 0, 0, 0, // red → transparent 1166 + 0, 255, 0, 255, // green → opaque 1167 + ] 1168 + ); 1169 + } 1170 + 1171 + #[test] 1172 + fn decode_16bit_rgb() { 1173 + let ihdr = make_ihdr(1, 1, 16, COLOR_RGB); 1174 + // 16-bit RGB: (0xFF00, 0x8000, 0x4000) 1175 + let raw = make_filtered_rows(&[vec![0xFF, 0x00, 0x80, 0x00, 0x40, 0x00]]); 1176 + let png = build_png(&ihdr, None, None, &raw); 1177 + let img = decode_png(&png).unwrap(); 1178 + // Downconvert takes high byte: R=0xFF, G=0x80, B=0x40 1179 + assert_eq!(img.data, vec![0xFF, 0x80, 0x40, 255]); 1180 + } 1181 + 1182 + #[test] 1183 + fn decode_16bit_grayscale() { 1184 + let ihdr = make_ihdr(1, 1, 16, COLOR_GRAYSCALE); 1185 + let raw = make_filtered_rows(&[vec![0xAB, 0xCD]]); 1186 + let png = build_png(&ihdr, None, None, &raw); 1187 + let img = decode_png(&png).unwrap(); 1188 + assert_eq!(img.data, vec![0xAB, 0xAB, 0xAB, 255]); 1189 + } 1190 + 1191 + #[test] 1192 + fn decode_1bit_grayscale() { 1193 + let ihdr = make_ihdr(8, 1, 1, COLOR_GRAYSCALE); 1194 + // 8 pixels in 1 byte: 0b10101010 → 255,0,255,0,255,0,255,0 1195 + let raw = make_filtered_rows(&[vec![0b1010_1010]]); 1196 + let png = build_png(&ihdr, None, None, &raw); 1197 + let img = decode_png(&png).unwrap(); 1198 + assert_eq!(img.width, 8); 1199 + let expected_gray = [255, 0, 255, 0, 255, 0, 255, 0]; 1200 + for (i, &g) in expected_gray.iter().enumerate() { 1201 + assert_eq!(img.data[i * 4], g, "pixel {i} R"); 1202 + assert_eq!(img.data[i * 4 + 1], g, "pixel {i} G"); 1203 + assert_eq!(img.data[i * 4 + 2], g, "pixel {i} B"); 1204 + assert_eq!(img.data[i * 4 + 3], 255, "pixel {i} A"); 1205 + } 1206 + } 1207 + 1208 + #[test] 1209 + fn decode_4bit_grayscale() { 1210 + let ihdr = make_ihdr(2, 1, 4, COLOR_GRAYSCALE); 1211 + // 2 pixels in 1 byte: 0xF0 → 255, 0 1212 + let raw = make_filtered_rows(&[vec![0xF0]]); 1213 + let png = build_png(&ihdr, None, None, &raw); 1214 + let img = decode_png(&png).unwrap(); 1215 + assert_eq!(img.data, vec![255, 255, 255, 255, 0, 0, 0, 255]); 1216 + } 1217 + 1218 + #[test] 1219 + fn decode_2bit_indexed() { 1220 + let ihdr = make_ihdr(4, 1, 2, COLOR_INDEXED); 1221 + let palette = vec![ 1222 + 255, 0, 0, // 0: red 1223 + 0, 255, 0, // 1: green 1224 + 0, 0, 255, // 2: blue 1225 + 255, 255, 0, // 3: yellow 1226 + ]; 1227 + // 4 pixels at 2 bits each = 1 byte: indices 0,1,2,3 → 0b00_01_10_11 1228 + let raw = make_filtered_rows(&[vec![0b00_01_10_11]]); 1229 + let png = build_png(&ihdr, Some(&palette), None, &raw); 1230 + let img = decode_png(&png).unwrap(); 1231 + assert_eq!( 1232 + img.data, 1233 + vec![ 1234 + 255, 0, 0, 255, // red 1235 + 0, 255, 0, 255, // green 1236 + 0, 0, 255, 255, // blue 1237 + 255, 255, 0, 255, // yellow 1238 + ] 1239 + ); 1240 + } 1241 + 1242 + #[test] 1243 + fn decode_sub_filter() { 1244 + let ihdr = make_ihdr(3, 1, 8, COLOR_GRAYSCALE); 1245 + // Sub filter: each byte = current - left 1246 + // Desired output: [100, 110, 120] 1247 + // Encoded: [100, 10, 10] (first is raw, rest are deltas from left) 1248 + let image_data = vec![FILTER_SUB, 100, 10, 10]; 1249 + let png = build_png(&ihdr, None, None, &image_data); 1250 + let img = decode_png(&png).unwrap(); 1251 + assert_eq!( 1252 + img.data, 1253 + vec![100, 100, 100, 255, 110, 110, 110, 255, 120, 120, 120, 255,] 1254 + ); 1255 + } 1256 + 1257 + #[test] 1258 + fn decode_up_filter() { 1259 + let ihdr = make_ihdr(2, 2, 8, COLOR_GRAYSCALE); 1260 + // Row 0: no filter [50, 60] 1261 + // Row 1: up filter, deltas [10, 10] → [60, 70] 1262 + let image_data = vec![FILTER_NONE, 50, 60, FILTER_UP, 10, 10]; 1263 + let png = build_png(&ihdr, None, None, &image_data); 1264 + let img = decode_png(&png).unwrap(); 1265 + assert_eq!( 1266 + img.data, 1267 + vec![50, 50, 50, 255, 60, 60, 60, 255, 60, 60, 60, 255, 70, 70, 70, 255,] 1268 + ); 1269 + } 1270 + 1271 + #[test] 1272 + fn decode_average_filter() { 1273 + let ihdr = make_ihdr(2, 1, 8, COLOR_GRAYSCALE); 1274 + // Average filter: each byte = current - floor((left + up) / 2) 1275 + // With no prior row, up=0 for all. 1276 + // Desired: [100, 80] 1277 + // Encoded: [100, 30] → pixel 0: 100+avg(0,0)=100, pixel 1: 30+avg(100,0)=30+50=80 1278 + let image_data = vec![FILTER_AVERAGE, 100, 30]; 1279 + let png = build_png(&ihdr, None, None, &image_data); 1280 + let img = decode_png(&png).unwrap(); 1281 + assert_eq!(img.data, vec![100, 100, 100, 255, 80, 80, 80, 255,]); 1282 + } 1283 + 1284 + #[test] 1285 + fn decode_paeth_filter() { 1286 + let ihdr = make_ihdr(2, 1, 8, COLOR_GRAYSCALE); 1287 + // Paeth filter, single row: up=0, upper_left=0 for all. 1288 + // pixel 0: a=0, b=0, c=0 → paeth=0 → 100+0=100 1289 + // pixel 1: a=100, b=0, c=0 → p=100, pa=0, pb=100, pc=100 → a → 10+100=110 1290 + let image_data = vec![FILTER_PAETH, 100, 10]; 1291 + let png = build_png(&ihdr, None, None, &image_data); 1292 + let img = decode_png(&png).unwrap(); 1293 + assert_eq!(img.data, vec![100, 100, 100, 255, 110, 110, 110, 255,]); 1294 + } 1295 + 1296 + // -- Adam7 interlacing tests -- 1297 + 1298 + #[test] 1299 + fn decode_adam7_2x2() { 1300 + // A 2x2 Adam7 interlaced image. 1301 + // Pass 1: (0,0) step (8,8) → pixel (0,0) → 1 pixel 1302 + // Pass 6: (1,0) step (2,2) → pixel (1,0) → 1 pixel 1303 + // Pass 7: (0,1) step (1,2) → pixels (0,1),(1,1) → 2 pixels 1304 + // Other passes have 0 pixels for a 2x2 image. 1305 + let ihdr = Ihdr { 1306 + width: 2, 1307 + height: 2, 1308 + bit_depth: 8, 1309 + color_type: COLOR_GRAYSCALE, 1310 + interlace: 1, 1311 + }; 1312 + 1313 + // Pass 1: 1x1 image, 1 byte per row 1314 + // Pass 6: 1x1 image, 1 byte per row 1315 + // Pass 7: 2x1 image, 2 bytes per row 1316 + let mut image_data = Vec::new(); 1317 + // Pass 1: pixel (0,0) = gray 10 1318 + image_data.push(FILTER_NONE); 1319 + image_data.push(10); 1320 + // Pass 6: pixel (1,0) = gray 20 1321 + image_data.push(FILTER_NONE); 1322 + image_data.push(20); 1323 + // Pass 7: pixels (0,1)=gray 30, (1,1)=gray 40 1324 + image_data.push(FILTER_NONE); 1325 + image_data.push(30); 1326 + image_data.push(40); 1327 + 1328 + let png = build_png(&ihdr, None, None, &image_data); 1329 + let img = decode_png(&png).unwrap(); 1330 + 1331 + // Expected layout: 1332 + // (0,0)=10 (1,0)=20 1333 + // (0,1)=30 (1,1)=40 1334 + assert_eq!( 1335 + img.data, 1336 + vec![10, 10, 10, 255, 20, 20, 20, 255, 30, 30, 30, 255, 40, 40, 40, 255,] 1337 + ); 1338 + } 1339 + 1340 + // -- Error cases -- 1341 + 1342 + #[test] 1343 + fn missing_ihdr() { 1344 + let mut png = Vec::new(); 1345 + png.extend_from_slice(&PNG_SIGNATURE); 1346 + write_chunk(&mut png, b"IEND", &[]); 1347 + assert!(decode_png(&png).is_err()); 1348 + } 1349 + 1350 + #[test] 1351 + fn missing_idat() { 1352 + let ihdr = make_ihdr(1, 1, 8, COLOR_RGB); 1353 + let mut png = Vec::new(); 1354 + png.extend_from_slice(&PNG_SIGNATURE); 1355 + 1356 + let mut ihdr_data = Vec::new(); 1357 + ihdr_data.extend_from_slice(&ihdr.width.to_be_bytes()); 1358 + ihdr_data.extend_from_slice(&ihdr.height.to_be_bytes()); 1359 + ihdr_data.push(ihdr.bit_depth); 1360 + ihdr_data.push(ihdr.color_type); 1361 + ihdr_data.push(0); 1362 + ihdr_data.push(0); 1363 + ihdr_data.push(0); 1364 + write_chunk(&mut png, b"IHDR", &ihdr_data); 1365 + write_chunk(&mut png, b"IEND", &[]); 1366 + assert!(decode_png(&png).is_err()); 1367 + } 1368 + 1369 + #[test] 1370 + fn invalid_plte_length() { 1371 + // PLTE must be a multiple of 3 1372 + let ihdr = make_ihdr(1, 1, 8, COLOR_INDEXED); 1373 + let mut png = Vec::new(); 1374 + png.extend_from_slice(&PNG_SIGNATURE); 1375 + 1376 + let mut ihdr_data = Vec::new(); 1377 + ihdr_data.extend_from_slice(&ihdr.width.to_be_bytes()); 1378 + ihdr_data.extend_from_slice(&ihdr.height.to_be_bytes()); 1379 + ihdr_data.push(ihdr.bit_depth); 1380 + ihdr_data.push(ihdr.color_type); 1381 + ihdr_data.push(0); 1382 + ihdr_data.push(0); 1383 + ihdr_data.push(0); 1384 + write_chunk(&mut png, b"IHDR", &ihdr_data); 1385 + write_chunk(&mut png, b"PLTE", &[1, 2]); // invalid: 2 bytes, not multiple of 3 1386 + write_chunk(&mut png, b"IEND", &[]); 1387 + assert!(decode_png(&png).is_err()); 1388 + } 1389 + 1390 + #[test] 1391 + fn crc_mismatch_rejected() { 1392 + let ihdr = make_ihdr(1, 1, 8, COLOR_RGB); 1393 + let raw = make_filtered_rows(&[vec![255, 0, 0]]); 1394 + let mut png = build_png(&ihdr, None, None, &raw); 1395 + // Corrupt CRC of the IHDR chunk (bytes 16-19 after signature+length+type+data) 1396 + // IHDR chunk starts at offset 8, length=4 bytes, type=4 bytes, data=13 bytes, then CRC=4 bytes 1397 + // CRC is at offset 8 + 4 + 4 + 13 = 29 1398 + if png.len() > 32 { 1399 + png[29] ^= 0xFF; 1400 + } 1401 + assert!(decode_png(&png).is_err()); 1402 + } 1403 + 1404 + // -- Larger image test -- 1405 + 1406 + #[test] 1407 + fn decode_10x10_rgb_gradient() { 1408 + let ihdr = make_ihdr(10, 10, 8, COLOR_RGB); 1409 + let mut rows = Vec::new(); 1410 + for y in 0..10u8 { 1411 + let mut row = Vec::new(); 1412 + for x in 0..10u8 { 1413 + row.push(x * 25); // R 1414 + row.push(y * 25); // G 1415 + row.push(128); // B 1416 + } 1417 + rows.push(row); 1418 + } 1419 + let raw = make_filtered_rows(&rows); 1420 + let png = build_png(&ihdr, None, None, &raw); 1421 + let img = decode_png(&png).unwrap(); 1422 + assert_eq!(img.width, 10); 1423 + assert_eq!(img.height, 10); 1424 + assert_eq!(img.data.len(), 10 * 10 * 4); 1425 + // Spot-check pixel (0,0) 1426 + assert_eq!(&img.data[0..4], &[0, 0, 128, 255]); 1427 + // Spot-check pixel (9,9) 1428 + let offset = (9 * 10 + 9) * 4; 1429 + assert_eq!(&img.data[offset..offset + 4], &[225, 225, 128, 255]); 1430 + } 1431 + 1432 + // -- 16-bit RGBA -- 1433 + 1434 + #[test] 1435 + fn decode_16bit_rgba() { 1436 + let ihdr = make_ihdr(1, 1, 16, COLOR_RGBA); 1437 + // R=0xFF00, G=0x8000, B=0x4000, A=0xC000 1438 + let raw = make_filtered_rows(&[vec![0xFF, 0x00, 0x80, 0x00, 0x40, 0x00, 0xC0, 0x00]]); 1439 + let png = build_png(&ihdr, None, None, &raw); 1440 + let img = decode_png(&png).unwrap(); 1441 + assert_eq!(img.data, vec![0xFF, 0x80, 0x40, 0xC0]); 1442 + } 1443 + 1444 + // -- 16-bit grayscale+alpha -- 1445 + 1446 + #[test] 1447 + fn decode_16bit_grayscale_alpha() { 1448 + let ihdr = make_ihdr(1, 1, 16, COLOR_GRAYSCALE_ALPHA); 1449 + let raw = make_filtered_rows(&[vec![0xAB, 0xCD, 0x80, 0x00]]); 1450 + let png = build_png(&ihdr, None, None, &raw); 1451 + let img = decode_png(&png).unwrap(); 1452 + assert_eq!(img.data, vec![0xAB, 0xAB, 0xAB, 0x80]); 1453 + } 1454 + 1455 + // -- Multiple IDAT chunks -- 1456 + 1457 + #[test] 1458 + fn decode_multiple_idat() { 1459 + let ihdr = make_ihdr(1, 1, 8, COLOR_RGB); 1460 + let raw = make_filtered_rows(&[vec![42, 84, 126]]); 1461 + let compressed = zlib_compress(&raw); 1462 + 1463 + let mut png = Vec::new(); 1464 + png.extend_from_slice(&PNG_SIGNATURE); 1465 + 1466 + let mut ihdr_data = Vec::new(); 1467 + ihdr_data.extend_from_slice(&ihdr.width.to_be_bytes()); 1468 + ihdr_data.extend_from_slice(&ihdr.height.to_be_bytes()); 1469 + ihdr_data.push(ihdr.bit_depth); 1470 + ihdr_data.push(ihdr.color_type); 1471 + ihdr_data.push(0); 1472 + ihdr_data.push(0); 1473 + ihdr_data.push(0); 1474 + write_chunk(&mut png, b"IHDR", &ihdr_data); 1475 + 1476 + // Split compressed data into two IDAT chunks. 1477 + let mid = compressed.len() / 2; 1478 + write_chunk(&mut png, b"IDAT", &compressed[..mid]); 1479 + write_chunk(&mut png, b"IDAT", &compressed[mid..]); 1480 + write_chunk(&mut png, b"IEND", &[]); 1481 + 1482 + let img = decode_png(&png).unwrap(); 1483 + assert_eq!(img.data, vec![42, 84, 126, 255]); 1484 + } 1485 + 1486 + // -- Ancillary chunks are skipped -- 1487 + 1488 + #[test] 1489 + fn ancillary_chunks_skipped() { 1490 + let ihdr = make_ihdr(1, 1, 8, COLOR_RGB); 1491 + let raw = make_filtered_rows(&[vec![1, 2, 3]]); 1492 + 1493 + let mut png = Vec::new(); 1494 + png.extend_from_slice(&PNG_SIGNATURE); 1495 + 1496 + let mut ihdr_data = Vec::new(); 1497 + ihdr_data.extend_from_slice(&ihdr.width.to_be_bytes()); 1498 + ihdr_data.extend_from_slice(&ihdr.height.to_be_bytes()); 1499 + ihdr_data.push(ihdr.bit_depth); 1500 + ihdr_data.push(ihdr.color_type); 1501 + ihdr_data.push(0); 1502 + ihdr_data.push(0); 1503 + ihdr_data.push(0); 1504 + write_chunk(&mut png, b"IHDR", &ihdr_data); 1505 + // Add an ancillary chunk (lowercase first letter = ancillary) 1506 + write_chunk(&mut png, b"tEXt", b"Comment\x00Hello"); 1507 + let compressed = zlib_compress(&raw); 1508 + write_chunk(&mut png, b"IDAT", &compressed); 1509 + write_chunk(&mut png, b"IEND", &[]); 1510 + 1511 + let img = decode_png(&png).unwrap(); 1512 + assert_eq!(img.data, vec![1, 2, 3, 255]); 1513 + } 1514 + 1515 + // -- 1-bit indexed (palette) -- 1516 + 1517 + #[test] 1518 + fn decode_1bit_indexed() { 1519 + let ihdr = make_ihdr(8, 1, 1, COLOR_INDEXED); 1520 + let palette = vec![ 1521 + 0, 0, 0, // index 0: black 1522 + 255, 255, 255, // index 1: white 1523 + ]; 1524 + // 8 pixels in 1 byte: 0b10101010 → indices: 1,0,1,0,1,0,1,0 1525 + let raw = make_filtered_rows(&[vec![0b1010_1010]]); 1526 + let png = build_png(&ihdr, Some(&palette), None, &raw); 1527 + let img = decode_png(&png).unwrap(); 1528 + for i in 0..8 { 1529 + let expected = if i % 2 == 0 { 255 } else { 0 }; 1530 + assert_eq!(img.data[i * 4], expected, "pixel {i} R"); 1531 + assert_eq!(img.data[i * 4 + 1], expected, "pixel {i} G"); 1532 + assert_eq!(img.data[i * 4 + 2], expected, "pixel {i} B"); 1533 + assert_eq!(img.data[i * 4 + 3], 255, "pixel {i} A"); 1534 + } 1535 + } 1536 + 1537 + // -- Error Display test -- 1538 + 1539 + #[test] 1540 + fn error_display_decode() { 1541 + let err = ImageError::Decode("test error".to_string()); 1542 + assert_eq!(err.to_string(), "decode error: test error"); 1543 + } 1544 + }