1use otf_pixels_core::{ImageDescriptor, Limits, PixelFormat, PixelsError, Result, Source};
8
9use otf_pixels_compress::Crc32;
10
11pub const SIGNATURE: [u8; 8] = [0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum ColorType {
20 Grayscale,
22 Rgb,
24 Palette,
26 GrayscaleAlpha,
28 Rgba,
30}
31
32impl ColorType {
33 fn from_byte(byte: u8) -> Result<Self> {
35 match byte {
36 0 => Ok(Self::Grayscale),
37 2 => Ok(Self::Rgb),
38 3 => Ok(Self::Palette),
39 4 => Ok(Self::GrayscaleAlpha),
40 6 => Ok(Self::Rgba),
41 other => Err(PixelsError::malformed(
42 "png",
43 format!("colour type {other} is not one of 0, 2, 3, 4, 6"),
44 )),
45 }
46 }
47
48 #[must_use]
50 pub const fn to_byte(self) -> u8 {
51 match self {
52 Self::Grayscale => 0,
53 Self::Rgb => 2,
54 Self::Palette => 3,
55 Self::GrayscaleAlpha => 4,
56 Self::Rgba => 6,
57 }
58 }
59
60 #[must_use]
62 pub const fn channels(self) -> usize {
63 match self {
64 Self::Grayscale | Self::Palette => 1,
65 Self::GrayscaleAlpha => 2,
66 Self::Rgb => 3,
67 Self::Rgba => 4,
68 }
69 }
70
71 #[must_use]
73 pub const fn allows_depth(self, depth: u8) -> bool {
74 match self {
75 Self::Grayscale => matches!(depth, 1 | 2 | 4 | 8 | 16),
76 Self::Palette => matches!(depth, 1 | 2 | 4 | 8),
77 Self::Rgb | Self::GrayscaleAlpha | Self::Rgba => matches!(depth, 8 | 16),
78 }
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84#[non_exhaustive]
85pub struct Header {
86 pub width: u32,
88 pub height: u32,
90 pub bit_depth: u8,
92 pub color_type: ColorType,
94 pub interlaced: bool,
96}
97
98impl Header {
99 pub fn parse(payload: &[u8], limits: &Limits) -> Result<Self> {
109 if payload.len() != 13 {
110 return Err(PixelsError::malformed(
111 "png",
112 format!("IHDR must be 13 bytes, got {}", payload.len()),
113 ));
114 }
115 let width = read_u32(payload, 0)?;
116 let height = read_u32(payload, 4)?;
117 let bit_depth = payload.get(8).copied().unwrap_or(0);
118 let color_type = ColorType::from_byte(payload.get(9).copied().unwrap_or(0))?;
119 let compression = payload.get(10).copied().unwrap_or(0);
120 let filter = payload.get(11).copied().unwrap_or(0);
121 let interlace = payload.get(12).copied().unwrap_or(0);
122
123 if width == 0 || height == 0 {
124 return Err(PixelsError::malformed(
125 "png",
126 format!("dimensions must be non-zero, got {width}x{height}"),
127 ));
128 }
129 if !matches!(bit_depth, 1 | 2 | 4 | 8 | 16) {
130 return Err(PixelsError::malformed(
131 "png",
132 format!("bit depth {bit_depth} is not one of 1, 2, 4, 8, 16"),
133 ));
134 }
135 if !color_type.allows_depth(bit_depth) {
136 return Err(PixelsError::malformed(
137 "png",
138 format!("bit depth {bit_depth} is not allowed for colour type {color_type:?}"),
139 ));
140 }
141 if compression != 0 {
142 return Err(PixelsError::malformed(
143 "png",
144 format!("compression method {compression} is not deflate"),
145 ));
146 }
147 if filter != 0 {
148 return Err(PixelsError::malformed(
149 "png",
150 format!("filter method {filter} is not the only defined one"),
151 ));
152 }
153 if !matches!(interlace, 0 | 1) {
154 return Err(PixelsError::malformed(
155 "png",
156 format!("interlace method {interlace} is not none or Adam7"),
157 ));
158 }
159 limits.check(width, height)?;
161
162 Ok(Self {
163 width,
164 height,
165 bit_depth,
166 color_type,
167 interlaced: interlace == 1,
168 })
169 }
170
171 #[must_use]
173 pub const fn bits_per_pixel(&self) -> usize {
174 self.color_type.channels() * self.bit_depth as usize
175 }
176
177 #[must_use]
180 pub const fn row_bytes(&self, width: u32) -> usize {
181 (width as usize * self.bits_per_pixel()).div_ceil(8)
182 }
183
184 #[must_use]
188 pub const fn filter_stride(&self) -> usize {
189 let bytes = self.bits_per_pixel().div_ceil(8);
192 if bytes == 0 { 1 } else { bytes }
193 }
194
195 #[must_use]
205 pub const fn output_format(&self, has_transparency: bool) -> PixelFormat {
206 let deep = self.bit_depth == 16;
207 match self.color_type {
208 ColorType::Grayscale => match (has_transparency, deep) {
209 (false, false) => PixelFormat::Gray8,
210 (false, true) => PixelFormat::Gray16,
211 (true, false) => PixelFormat::GrayA8,
212 (true, true) => PixelFormat::Rgba16,
214 },
215 ColorType::GrayscaleAlpha => {
216 if deep {
217 PixelFormat::Rgba16
218 } else {
219 PixelFormat::GrayA8
220 }
221 }
222 ColorType::Rgb => match (has_transparency, deep) {
223 (false, false) => PixelFormat::Rgb8,
224 (false, true) => PixelFormat::Rgb16,
225 (true, false) => PixelFormat::Rgba8,
226 (true, true) => PixelFormat::Rgba16,
227 },
228 ColorType::Rgba => {
229 if deep {
230 PixelFormat::Rgba16
231 } else {
232 PixelFormat::Rgba8
233 }
234 }
235 ColorType::Palette => {
237 if has_transparency {
238 PixelFormat::Rgba8
239 } else {
240 PixelFormat::Rgb8
241 }
242 }
243 }
244 }
245
246 pub fn descriptor(&self, has_transparency: bool, limits: &Limits) -> Result<ImageDescriptor> {
252 ImageDescriptor::with_limits(
253 self.width,
254 self.height,
255 self.output_format(has_transparency),
256 limits,
257 )
258 }
259
260 #[must_use]
265 pub fn filtered_size(&self) -> usize {
266 if self.interlaced {
267 (0..7)
270 .map(|pass| {
271 let (width, height) = adam7_pass_size(pass, self.width, self.height);
272 if width == 0 || height == 0 {
273 0
274 } else {
275 height as usize * (1 + self.row_bytes(width))
276 }
277 })
278 .sum()
279 } else {
280 self.height as usize * (1 + self.row_bytes(self.width))
281 }
282 }
283}
284
285fn read_u32(bytes: &[u8], offset: usize) -> Result<u32> {
287 let slice = bytes
288 .get(offset..offset + 4)
289 .ok_or_else(|| PixelsError::malformed("png", "truncated 4-byte field"))?;
290 Ok(u32::from_be_bytes([
291 slice.first().copied().unwrap_or(0),
292 slice.get(1).copied().unwrap_or(0),
293 slice.get(2).copied().unwrap_or(0),
294 slice.get(3).copied().unwrap_or(0),
295 ]))
296}
297
298#[derive(Debug, Clone)]
300pub struct Chunk {
301 pub kind: [u8; 4],
303 pub data: Vec<u8>,
305}
306
307impl Chunk {
308 #[must_use]
310 pub fn is(&self, name: &[u8; 4]) -> bool {
311 self.kind == *name
312 }
313
314 #[cfg(test)]
321 #[must_use]
322 pub fn is_ancillary(&self) -> bool {
323 self.kind.first().copied().unwrap_or(0) & 0x20 != 0
324 }
325
326 #[must_use]
328 pub fn name(&self) -> String {
329 String::from_utf8_lossy(&self.kind).into_owned()
330 }
331}
332
333#[derive(Debug)]
335pub struct ChunkReader<'a> {
336 data: &'a [u8],
337 position: usize,
338}
339
340impl<'a> ChunkReader<'a> {
341 pub fn new(data: &'a [u8]) -> Result<Self> {
347 let head = data
348 .get(..8)
349 .ok_or_else(|| PixelsError::malformed("png", "shorter than the 8-byte signature"))?;
350 if head != SIGNATURE {
351 return Err(PixelsError::malformed("png", "signature does not match"));
352 }
353 Ok(Self { data, position: 8 })
354 }
355
356 #[cfg(test)]
358 #[must_use]
359 pub const fn is_finished(&self) -> bool {
360 self.position >= self.data.len()
361 }
362
363 pub fn next_chunk(&mut self) -> Result<Chunk> {
370 let length = read_u32(self.data, self.position)? as usize;
371 if length > 0x7FFF_FFFF {
374 return Err(PixelsError::malformed(
375 "png",
376 format!("chunk length {length} exceeds the 2^31-1 maximum"),
377 ));
378 }
379 let kind_at = self.position + 4;
380 let data_at = kind_at + 4;
381 let crc_at = data_at + length;
382
383 let kind_slice = self
384 .data
385 .get(kind_at..kind_at + 4)
386 .ok_or_else(|| PixelsError::malformed("png", "truncated chunk type"))?;
387 let mut kind = [0_u8; 4];
388 kind.copy_from_slice(kind_slice);
389
390 let payload = self.data.get(data_at..crc_at).ok_or_else(|| {
391 PixelsError::malformed(
392 "png",
393 format!(
394 "chunk `{}` declares {length} bytes but the stream is shorter",
395 String::from_utf8_lossy(&kind)
396 ),
397 )
398 })?;
399 let expected = read_u32(self.data, crc_at)?;
400
401 let mut crc = Crc32::new();
402 crc.update(&kind);
403 crc.update(payload);
404 let actual = crc.finish();
405 if actual != expected {
406 return Err(PixelsError::malformed(
407 "png",
408 format!(
409 "chunk `{}` CRC mismatch: declares {expected:#010x}, data is {actual:#010x}",
410 String::from_utf8_lossy(&kind)
411 ),
412 ));
413 }
414
415 self.position = crc_at + 4;
416 Ok(Chunk {
417 kind,
418 data: payload.to_vec(),
419 })
420 }
421}
422
423const SKIP_BUFFER: usize = 32 * 1024;
429
430#[derive(Debug)]
437pub struct ChunkStream<S: Source> {
438 source: S,
439 remaining: usize,
441 crc: Crc32,
443 kind: [u8; 4],
444 open: bool,
445}
446
447impl<S: Source> ChunkStream<S> {
448 #[must_use]
451 pub const fn new(source: S) -> Self {
452 Self {
453 source,
454 remaining: 0,
455 crc: Crc32::new(),
456 kind: [0; 4],
457 open: false,
458 }
459 }
460
461 #[must_use]
463 pub const fn payload_done(&self) -> bool {
464 self.remaining == 0
465 }
466
467 #[must_use]
469 pub const fn is_ancillary(&self) -> bool {
470 self.kind[0] & 0x20 != 0
471 }
472
473 #[must_use]
475 pub fn name(&self) -> String {
476 String::from_utf8_lossy(&self.kind).into_owned()
477 }
478
479 pub fn open_next(&mut self) -> Result<[u8; 4]> {
487 let mut head = [0_u8; 8];
488 self.source.read_exact(&mut head)?;
489 let length = u32::from_be_bytes([head[0], head[1], head[2], head[3]]) as usize;
490 if length > 0x7FFF_FFFF {
492 return Err(PixelsError::malformed(
493 "png",
494 format!("chunk length {length} exceeds the 2^31-1 maximum"),
495 ));
496 }
497 let mut kind = [0_u8; 4];
498 kind.copy_from_slice(head.get(4..8).unwrap_or(&[0; 4]));
499
500 self.kind = kind;
501 self.remaining = length;
502 self.crc = Crc32::new();
503 self.crc.update(&kind);
504 self.open = true;
505 Ok(kind)
506 }
507
508 pub fn read_payload(&mut self, buf: &mut [u8]) -> Result<usize> {
518 let want = self.remaining.min(buf.len());
519 if want == 0 {
520 return Ok(0);
521 }
522 let Some(slot) = buf.get_mut(..want) else {
523 return Ok(0);
524 };
525 self.source.read_exact(slot)?;
526 self.crc.update(slot);
527 self.remaining -= want;
528 Ok(want)
529 }
530
531 pub fn read_payload_to_end(&mut self, max: usize) -> Result<Vec<u8>> {
541 if self.remaining > max {
542 return Err(PixelsError::malformed(
543 "png",
544 format!(
545 "chunk `{}` declares {} bytes, above the {max} the specification allows",
546 String::from_utf8_lossy(&self.kind),
547 self.remaining
548 ),
549 ));
550 }
551 let mut out = vec![0_u8; self.remaining];
552 let mut filled = 0;
553 while filled < out.len() {
554 let Some(rest) = out.get_mut(filled..) else {
555 break;
556 };
557 match self.read_payload(rest)? {
558 0 => break,
559 n => filled += n,
560 }
561 }
562 Ok(out)
563 }
564
565 pub fn skip_payload(&mut self) -> Result<()> {
571 let mut scratch = vec![0_u8; SKIP_BUFFER.min(self.remaining.max(1))];
572 while self.remaining > 0 {
573 if self.read_payload(&mut scratch)? == 0 {
574 break;
575 }
576 }
577 Ok(())
578 }
579
580 pub fn close(&mut self) -> Result<()> {
587 if self.remaining != 0 {
588 return Err(PixelsError::malformed(
589 "png",
590 "chunk closed before its payload was read",
591 ));
592 }
593 let mut trailer = [0_u8; 4];
594 self.source.read_exact(&mut trailer)?;
595 let expected = u32::from_be_bytes(trailer);
596 let actual = self.crc.finish();
597 self.open = false;
598 if actual != expected {
599 return Err(PixelsError::malformed(
600 "png",
601 format!(
602 "chunk `{}` CRC mismatch: declares {expected:#010x}, data is {actual:#010x}",
603 String::from_utf8_lossy(&self.kind)
604 ),
605 ));
606 }
607 Ok(())
608 }
609}
610
611pub fn write_chunk(out: &mut Vec<u8>, kind: &[u8; 4], payload: &[u8]) {
613 out.extend_from_slice(&(payload.len() as u32).to_be_bytes());
614 out.extend_from_slice(kind);
615 out.extend_from_slice(payload);
616 let mut crc = Crc32::new();
617 crc.update(kind);
618 crc.update(payload);
619 out.extend_from_slice(&crc.finish().to_be_bytes());
620}
621
622#[derive(Debug, Clone, Copy, PartialEq, Eq)]
624pub enum Filter {
625 None,
627 Sub,
629 Up,
631 Average,
633 Paeth,
635}
636
637impl Filter {
638 pub fn from_byte(byte: u8) -> Result<Self> {
644 match byte {
645 0 => Ok(Self::None),
646 1 => Ok(Self::Sub),
647 2 => Ok(Self::Up),
648 3 => Ok(Self::Average),
649 4 => Ok(Self::Paeth),
650 other => Err(PixelsError::malformed(
651 "png",
652 format!("filter type {other} is not one of 0..=4"),
653 )),
654 }
655 }
656
657 #[must_use]
659 pub const fn to_byte(self) -> u8 {
660 match self {
661 Self::None => 0,
662 Self::Sub => 1,
663 Self::Up => 2,
664 Self::Average => 3,
665 Self::Paeth => 4,
666 }
667 }
668}
669
670const fn paeth(a: u8, b: u8, c: u8) -> u8 {
674 let p = a as i16 + b as i16 - c as i16;
675 let pa = (p - a as i16).abs();
676 let pb = (p - b as i16).abs();
677 let pc = (p - c as i16).abs();
678 if pa <= pb && pa <= pc {
680 a
681 } else if pb <= pc {
682 b
683 } else {
684 c
685 }
686}
687
688fn paeth_pixels<const N: usize>(current: &mut [u8], previous: &[u8]) -> bool {
700 let mut left = [0_u8; N];
701 let mut above_left = [0_u8; N];
702 for (pixel, above) in current.chunks_exact_mut(N).zip(previous.chunks_exact(N)) {
703 for (((slot, &up), left), above_left) in pixel
704 .iter_mut()
705 .zip(above)
706 .zip(left.iter_mut())
707 .zip(above_left.iter_mut())
708 {
709 *slot = slot.wrapping_add(paeth(*left, up, *above_left));
710 *left = *slot;
711 *above_left = up;
712 }
713 }
714 true
715}
716
717pub fn unfilter(filter: Filter, current: &mut [u8], previous: &[u8], stride: usize) -> Result<()> {
726 if previous.len() != current.len() {
727 return Err(PixelsError::malformed(
728 "png",
729 "scanlines differ in length while unfiltering",
730 ));
731 }
732 let lead = stride.min(current.len());
736 match filter {
737 Filter::None => {}
738 Filter::Sub => {
739 for index in lead..current.len() {
740 let left = current.get(index - stride).copied().unwrap_or(0);
741 if let Some(slot) = current.get_mut(index) {
742 *slot = slot.wrapping_add(left);
743 }
744 }
745 }
746 Filter::Up => {
747 for (slot, &above) in current.iter_mut().zip(previous) {
748 *slot = slot.wrapping_add(above);
749 }
750 }
751 Filter::Average => {
752 for (slot, &above) in current.iter_mut().zip(previous).take(lead) {
753 *slot = slot.wrapping_add(above / 2);
754 }
755 for index in lead..current.len() {
756 let left = current.get(index - stride).copied().unwrap_or(0);
757 let above = previous.get(index).copied().unwrap_or(0);
758 let mean = ((u16::from(left) + u16::from(above)) / 2) as u8;
760 if let Some(slot) = current.get_mut(index) {
761 *slot = slot.wrapping_add(mean);
762 }
763 }
764 }
765 Filter::Paeth => {
766 if current.len() % stride.max(1) == 0 {
769 let done = match stride {
770 1 => paeth_pixels::<1>(current, previous),
771 2 => paeth_pixels::<2>(current, previous),
772 3 => paeth_pixels::<3>(current, previous),
773 4 => paeth_pixels::<4>(current, previous),
774 6 => paeth_pixels::<6>(current, previous),
775 8 => paeth_pixels::<8>(current, previous),
776 _ => false,
777 };
778 if done {
779 return Ok(());
780 }
781 }
782 for (slot, &above) in current.iter_mut().zip(previous).take(lead) {
784 *slot = slot.wrapping_add(above);
785 }
786 for index in lead..current.len() {
787 let left = current.get(index - stride).copied().unwrap_or(0);
788 let above = previous.get(index).copied().unwrap_or(0);
789 let above_left = previous.get(index - stride).copied().unwrap_or(0);
790 if let Some(slot) = current.get_mut(index) {
791 *slot = slot.wrapping_add(paeth(left, above, above_left));
792 }
793 }
794 }
795 }
796 Ok(())
797}
798
799pub fn apply_filter(
801 filter: Filter,
802 current: &[u8],
803 previous: &[u8],
804 stride: usize,
805 out: &mut Vec<u8>,
806) {
807 for index in 0..current.len() {
808 let raw = current.get(index).copied().unwrap_or(0);
809 let left = index
810 .checked_sub(stride)
811 .and_then(|i| current.get(i).copied())
812 .unwrap_or(0);
813 let above = previous.get(index).copied().unwrap_or(0);
814 let above_left = index
815 .checked_sub(stride)
816 .and_then(|i| previous.get(i).copied())
817 .unwrap_or(0);
818 let value = match filter {
819 Filter::None => raw,
820 Filter::Sub => raw.wrapping_sub(left),
821 Filter::Up => raw.wrapping_sub(above),
822 Filter::Average => {
823 let mean = ((u16::from(left) + u16::from(above)) / 2) as u8;
824 raw.wrapping_sub(mean)
825 }
826 Filter::Paeth => raw.wrapping_sub(paeth(left, above, above_left)),
827 };
828 out.push(value);
829 }
830}
831
832const ADAM7: [(u32, u32, u32, u32); 7] = [
834 (0, 8, 0, 8),
835 (4, 8, 0, 8),
836 (0, 4, 4, 8),
837 (2, 4, 0, 4),
838 (0, 2, 2, 4),
839 (1, 2, 0, 2),
840 (0, 1, 1, 2),
841];
842
843#[must_use]
848pub fn adam7_pass_size(pass: usize, width: u32, height: u32) -> (u32, u32) {
849 let Some(&(x0, dx, y0, dy)) = ADAM7.get(pass) else {
850 return (0, 0);
851 };
852 let pass_width = if width > x0 {
853 (width - x0).div_ceil(dx)
854 } else {
855 0
856 };
857 let pass_height = if height > y0 {
858 (height - y0).div_ceil(dy)
859 } else {
860 0
861 };
862 (pass_width, pass_height)
863}
864
865#[must_use]
867pub fn adam7_position(pass: usize, x: u32, y: u32) -> (u32, u32) {
868 let Some(&(x0, dx, y0, dy)) = ADAM7.get(pass) else {
869 return (0, 0);
870 };
871 (x0 + x * dx, y0 + y * dy)
872}
873
874#[cfg(test)]
875#[allow(
876 clippy::unwrap_used,
877 clippy::expect_used,
878 clippy::indexing_slicing,
879 clippy::panic,
880 reason = "tests operate on known-good values and assert shapes directly"
881)]
882mod tests {
883 use super::*;
884
885 fn ihdr(width: u32, height: u32, depth: u8, color: u8, interlace: u8) -> Vec<u8> {
886 let mut payload = Vec::new();
887 payload.extend_from_slice(&width.to_be_bytes());
888 payload.extend_from_slice(&height.to_be_bytes());
889 payload.extend_from_slice(&[depth, color, 0, 0, interlace]);
890 payload
891 }
892
893 #[test]
894 fn a_valid_header_parses() {
895 let header = Header::parse(&ihdr(32, 16, 8, 2, 0), &Limits::default()).unwrap();
896 assert_eq!((header.width, header.height), (32, 16));
897 assert_eq!(header.bit_depth, 8);
898 assert_eq!(header.color_type, ColorType::Rgb);
899 assert!(!header.interlaced);
900 assert_eq!(header.bits_per_pixel(), 24);
901 assert_eq!(header.row_bytes(32), 96);
902 assert_eq!(header.filter_stride(), 3);
903 }
904
905 #[test]
906 fn invalid_headers_are_rejected() {
907 let limits = Limits::default();
908 assert!(Header::parse(&[0; 12], &limits).is_err());
910 assert!(Header::parse(&[0; 14], &limits).is_err());
911 assert!(Header::parse(&ihdr(0, 8, 8, 0, 0), &limits).is_err());
913 assert!(Header::parse(&ihdr(8, 0, 8, 0, 0), &limits).is_err());
914 assert!(Header::parse(&ihdr(8, 8, 3, 0, 0), &limits).is_err());
916 assert!(Header::parse(&ihdr(8, 8, 0, 0, 0), &limits).is_err());
917 assert!(Header::parse(&ihdr(8, 8, 8, 1, 0), &limits).is_err());
919 assert!(Header::parse(&ihdr(8, 8, 8, 5, 0), &limits).is_err());
920 assert!(
922 Header::parse(&ihdr(8, 8, 1, 2, 0), &limits).is_err(),
923 "1-bit RGB"
924 );
925 assert!(
926 Header::parse(&ihdr(8, 8, 16, 3, 0), &limits).is_err(),
927 "16-bit palette"
928 );
929 let mut bad = ihdr(8, 8, 8, 0, 0);
931 bad[10] = 1;
932 assert!(Header::parse(&bad, &limits).is_err());
933 let mut bad = ihdr(8, 8, 8, 0, 0);
934 bad[11] = 1;
935 assert!(Header::parse(&bad, &limits).is_err());
936 assert!(Header::parse(&ihdr(8, 8, 8, 0, 2), &limits).is_err());
937 }
938
939 #[test]
940 fn max_pixels_is_enforced_at_header_parse() {
941 let limits = Limits::default();
943 let err = Header::parse(&ihdr(u32::MAX, u32::MAX, 8, 6, 0), &limits).unwrap_err();
944 assert_eq!(err.code(), otf_pixels_core::ErrorCode::LimitExceeded);
945 let tight = Limits::default().with_max_pixels(100);
947 assert!(Header::parse(&ihdr(20, 20, 8, 0, 0), &tight).is_err());
948 assert!(Header::parse(&ihdr(10, 10, 8, 0, 0), &tight).is_ok());
949 }
950
951 #[test]
952 fn every_legal_depth_and_colour_combination_is_accepted() {
953 let limits = Limits::default();
954 let combinations: [(u8, u8); 15] = [
955 (1, 0),
956 (2, 0),
957 (4, 0),
958 (8, 0),
959 (16, 0),
960 (8, 2),
961 (16, 2),
962 (1, 3),
963 (2, 3),
964 (4, 3),
965 (8, 3),
966 (8, 4),
967 (16, 4),
968 (8, 6),
969 (16, 6),
970 ];
971 for (depth, color) in combinations {
972 assert!(
973 Header::parse(&ihdr(8, 8, depth, color, 0), &limits).is_ok(),
974 "depth {depth} colour {color} should be legal"
975 );
976 }
977 }
978
979 #[test]
980 fn row_bytes_round_up_for_sub_byte_depths() {
981 let limits = Limits::default();
982 let header = Header::parse(&ihdr(9, 1, 1, 0, 0), &limits).unwrap();
983 assert_eq!(header.row_bytes(9), 2, "9 one-bit pixels need 2 bytes");
984 assert_eq!(header.filter_stride(), 1, "sub-byte depths filter per byte");
985
986 let header = Header::parse(&ihdr(3, 1, 4, 0, 0), &limits).unwrap();
987 assert_eq!(header.row_bytes(3), 2, "3 four-bit pixels need 2 bytes");
988
989 let header = Header::parse(&ihdr(1, 1, 16, 6, 0), &limits).unwrap();
990 assert_eq!(header.row_bytes(1), 8, "one 16-bit RGBA pixel is 8 bytes");
991 assert_eq!(header.filter_stride(), 8);
992 }
993
994 #[test]
995 fn output_formats_follow_colour_type_and_transparency() {
996 let limits = Limits::default();
997 let case = |depth, color, trns| {
998 Header::parse(&ihdr(4, 4, depth, color, 0), &limits)
999 .unwrap()
1000 .output_format(trns)
1001 };
1002 assert_eq!(case(8, 0, false), PixelFormat::Gray8);
1003 assert_eq!(case(16, 0, false), PixelFormat::Gray16);
1004 assert_eq!(case(8, 0, true), PixelFormat::GrayA8, "tRNS adds alpha");
1005 assert_eq!(case(8, 2, false), PixelFormat::Rgb8);
1006 assert_eq!(case(16, 2, false), PixelFormat::Rgb16);
1007 assert_eq!(case(8, 2, true), PixelFormat::Rgba8);
1008 assert_eq!(
1009 case(8, 3, false),
1010 PixelFormat::Rgb8,
1011 "palette expands to RGB"
1012 );
1013 assert_eq!(case(8, 3, true), PixelFormat::Rgba8);
1014 assert_eq!(case(8, 4, false), PixelFormat::GrayA8);
1015 assert_eq!(case(8, 6, false), PixelFormat::Rgba8);
1016 assert_eq!(case(16, 6, false), PixelFormat::Rgba16);
1017 assert_eq!(case(16, 4, false), PixelFormat::Rgba16);
1020 assert_eq!(case(16, 0, true), PixelFormat::Rgba16);
1021 assert_eq!(case(1, 0, false), PixelFormat::Gray8);
1024 assert_eq!(case(4, 0, false), PixelFormat::Gray8);
1025 }
1026
1027 #[test]
1028 fn the_paeth_predictor_matches_the_specification() {
1029 assert_eq!(paeth(0, 0, 0), 0);
1031 assert_eq!(paeth(1, 2, 3), 1, "ties resolve toward the left pixel");
1032 assert_eq!(paeth(10, 20, 30), 10);
1033 assert_eq!(paeth(200, 100, 50), 200);
1034 assert_eq!(paeth(50, 100, 200), 50);
1036 assert_eq!(paeth(5, 7, 5), 7);
1038 }
1039
1040 #[test]
1041 fn every_filter_round_trips() {
1042 let filters = [
1043 Filter::None,
1044 Filter::Sub,
1045 Filter::Up,
1046 Filter::Average,
1047 Filter::Paeth,
1048 ];
1049 let previous: Vec<u8> = (0..32).map(|i| (i * 7 % 251) as u8).collect();
1050 let original: Vec<u8> = (0..32).map(|i| (i * 13 + 5) as u8).collect();
1051 for filter in filters {
1052 for stride in [1, 3, 4, 8] {
1053 let mut filtered = Vec::new();
1054 apply_filter(filter, &original, &previous, stride, &mut filtered);
1055 let mut restored = filtered.clone();
1056 unfilter(filter, &mut restored, &previous, stride).unwrap();
1057 assert_eq!(restored, original, "{filter:?} at stride {stride}");
1058 }
1059 }
1060 }
1061
1062 #[test]
1063 fn whole_pixel_paeth_matches_the_byte_loop_at_every_stride() {
1064 for stride in [1_usize, 2, 3, 4, 6, 8] {
1066 let len = stride * 23;
1067 let previous: Vec<u8> = (0..len).map(|i| (i * 89 % 256) as u8).collect();
1068 let line: Vec<u8> = (0..len).map(|i| (i * 53 + 11) as u8).collect();
1069
1070 let mut expected = line.clone();
1071 for index in 0..len {
1072 let left = if index >= stride {
1073 expected[index - stride]
1074 } else {
1075 0
1076 };
1077 let above_left = if index >= stride {
1078 previous[index - stride]
1079 } else {
1080 0
1081 };
1082 expected[index] =
1083 expected[index].wrapping_add(paeth(left, previous[index], above_left));
1084 }
1085
1086 let mut actual = line.clone();
1087 unfilter(Filter::Paeth, &mut actual, &previous, stride).unwrap();
1088 assert_eq!(actual, expected, "stride {stride}");
1089 }
1090 }
1091
1092 #[test]
1093 fn the_first_line_filters_against_zeroes() {
1094 let zeros = vec![0_u8; 8];
1097 let original: Vec<u8> = vec![10, 20, 30, 40, 50, 60, 70, 80];
1098 for filter in [Filter::Sub, Filter::Up, Filter::Average, Filter::Paeth] {
1099 let mut filtered = Vec::new();
1100 apply_filter(filter, &original, &zeros, 3, &mut filtered);
1101 let mut restored = filtered;
1102 unfilter(filter, &mut restored, &zeros, 3).unwrap();
1103 assert_eq!(restored, original, "{filter:?}");
1104 }
1105 }
1106
1107 #[test]
1108 fn filter_bytes_are_validated() {
1109 assert_eq!(Filter::from_byte(0).unwrap(), Filter::None);
1110 assert_eq!(Filter::from_byte(4).unwrap(), Filter::Paeth);
1111 let err = Filter::from_byte(5).unwrap_err();
1112 assert_eq!(err.code(), otf_pixels_core::ErrorCode::Malformed);
1113 assert!(Filter::from_byte(255).is_err());
1114 for byte in 0..=4_u8 {
1115 assert_eq!(Filter::from_byte(byte).unwrap().to_byte(), byte);
1116 }
1117 }
1118
1119 #[test]
1120 fn unfiltering_mismatched_lines_is_an_error() {
1121 let mut current = vec![0_u8; 8];
1122 let err = unfilter(Filter::Up, &mut current, &[0; 4], 1).unwrap_err();
1123 assert_eq!(err.code(), otf_pixels_core::ErrorCode::Malformed);
1124 }
1125
1126 #[test]
1127 fn adam7_passes_partition_the_image() {
1128 for width in 1..=17_u32 {
1131 for height in 1..=17_u32 {
1132 let mut seen = vec![0_u32; (width * height) as usize];
1133 for pass in 0..7 {
1134 let (pass_width, pass_height) = adam7_pass_size(pass, width, height);
1135 for y in 0..pass_height {
1136 for x in 0..pass_width {
1137 let (image_x, image_y) = adam7_position(pass, x, y);
1138 assert!(image_x < width, "{width}x{height} pass {pass}");
1139 assert!(image_y < height, "{width}x{height} pass {pass}");
1140 seen[(image_y * width + image_x) as usize] += 1;
1141 }
1142 }
1143 }
1144 assert!(
1145 seen.iter().all(|&count| count == 1),
1146 "{width}x{height} is not partitioned exactly once per pixel"
1147 );
1148 }
1149 }
1150 }
1151
1152 #[test]
1153 fn small_images_have_empty_adam7_passes() {
1154 assert_eq!(adam7_pass_size(0, 1, 1), (1, 1));
1160 for pass in 1..7 {
1161 let (width, height) = adam7_pass_size(pass, 1, 1);
1162 assert!(
1163 width == 0 || height == 0,
1164 "pass {pass} of a 1x1 should be empty, got {width}x{height}"
1165 );
1166 }
1167 let sizes: Vec<(u32, u32)> = (0..7).map(|p| adam7_pass_size(p, 5, 5)).collect();
1171 assert_eq!(sizes[0], (1, 1));
1172 assert!(
1173 sizes.iter().all(|&(w, h)| w > 0 && h > 0),
1174 "5x5 passes: {sizes:?}"
1175 );
1176 let covered: u32 = sizes.iter().map(|&(w, h)| w * h).sum();
1177 assert_eq!(covered, 25, "passes must cover every pixel of a 5x5");
1178
1179 let covered: u32 = (0..7)
1181 .map(|p| adam7_pass_size(p, 3, 2))
1182 .map(|(w, h)| w * h)
1183 .sum();
1184 assert_eq!(covered, 6, "passes must cover every pixel of a 3x2");
1185 }
1186
1187 #[test]
1188 fn chunks_round_trip_with_their_crc() {
1189 let mut out = Vec::new();
1190 out.extend_from_slice(&SIGNATURE);
1191 write_chunk(&mut out, b"IHDR", &ihdr(4, 4, 8, 0, 0));
1192 write_chunk(&mut out, b"IEND", &[]);
1193
1194 let mut reader = ChunkReader::new(&out).unwrap();
1195 let first = reader.next_chunk().unwrap();
1196 assert!(first.is(b"IHDR"));
1197 assert_eq!(first.data.len(), 13);
1198 assert!(!first.is_ancillary(), "IHDR is critical");
1199 let second = reader.next_chunk().unwrap();
1200 assert!(second.is(b"IEND"));
1201 assert!(reader.is_finished());
1202 }
1203
1204 #[test]
1205 fn a_corrupted_crc_is_detected() {
1206 let mut out = Vec::new();
1208 out.extend_from_slice(&SIGNATURE);
1209 write_chunk(&mut out, b"IHDR", &ihdr(4, 4, 8, 0, 0));
1210 let last = out.len() - 1;
1211 out[last] ^= 0xFF;
1212 let mut reader = ChunkReader::new(&out).unwrap();
1213 let err = reader.next_chunk().unwrap_err();
1214 assert!(err.to_string().contains("CRC"), "{err}");
1215 }
1216
1217 #[test]
1218 fn a_bad_signature_is_rejected() {
1219 assert!(ChunkReader::new(&[]).is_err());
1221 assert!(
1222 ChunkReader::new(&[0x89, b'P', b'N', b'G']).is_err(),
1223 "truncated"
1224 );
1225 let mut wrong = SIGNATURE;
1226 wrong[0] = 0x88;
1227 assert!(ChunkReader::new(&wrong).is_err());
1228 let mut mangled = SIGNATURE;
1230 mangled[4] = b'\n';
1231 assert!(ChunkReader::new(&mangled).is_err());
1232 }
1233
1234 #[test]
1235 fn an_overlong_chunk_length_is_rejected_before_allocating() {
1236 let mut out = Vec::new();
1238 out.extend_from_slice(&SIGNATURE);
1239 out.extend_from_slice(&0xFFFF_FFFF_u32.to_be_bytes());
1240 out.extend_from_slice(b"IDAT");
1241 let mut reader = ChunkReader::new(&out).unwrap();
1242 let err = reader.next_chunk().unwrap_err();
1243 assert_eq!(err.code(), otf_pixels_core::ErrorCode::Malformed);
1244 assert!(err.to_string().contains("2^31-1"), "{err}");
1245 }
1246
1247 #[test]
1248 fn ancillary_chunks_are_distinguishable_from_critical_ones() {
1249 let critical = Chunk {
1250 kind: *b"IHDR",
1251 data: Vec::new(),
1252 };
1253 let ancillary = Chunk {
1254 kind: *b"tEXt",
1255 data: Vec::new(),
1256 };
1257 assert!(!critical.is_ancillary());
1258 assert!(ancillary.is_ancillary());
1259 assert_eq!(ancillary.name(), "tEXt");
1260 }
1261
1262 #[test]
1263 fn the_filtered_size_accounts_for_interlace_overhead() {
1264 let limits = Limits::default();
1265 let plain = Header::parse(&ihdr(8, 8, 8, 0, 0), &limits).unwrap();
1266 assert_eq!(plain.filtered_size(), 8 * (1 + 8));
1267 let interlaced = Header::parse(&ihdr(8, 8, 8, 0, 1), &limits).unwrap();
1270 assert!(interlaced.filtered_size() > plain.filtered_size());
1271 }
1272}