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

Configure Feed

Select the types of activity you want to include in your feed.

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>

authored by

Pierre Le Fevre
Claude Opus 4.6
and committed by tangled.org 3e2d2105 a4f45612

+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 + }