we (web engine): Experimental web browser project to understand the limits of Claude
at css-loading 1544 lines 52 kB view raw
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 7use crate::pixel::{self, Image, ImageError}; 8use crate::zlib; 9 10// --------------------------------------------------------------------------- 11// CRC-32 12// --------------------------------------------------------------------------- 13 14/// CRC-32 lookup table (polynomial 0xEDB88320, reflected). 15const 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. 36fn 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. 50const PNG_SIGNATURE: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10]; 51 52// Chunk type tags (as big-endian u32). 53const CHUNK_IHDR: u32 = u32::from_be_bytes(*b"IHDR"); 54const CHUNK_PLTE: u32 = u32::from_be_bytes(*b"PLTE"); 55const CHUNK_IDAT: u32 = u32::from_be_bytes(*b"IDAT"); 56const CHUNK_IEND: u32 = u32::from_be_bytes(*b"IEND"); 57const CHUNK_TRNS: u32 = u32::from_be_bytes(*b"tRNS"); 58 59// Color type flags. 60const COLOR_GRAYSCALE: u8 = 0; 61const COLOR_RGB: u8 = 2; 62const COLOR_INDEXED: u8 = 3; 63const COLOR_GRAYSCALE_ALPHA: u8 = 4; 64const COLOR_RGBA: u8 = 6; 65 66// Filter types. 67const FILTER_NONE: u8 = 0; 68const FILTER_SUB: u8 = 1; 69const FILTER_UP: u8 = 2; 70const FILTER_AVERAGE: u8 = 3; 71const FILTER_PAETH: u8 = 4; 72 73// Adam7 interlace pass parameters: (x_start, y_start, x_step, y_step) 74const 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)] 89struct Ihdr { 90 width: u32, 91 height: u32, 92 bit_depth: u8, 93 color_type: u8, 94 interlace: u8, 95} 96 97impl 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)] 178enum 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 191struct ChunkReader<'a> { 192 data: &'a [u8], 193 pos: usize, 194} 195 196struct Chunk<'a> { 197 chunk_type: u32, 198 data: &'a [u8], 199} 200 201impl<'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). 257fn 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). 279fn 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). 355fn 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. 370fn 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. 392fn 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. 405fn 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. 554fn 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. 635pub 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 743fn 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 752fn read_u16_be(data: &[u8], offset: usize) -> u16 { 753 u16::from_be_bytes([data[offset], data[offset + 1]]) 754} 755 756fn decode_err(msg: &str) -> ImageError { 757 ImageError::Decode(msg.to_string()) 758} 759 760// --------------------------------------------------------------------------- 761// Tests 762// --------------------------------------------------------------------------- 763 764#[cfg(test)] 765mod 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}