Skip to main content

otf_pixels_codec_png/
format.rs

1//! PNG structure: chunks, the header, filters, Adam7 interlacing, palettes.
2//!
3//! Everything here operates on already-buffered bytes and returns errors for
4//! anything malformed. The PNG specification is ISO/IEC 15948; section
5//! references below are to it.
6
7use otf_pixels_core::{ImageDescriptor, Limits, PixelFormat, PixelsError, Result, Source};
8
9use otf_pixels_compress::Crc32;
10
11/// The eight-byte PNG signature (§5.2).
12///
13/// The non-ASCII bytes are deliberate: they catch transfers that mangle
14/// line endings or strip the high bit.
15pub const SIGNATURE: [u8; 8] = [0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
16
17/// A PNG colour type (§11.2.2).
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum ColorType {
20    /// Greyscale.
21    Grayscale,
22    /// Red, green, blue.
23    Rgb,
24    /// Palette indices; requires a `PLTE` chunk.
25    Palette,
26    /// Greyscale with an alpha channel.
27    GrayscaleAlpha,
28    /// RGB with an alpha channel.
29    Rgba,
30}
31
32impl ColorType {
33    /// The colour type for a header byte.
34    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    /// The header byte for this colour type.
49    #[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    /// Channels per pixel *in the encoded stream* (palette counts as one).
61    #[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    /// Whether this colour type permits `depth` (§11.2.2 table).
72    #[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/// A parsed `IHDR` chunk (§11.2.2).
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84#[non_exhaustive]
85pub struct Header {
86    /// Image width in pixels.
87    pub width: u32,
88    /// Image height in pixels.
89    pub height: u32,
90    /// Bits per channel: 1, 2, 4, 8 or 16.
91    pub bit_depth: u8,
92    /// How samples are interpreted.
93    pub color_type: ColorType,
94    /// Whether the image is Adam7 interlaced.
95    pub interlaced: bool,
96}
97
98impl Header {
99    /// Parse a 13-byte `IHDR` payload, validating it against `limits`.
100    ///
101    /// Dimension limits are enforced here, before any pixel buffer exists
102    /// (SPEC §Safety), so a header claiming 4 billion pixels costs nothing.
103    ///
104    /// # Errors
105    ///
106    /// Returns [`PixelsError::Malformed`] for an invalid header, or
107    /// [`PixelsError::LimitExceeded`] if the dimensions exceed `limits`.
108    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        // Checked before any allocation proportional to the claimed size.
160        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    /// Bits per pixel in the encoded stream.
172    #[must_use]
173    pub const fn bits_per_pixel(&self) -> usize {
174        self.color_type.channels() * self.bit_depth as usize
175    }
176
177    /// Bytes in one filtered scanline of `width` pixels, excluding the filter
178    /// byte. Sub-byte depths round up.
179    #[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    /// Bytes per pixel, rounded up — the filter offset (§9.2).
185    ///
186    /// For depths below 8 this is 1, which is what the filters require.
187    #[must_use]
188    pub const fn filter_stride(&self) -> usize {
189        // `max` is not const-stable, and bits_per_pixel is never zero for a
190        // validated header, so the floor is written out longhand.
191        let bytes = self.bits_per_pixel().div_ceil(8);
192        if bytes == 0 { 1 } else { bytes }
193    }
194
195    /// The pixel format this header decodes into.
196    ///
197    /// The mapping is constrained by SPEC §Pixel formats, which has no
198    /// 16-bit grey-with-alpha type. Rather than silently truncating those to
199    /// 8 bits, they widen to `Rgba16`: more memory, but no data thrown away by
200    /// a decoder the caller did not ask to be lossy.
201    ///
202    /// Sub-byte greyscale expands to `Gray8` and palettes expand to RGB or
203    /// RGBA, because the engine's formats are byte-aligned and unpalettised.
204    #[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                // No GrayA16 exists, so widen rather than truncate.
213                (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            // Palette entries are 8-bit by definition (§11.2.3).
236            ColorType::Palette => {
237                if has_transparency {
238                    PixelFormat::Rgba8
239                } else {
240                    PixelFormat::Rgb8
241                }
242            }
243        }
244    }
245
246    /// The engine descriptor for this header.
247    ///
248    /// # Errors
249    ///
250    /// Propagates [`ImageDescriptor::with_limits`].
251    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    /// Total filtered bytes the whole image decompresses to.
261    ///
262    /// This is the exact output limit handed to inflate, which is what turns a
263    /// decompression bomb into a malformed-input error (SPEC §Safety).
264    #[must_use]
265    pub fn filtered_size(&self) -> usize {
266        if self.interlaced {
267            // Each Adam7 pass is its own filtered raster with its own filter
268            // bytes, so the total exceeds the non-interlaced size.
269            (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
285/// Read a big-endian `u32` at `offset`.
286fn 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/// One PNG chunk (§5.3).
299#[derive(Debug, Clone)]
300pub struct Chunk {
301    /// The four-byte type code, e.g. `IHDR`.
302    pub kind: [u8; 4],
303    /// The chunk payload, excluding length, type and CRC.
304    pub data: Vec<u8>,
305}
306
307impl Chunk {
308    /// Whether this chunk's type matches `name`.
309    #[must_use]
310    pub fn is(&self, name: &[u8; 4]) -> bool {
311        self.kind == *name
312    }
313
314    /// Whether an unrecognised chunk may be skipped (§5.4).
315    ///
316    /// The fifth bit of the first byte is the ancillary bit: lowercase means
317    /// ancillary, so a decoder that does not understand it may ignore it. An
318    /// unknown *critical* chunk means the image cannot be rendered correctly,
319    /// so it is an error rather than something to skip.
320    #[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    /// The chunk type as a display string, for diagnostics.
327    #[must_use]
328    pub fn name(&self) -> String {
329        String::from_utf8_lossy(&self.kind).into_owned()
330    }
331}
332
333/// Reads chunks from an in-memory PNG stream.
334#[derive(Debug)]
335pub struct ChunkReader<'a> {
336    data: &'a [u8],
337    position: usize,
338}
339
340impl<'a> ChunkReader<'a> {
341    /// Start after the signature, which is verified here.
342    ///
343    /// # Errors
344    ///
345    /// Returns [`PixelsError::Malformed`] if the signature is absent or wrong.
346    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    /// Whether every byte has been consumed.
357    #[cfg(test)]
358    #[must_use]
359    pub const fn is_finished(&self) -> bool {
360        self.position >= self.data.len()
361    }
362
363    /// Read the next chunk, verifying its CRC.
364    ///
365    /// # Errors
366    ///
367    /// Returns [`PixelsError::Malformed`] for a truncated chunk, a length
368    /// beyond the spec's limit, or a CRC mismatch.
369    pub fn next_chunk(&mut self) -> Result<Chunk> {
370        let length = read_u32(self.data, self.position)? as usize;
371        // §5.3: lengths must not exceed 2^31-1. Rejecting here means the
372        // allocation below is always sane.
373        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
423/// The largest ancillary chunk read whole before being discarded.
424///
425/// Ancillary chunks are skipped, but a streaming reader still has to walk past
426/// them. Reading in bounded pieces means a chunk declaring 2 GiB of text costs
427/// time rather than memory.
428const SKIP_BUFFER: usize = 32 * 1024;
429
430/// Reads chunks from a forward-only [`Source`], one piece at a time.
431///
432/// The in-memory [`ChunkReader`] needs the whole file; this one needs only the
433/// chunk it is currently walking through, which is what lets a PNG decode in
434/// constant memory (SPEC §Guarantees 1). `IDAT` payloads are handed out in
435/// pieces rather than collected, so a 2 GiB image never exists as bytes.
436#[derive(Debug)]
437pub struct ChunkStream<S: Source> {
438    source: S,
439    /// Payload bytes of the current chunk still unread.
440    remaining: usize,
441    /// Running CRC over the type and payload seen so far.
442    crc: Crc32,
443    kind: [u8; 4],
444    open: bool,
445}
446
447impl<S: Source> ChunkStream<S> {
448    /// Start reading chunks from `source`, which must be positioned just past
449    /// the signature.
450    #[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    /// Whether the open chunk's payload has been fully read.
462    #[must_use]
463    pub const fn payload_done(&self) -> bool {
464        self.remaining == 0
465    }
466
467    /// Whether the open chunk may be skipped if unrecognised (§5.4).
468    #[must_use]
469    pub const fn is_ancillary(&self) -> bool {
470        self.kind[0] & 0x20 != 0
471    }
472
473    /// The open chunk's type as a display string, for diagnostics.
474    #[must_use]
475    pub fn name(&self) -> String {
476        String::from_utf8_lossy(&self.kind).into_owned()
477    }
478
479    /// Open the next chunk, returning its type.
480    ///
481    /// # Errors
482    ///
483    /// Returns [`PixelsError::Malformed`] for a truncated header or a length
484    /// beyond the specification's 2^31-1 limit, and [`PixelsError::Io`] on
485    /// source failure.
486    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        // §5.3: lengths must not exceed 2^31-1.
491        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    /// Read up to `buf.len()` payload bytes of the open chunk.
509    ///
510    /// Returns zero when the payload is exhausted, at which point
511    /// [`ChunkStream::close`] verifies the CRC.
512    ///
513    /// # Errors
514    ///
515    /// Returns [`PixelsError::Io`] on source failure, or
516    /// [`PixelsError::Malformed`] if the stream ends inside the payload.
517    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    /// Read the whole remaining payload. For small chunks only.
532    ///
533    /// `max` is the specification's own cap for the chunk type, so exceeding
534    /// it is malformed input rather than a configurable limit.
535    ///
536    /// # Errors
537    ///
538    /// As [`ChunkStream::read_payload`], plus [`PixelsError::Malformed`] if
539    /// the chunk is larger than `max`.
540    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    /// Discard the rest of the payload in bounded pieces.
566    ///
567    /// # Errors
568    ///
569    /// As [`ChunkStream::read_payload`].
570    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    /// Finish the open chunk, verifying its CRC.
581    ///
582    /// # Errors
583    ///
584    /// Returns [`PixelsError::Malformed`] if the payload was not fully read or
585    /// the CRC does not match.
586    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
611/// Write one chunk, with its length, type and CRC.
612pub 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/// A PNG scanline filter (§9.2).
623#[derive(Debug, Clone, Copy, PartialEq, Eq)]
624pub enum Filter {
625    /// No transformation.
626    None,
627    /// Difference from the pixel to the left.
628    Sub,
629    /// Difference from the pixel above.
630    Up,
631    /// Difference from the mean of left and above.
632    Average,
633    /// Difference from the Paeth predictor of left, above and above-left.
634    Paeth,
635}
636
637impl Filter {
638    /// The filter for a leading scanline byte.
639    ///
640    /// # Errors
641    ///
642    /// Returns [`PixelsError::Malformed`] for a byte above 4.
643    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    /// The byte for this filter.
658    #[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
670/// The Paeth predictor (§9.4).
671///
672/// Chooses whichever of left, above and above-left is closest to `a + b - c`.
673const 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    // Ties resolve toward `a`, then `b`; the order is normative.
679    if pa <= pb && pa <= pc {
680        a
681    } else if pb <= pc {
682        b
683    } else {
684        c
685    }
686}
687
688/// Reverse Paeth a whole pixel of `N` bytes at a time.
689///
690/// Each byte predicts from the byte one pixel to its left, which was itself
691/// just reconstructed, so a byte-at-a-time loop is one long dependency chain.
692/// Carrying the left and above-left pixels in registers lets the channels of
693/// a pixel proceed independently. The caller passes a line that is a whole
694/// number of pixels; the first pixel's neighbours start at zero, as §9.2
695/// requires.
696///
697/// Returns `true`, so a caller can tell it apart from "no version for this
698/// stride".
699fn 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
717/// Reverse a filter, writing the reconstructed line into `current`.
718///
719/// `previous` is the already-reconstructed line above, or zeroes for the first
720/// line. `stride` is [`Header::filter_stride`].
721///
722/// # Errors
723///
724/// Returns [`PixelsError::Malformed`] if the lines disagree in length.
725pub 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    // The filter is chosen once per line, so each one gets its own loop
733    // rather than a `match` per byte. Bytes before the first pixel are
734    // treated as zero (§9.2), which is what the `stride` split expresses.
735    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                // The average is computed in 9 bits then truncated.
759                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            // Every stride PNG can produce has a whole-pixel version, and a
767            // real scanline is always a whole number of pixels.
768            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            // With no left neighbours, Paeth predicts `above`.
783            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
799/// Apply a filter, writing the filtered bytes into `out`.
800pub 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
832/// Column start and step, then row start and step, for each Adam7 pass (§8.1).
833const 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/// The pixel dimensions of one Adam7 pass over a `width` x `height` image.
844///
845/// Passes can be empty for small images, which is the classic source of
846/// interlace bugs — hence PngSuite's 1x1 through 9x9 cases.
847#[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/// Where pixel `(x, y)` of `pass` lands in the full image.
866#[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        // Wrong length.
909        assert!(Header::parse(&[0; 12], &limits).is_err());
910        assert!(Header::parse(&[0; 14], &limits).is_err());
911        // Zero dimensions.
912        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        // Bad bit depth (PngSuite xd*).
915        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        // Bad colour type (PngSuite xc*).
918        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        // Depth/colour combinations the spec forbids.
921        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        // Unknown compression, filter or interlace method.
930        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        // A hostile header must be rejected before any buffer exists.
942        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        // A tighter limit rejects a modest image too.
946        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        // v1 has no 16-bit grey+alpha format, so those widen rather than
1018        // silently losing the low byte.
1019        assert_eq!(case(16, 4, false), PixelFormat::Rgba16);
1020        assert_eq!(case(16, 0, true), PixelFormat::Rgba16);
1021        // Sub-byte greyscale expands to Gray8, since engine formats are
1022        // byte-aligned.
1023        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        // The worked examples from PNG §9.4, including the tie rules.
1030        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        // p = 50 + 100 - 200 = -50, so the left pixel is nearest.
1035        assert_eq!(paeth(50, 100, 200), 50);
1036        // a + b - c exactly equals one of them.
1037        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        // The byte loop is the reference: it is §9.4 written out directly.
1065        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        // Bytes above the first line are zero, and bytes left of the first
1095        // pixel are zero (§9.2).
1096        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        // Every pixel must belong to exactly one pass. Small images are where
1129        // this goes wrong, so they are swept exhaustively.
1130        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        // A 1x1 image lives entirely in pass 0; the rest are empty. Forgetting
1155        // this is the classic interlace crash.
1156        //
1157        // "Empty" means zero in *either* dimension, not (0, 0): pass 1 of a
1158        // 1x1 is (0, 1), because the single row is in range but no column is.
1159        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        // A 5x5 image, by contrast, has every pass non-empty — the passes are
1168        // uneven, not absent. What must hold at every size is that they cover
1169        // the image exactly once.
1170        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        // Sizes where passes really are empty.
1180        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        // PngSuite xcrn/xcsn are exactly this case.
1207        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        // PngSuite xs1n/xs2n/xs4n/xs7n.
1220        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        // The CR/LF bytes catch mangled transfers.
1229        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        // PngSuite xlfn: a length field larger than the file.
1237        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        // Interlaced images carry a filter byte per pass row, so they need
1268        // more space than the same image non-interlaced.
1269        let interlaced = Header::parse(&ihdr(8, 8, 8, 0, 1), &limits).unwrap();
1270        assert!(interlaced.filtered_size() > plain.filtered_size());
1271    }
1272}