we (web engine): Experimental web browser project to understand the limits of Claude
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}