Skip to main content

otf_pixels_codec_png/
decoder.rs

1//! The PNG decoder.
2//!
3//! # Laziness
4//!
5//! Construction reads the chunks up to the first `IDAT` and stops at its
6//! header, so the descriptor and orientation are known without touching pixel
7//! data (SPEC §Guarantees 3). Everything that changes how a pixel is read or
8//! shown — `PLTE`, `tRNS`, `eXIf` — precedes `IDAT` (§5.6), so nothing found
9//! later can revise them. The image data is read on the first
10//! [`Decoder::read_row`].
11//!
12//! # Memory
13//!
14//! A non-interlaced PNG **streams**: chunks are walked one piece at a time,
15//! inflate runs incrementally, and each scanline is unfiltered and expanded as
16//! it arrives. Peak memory is two scanlines plus the 32 KiB inflate window and
17//! a read buffer — none of which grow with image height. That is what makes
18//! SPEC §Guarantees 1 true for PNG rather than merely claimed.
19//!
20//! Interlaced PNG is **internally buffered**, as SPEC §Formats says: Adam7
21//! scatters each pass across the whole image, so no row is final until every
22//! pass has been read. That is ADR-0005's stated allowance for formats that
23//! leave no choice.
24
25use otf_pixels_core::{
26    Codec, DecodeCapability, Decoder, Format, ImageDescriptor, Limits, Orientation, PixelFormat,
27    PixelsError, Result, Source,
28};
29
30use crate::format::{
31    ChunkReader, ChunkStream, ColorType, Filter, Header, SIGNATURE, adam7_pass_size,
32    adam7_position, unfilter,
33};
34use otf_pixels_compress::{ZlibStream, zlib_decompress};
35
36/// The specification's cap on a `PLTE` chunk: 256 entries of three bytes.
37const MAX_PLTE: usize = 256 * 3;
38/// The largest `tRNS` chunk any colour type permits: 256 palette alphas.
39const MAX_TRNS: usize = 256;
40/// How much compressed data is pulled from the source per refill.
41const READ_CHUNK: usize = 64 * 1024;
42/// How much of an `eXIf` chunk is read looking for the orientation.
43///
44/// The tag lives in the first directory, which writers put at the front; the
45/// rest is typically a thumbnail. A tag beyond this is not found, which is
46/// metadata declined rather than an image refused.
47const EXIF_PREFIX: usize = 64 * 1024;
48
49/// The largest ICC profile accepted, compressed or not. Real profiles run
50/// from a few hundred bytes (matrix/TRC) to a megabyte or two (LUT-based); a
51/// larger `iCCP` is declined as metadata, not refused as an image.
52const MAX_ICC: usize = 4 << 20;
53
54/// Transparency from a `tRNS` chunk (§11.3.2.1).
55#[derive(Debug, Clone)]
56enum Transparency {
57    /// One transparent grey level, in the image's bit depth.
58    Gray(u16),
59    /// One transparent RGB triple, in the image's bit depth.
60    Rgb(u16, u16, u16),
61    /// Per-palette-entry alpha; entries beyond the list are opaque.
62    Palette(Vec<u8>),
63}
64
65/// The profile in an `iCCP` payload (§11.3.3.3): a 1-79 byte name, a NUL,
66/// compression method 0, and the zlib-compressed profile.
67fn parse_iccp(data: &[u8]) -> Option<Vec<u8>> {
68    let nul = data
69        .iter()
70        .position(|&b| b == 0)
71        .filter(|&n| (1..=79).contains(&n))?;
72    let (&method, compressed) = data.get(nul + 1..)?.split_first()?;
73    if method != 0 {
74        return None;
75    }
76    zlib_decompress(compressed, MAX_ICC).ok()
77}
78
79/// Decodes a PNG stream.
80#[derive(Debug)]
81pub struct PngDecoder<S: Source> {
82    header: Header,
83    descriptor: ImageDescriptor,
84    /// Everything read before the image data, until a decode path takes it.
85    prelude: Option<Prelude<S>>,
86    orientation: Orientation,
87    /// The `iCCP` profile, decompressed.
88    icc: Option<Vec<u8>>,
89    /// The decoded image in output format, produced on first row read.
90    ///
91    /// Only used by the interlaced path; a non-interlaced image never
92    /// materializes here.
93    raster: Option<Vec<u8>>,
94    /// Per-row state for the non-interlaced streaming path.
95    stream: Option<Box<Streaming<S>>>,
96    row: u32,
97}
98
99/// The chunks before the image data, and the stream left at the first `IDAT`.
100#[derive(Debug)]
101struct Prelude<S: Source> {
102    /// Positioned inside the first `IDAT`, its payload unread.
103    chunks: ChunkStream<S>,
104    palette: Option<Vec<[u8; 3]>>,
105    transparency: Option<Transparency>,
106}
107
108/// Everything the streaming path carries between rows.
109///
110/// This is the whole memory cost of decoding a non-interlaced PNG: two
111/// scanlines, the inflate window, and one read buffer — none of which grow
112/// with image height.
113#[derive(Debug)]
114struct Streaming<S: Source> {
115    chunks: ChunkStream<S>,
116    zlib: ZlibStream,
117    /// Decompressed but not yet consumed filtered bytes.
118    filtered: Vec<u8>,
119    /// Read cursor into `filtered`.
120    at: usize,
121    /// The previous reconstructed scanline, which filters predict from.
122    previous: Vec<u8>,
123    palette: Option<Vec<[u8; 3]>>,
124    transparency: Option<Transparency>,
125    /// Set once `IEND` is reached or the final `IDAT` is consumed.
126    input_done: bool,
127}
128
129impl<S: Source> PngDecoder<S> {
130    /// Parse the signature, `IHDR` and every chunk before the image data.
131    ///
132    /// # Errors
133    ///
134    /// Returns [`PixelsError::Malformed`] for a bad signature, header or
135    /// pre-`IDAT` chunk, or [`PixelsError::LimitExceeded`] if the dimensions
136    /// exceed `limits`.
137    pub fn new(mut source: S, limits: Limits) -> Result<Self> {
138        // Signature plus a complete IHDR chunk: 8 + 4 + 4 + 13 + 4.
139        let mut prefix = vec![0_u8; 33];
140        source.read_exact(&mut prefix)?;
141
142        let mut reader = ChunkReader::new(&prefix)?;
143        let chunk = reader.next_chunk()?;
144        if !chunk.is(b"IHDR") {
145            return Err(PixelsError::malformed(
146                "png",
147                format!("first chunk must be IHDR, got `{}`", chunk.name()),
148            ));
149        }
150        let header = Header::parse(&chunk.data, &limits)?;
151
152        let mut chunks = ChunkStream::new(source);
153        let mut palette: Option<Vec<[u8; 3]>> = None;
154        let mut transparency: Option<Transparency> = None;
155        let mut orientation = None;
156        let mut icc = None;
157        loop {
158            let kind = chunks.open_next()?;
159            match &kind {
160                b"IHDR" => {
161                    return Err(PixelsError::malformed("png", "more than one IHDR"));
162                }
163                b"PLTE" => {
164                    let data = chunks.read_payload_to_end(MAX_PLTE)?;
165                    palette = Some(parse_plte(&data)?);
166                    chunks.close()?;
167                }
168                b"tRNS" => {
169                    let data = chunks.read_payload_to_end(MAX_TRNS)?;
170                    transparency = Some(parse_trns(&data, header.color_type)?);
171                    chunks.close()?;
172                }
173                b"eXIf" => {
174                    let mut exif = vec![0_u8; EXIF_PREFIX];
175                    let mut filled = 0;
176                    while let Some(rest) = exif.get_mut(filled..) {
177                        match chunks.read_payload(rest)? {
178                            0 => break,
179                            n => filled += n,
180                        }
181                    }
182                    exif.truncate(filled);
183                    chunks.skip_payload()?;
184                    chunks.close()?;
185                    // The first one wins; §11.3.6.1 permits only one.
186                    orientation = orientation.or_else(|| Orientation::from_exif_block(&exif));
187                }
188                b"iCCP" => {
189                    // One byte past the cap tells an oversized chunk apart.
190                    let mut data = vec![0_u8; MAX_ICC + 1];
191                    let mut filled = 0;
192                    while let Some(rest) = data.get_mut(filled..).filter(|r| !r.is_empty()) {
193                        match chunks.read_payload(rest)? {
194                            0 => break,
195                            n => filled += n,
196                        }
197                    }
198                    data.truncate(filled);
199                    chunks.skip_payload()?;
200                    chunks.close()?;
201                    // §11.3.3.3 permits one; a broken or oversized one is
202                    // dropped.
203                    if filled <= MAX_ICC {
204                        icc = icc.or_else(|| parse_iccp(&data));
205                    }
206                }
207                b"IDAT" => break,
208                b"IEND" => {
209                    return Err(PixelsError::malformed("png", "no IDAT data"));
210                }
211                _ => {
212                    if !chunks.is_ancillary() {
213                        return Err(PixelsError::malformed(
214                            "png",
215                            format!("unknown critical chunk `{}`", chunks.name()),
216                        ));
217                    }
218                    chunks.skip_payload()?;
219                    chunks.close()?;
220                }
221            }
222        }
223
224        if header.color_type == ColorType::Palette && palette.is_none() {
225            return Err(PixelsError::malformed(
226                "png",
227                "palette image has no PLTE chunk",
228            ));
229        }
230        let descriptor = header.descriptor(transparency.is_some(), &limits)?;
231
232        Ok(Self {
233            header,
234            descriptor,
235            prelude: Some(Prelude {
236                chunks,
237                palette,
238                transparency,
239            }),
240            orientation: orientation.unwrap_or_default(),
241            icc,
242            raster: None,
243            stream: None,
244            row: 0,
245        })
246    }
247
248    /// The parsed header.
249    #[must_use]
250    pub const fn header(&self) -> Header {
251        self.header
252    }
253
254    /// Start the streaming path from where construction stopped.
255    fn begin_streaming(&mut self) -> Result<Streaming<S>> {
256        let Some(Prelude {
257            chunks,
258            palette,
259            transparency,
260        }) = self.prelude.take()
261        else {
262            return Err(PixelsError::graph("png source was already consumed"));
263        };
264
265        let row_bytes = self.header.row_bytes(self.header.width);
266        Ok(Streaming {
267            chunks,
268            // The limit is the exact filtered size the header implies, which
269            // is what makes a decompression bomb a malformed-input error.
270            zlib: ZlibStream::new(self.header.filtered_size()),
271            filtered: Vec::new(),
272            at: 0,
273            previous: vec![0_u8; row_bytes],
274            palette,
275            transparency,
276            input_done: false,
277        })
278    }
279
280    /// Read the rest of the stream and produce the output-format raster.
281    fn decode_image(&mut self) -> Result<Vec<u8>> {
282        let Some(Prelude {
283            mut chunks,
284            palette,
285            transparency,
286        }) = self.prelude.take()
287        else {
288            return Err(PixelsError::graph("png source was already consumed"));
289        };
290
291        // Construction left the first IDAT open; collect it and any that
292        // follow, skipping ancillary chunks between them.
293        let mut compressed: Vec<u8> = Vec::new();
294        let mut buffer = vec![0_u8; READ_CHUNK];
295        loop {
296            loop {
297                let read = chunks.read_payload(&mut buffer)?;
298                if read == 0 {
299                    break;
300                }
301                compressed.extend_from_slice(buffer.get(..read).unwrap_or(&[]));
302            }
303            chunks.close()?;
304            match &chunks.open_next()? {
305                b"IDAT" => {}
306                b"IEND" => {
307                    chunks.skip_payload()?;
308                    chunks.close()?;
309                    break;
310                }
311                _ => {
312                    // Unknown critical chunks mean the image cannot be
313                    // rendered correctly; ancillary ones are skipped (§5.4).
314                    if !chunks.is_ancillary() {
315                        return Err(PixelsError::malformed(
316                            "png",
317                            format!("unknown critical chunk `{}`", chunks.name()),
318                        ));
319                    }
320                    chunks.skip_payload()?;
321                }
322            }
323        }
324
325        // The limit is the exact filtered size the header implies, which is
326        // what makes a decompression bomb a malformed-input error.
327        let filtered = zlib_decompress(&compressed, self.header.filtered_size())
328            .map_err(crate::compress_error)?;
329        let samples = self.unfilter_all(&filtered)?;
330        self.expand(&samples, palette.as_deref(), transparency.as_ref())
331    }
332
333    /// Reverse filtering, producing unfiltered sample rows in PNG layout.
334    ///
335    /// For interlaced images the passes are deinterlaced into a single raster
336    /// of `height` rows here, so everything downstream sees one image.
337    fn unfilter_all(&self, filtered: &[u8]) -> Result<Vec<u8>> {
338        let stride = self.header.filter_stride();
339        let full_row = self.header.row_bytes(self.header.width);
340
341        if !self.header.interlaced {
342            let mut out = vec![0_u8; self.header.height as usize * full_row];
343            let mut previous = vec![0_u8; full_row];
344            let mut at = 0;
345            for y in 0..self.header.height as usize {
346                let filter_byte = filtered
347                    .get(at)
348                    .copied()
349                    .ok_or_else(|| PixelsError::malformed("png", "raster ends early"))?;
350                let filter = Filter::from_byte(filter_byte)?;
351                at += 1;
352                let row = filtered
353                    .get(at..at + full_row)
354                    .ok_or_else(|| PixelsError::malformed("png", "scanline ends early"))?;
355                at += full_row;
356
357                let mut current = row.to_vec();
358                unfilter(filter, &mut current, &previous, stride)?;
359                let start = y * full_row;
360                if let Some(slot) = out.get_mut(start..start + full_row) {
361                    slot.copy_from_slice(&current);
362                }
363                previous = current;
364            }
365            return Ok(out);
366        }
367
368        // Adam7: each pass is an independent filtered raster, then its pixels
369        // are scattered into their positions in the full image.
370        let mut out = vec![0_u8; self.header.height as usize * full_row];
371        let mut at = 0;
372        for pass in 0..7 {
373            let (pass_width, pass_height) =
374                adam7_pass_size(pass, self.header.width, self.header.height);
375            if pass_width == 0 || pass_height == 0 {
376                continue;
377            }
378            let pass_row = self.header.row_bytes(pass_width);
379            let mut previous = vec![0_u8; pass_row];
380            for y in 0..pass_height {
381                let filter_byte = filtered.get(at).copied().ok_or_else(|| {
382                    PixelsError::malformed("png", format!("pass {pass} ends early"))
383                })?;
384                let filter = Filter::from_byte(filter_byte)?;
385                at += 1;
386                let row = filtered.get(at..at + pass_row).ok_or_else(|| {
387                    PixelsError::malformed("png", format!("pass {pass} scanline ends early"))
388                })?;
389                at += pass_row;
390
391                let mut current = row.to_vec();
392                unfilter(filter, &mut current, &previous, stride)?;
393                for x in 0..pass_width {
394                    let (image_x, image_y) = adam7_position(pass, x, y);
395                    copy_pixel_bits(
396                        &current,
397                        x as usize,
398                        &mut out,
399                        image_y as usize * full_row,
400                        image_x as usize,
401                        self.header.bits_per_pixel(),
402                    );
403                }
404                previous = current;
405            }
406        }
407        Ok(out)
408    }
409
410    /// Convert unfiltered PNG samples into the engine's output format.
411    fn expand(
412        &self,
413        samples: &[u8],
414        palette: Option<&[[u8; 3]]>,
415        transparency: Option<&Transparency>,
416    ) -> Result<Vec<u8>> {
417        let height = self.header.height as usize;
418        let row_bytes = self.header.row_bytes(self.header.width);
419        let out_row = self.descriptor.row_bytes();
420        let mut out = vec![0_u8; height * out_row];
421
422        for y in 0..height {
423            let row = samples
424                .get(y * row_bytes..(y + 1) * row_bytes)
425                .ok_or_else(|| PixelsError::malformed("png", "sample row missing"))?;
426            let Some(slot) = out.get_mut(y * out_row..(y + 1) * out_row) else {
427                return Err(PixelsError::graph("output row is missing"));
428            };
429            self.expand_row(row, palette, transparency, slot)?;
430        }
431        Ok(out)
432    }
433
434    /// Convert one unfiltered PNG scanline into the engine's output format.
435    ///
436    /// Shared by both paths: the streaming one calls it per scanline as it
437    /// arrives, the interlaced one per row of the deinterlaced raster. Having
438    /// one implementation is what makes "interlaced and non-interlaced decode
439    /// identically" a structural fact rather than a coincidence.
440    fn expand_row(
441        &self,
442        samples: &[u8],
443        palette: Option<&[[u8; 3]]>,
444        transparency: Option<&Transparency>,
445        out: &mut [u8],
446    ) -> Result<()> {
447        let format = self.descriptor.pixel;
448        let width = self.header.width as usize;
449        let depth = self.header.bit_depth;
450        let channels = self.header.color_type.channels();
451        let max = ((1_u32 << depth) - 1) as u16;
452
453        let mut at = 0;
454        for x in 0..width {
455            let mut channel = [0_u16; 4];
456            for (c, slot) in channel.iter_mut().take(channels).enumerate() {
457                *slot = read_sample(samples, x * channels + c, depth);
458            }
459            write_pixel(
460                out,
461                &mut at,
462                format,
463                self.header.color_type,
464                &channel,
465                depth,
466                max,
467                palette,
468                transparency,
469            )?;
470        }
471        Ok(())
472    }
473
474    /// The buffered path, used for interlaced images.
475    fn read_row_buffered(&mut self, out: &mut [u8]) -> Result<()> {
476        if self.raster.is_none() {
477            self.raster = Some(self.decode_image()?);
478        }
479        let Some(raster) = self.raster.as_ref() else {
480            return Err(PixelsError::graph("raster vanished after decoding"));
481        };
482        if self.row >= self.descriptor.height {
483            return Err(PixelsError::invalid_argument(
484                "out",
485                format!("all {} rows have already been read", self.descriptor.height),
486            ));
487        }
488        let row_bytes = self.descriptor.row_bytes();
489        if out.len() != row_bytes {
490            return Err(PixelsError::invalid_argument(
491                "out",
492                format!("row buffer is {} bytes, expected {row_bytes}", out.len()),
493            ));
494        }
495        let start = self.row as usize * row_bytes;
496        let row = raster
497            .get(start..start + row_bytes)
498            .ok_or_else(|| PixelsError::malformed("png", "decoded raster is short"))?;
499        out.copy_from_slice(row);
500        self.row += 1;
501        Ok(())
502    }
503}
504
505impl<S: Source> Streaming<S> {
506    /// Pull more compressed bytes and decompress them.
507    ///
508    /// Returns `false` once the stream is exhausted, so a caller asking for a
509    /// scanline that never arrives gets a truncation error rather than a hang.
510    fn refill(&mut self) -> Result<bool> {
511        if self.input_done {
512            return Ok(false);
513        }
514        let mut buffer = vec![0_u8; READ_CHUNK];
515
516        // Walk forward until this feed produced some output, or the stream
517        // ends. An IDAT boundary or a run of ancillary chunks can legitimately
518        // yield nothing, so one pass is not enough.
519        loop {
520            if self.chunks.payload_done() {
521                self.chunks.close()?;
522                // Find the next IDAT, skipping whatever sits between them.
523                loop {
524                    let kind = self.chunks.open_next()?;
525                    match &kind {
526                        b"IDAT" => break,
527                        b"IEND" => {
528                            // Closed, not just recognised: IEND carries a CRC
529                            // like any other chunk, and a stream truncated
530                            // inside it is still a truncated stream.
531                            self.chunks.skip_payload()?;
532                            self.chunks.close()?;
533                            self.input_done = true;
534                            let tail = self.zlib.finish().map_err(crate::compress_error)?;
535                            self.filtered.extend_from_slice(&tail);
536                            return Ok(!tail.is_empty());
537                        }
538                        _ => {
539                            if !self.chunks.is_ancillary() {
540                                return Err(PixelsError::malformed(
541                                    "png",
542                                    format!("unknown critical chunk `{}`", self.chunks.name()),
543                                ));
544                            }
545                            self.chunks.skip_payload()?;
546                            self.chunks.close()?;
547                        }
548                    }
549                }
550            }
551
552            let read = self.chunks.read_payload(&mut buffer)?;
553            if read == 0 {
554                continue;
555            }
556            let produced = self
557                .zlib
558                .push(buffer.get(..read).unwrap_or(&[]))
559                .map_err(crate::compress_error)?;
560            if !produced.is_empty() {
561                self.filtered.extend_from_slice(&produced);
562                return Ok(true);
563            }
564        }
565    }
566
567    /// Consume whatever follows the last scanline, so the stream is verified.
568    ///
569    /// The zlib Adler-32 and the trailing `IEND` both sit *after* the final
570    /// row. A caller that stops reading at the last row would otherwise never
571    /// reach them, and a corrupt checksum would pass unnoticed — which is
572    /// exactly what PngSuite's `xcsn0g01` checks.
573    fn finalize(&mut self) -> Result<()> {
574        while self.refill()? {}
575        if !self.input_done {
576            return Err(PixelsError::malformed(
577                "png",
578                "stream ends without an IEND chunk",
579            ));
580        }
581        Ok(())
582    }
583
584    /// Reconstruct the next scanline, returning it in PNG sample layout.
585    fn next_scanline(&mut self, row_bytes: usize, stride: usize) -> Result<Vec<u8>> {
586        let want = row_bytes + 1;
587        while self.filtered.len() - self.at < want {
588            if !self.refill()? {
589                return Err(PixelsError::malformed(
590                    "png",
591                    "image data ends before the last scanline",
592                ));
593            }
594        }
595
596        let filter_byte = self
597            .filtered
598            .get(self.at)
599            .copied()
600            .ok_or_else(|| PixelsError::malformed("png", "scanline ends early"))?;
601        let filter = Filter::from_byte(filter_byte)?;
602        let start = self.at + 1;
603        let mut current = self
604            .filtered
605            .get(start..start + row_bytes)
606            .ok_or_else(|| PixelsError::malformed("png", "scanline ends early"))?
607            .to_vec();
608        self.at = start + row_bytes;
609
610        // Consumed bytes are dropped rather than accumulated, which is what
611        // keeps this buffer at one scanline rather than one image.
612        if self.at >= self.filtered.len() {
613            self.filtered.clear();
614            self.at = 0;
615        } else if self.at > READ_CHUNK {
616            self.filtered.drain(..self.at);
617            self.at = 0;
618        }
619
620        unfilter(filter, &mut current, &self.previous, stride)?;
621        self.previous.clear();
622        self.previous.extend_from_slice(&current);
623        Ok(current)
624    }
625}
626
627/// Parse a `PLTE` payload into RGB triples.
628fn parse_plte(data: &[u8]) -> Result<Vec<[u8; 3]>> {
629    if data.len() % 3 != 0 || data.is_empty() {
630        return Err(PixelsError::malformed(
631            "png",
632            format!("PLTE length {} is not a positive multiple of 3", data.len()),
633        ));
634    }
635    if data.len() / 3 > 256 {
636        return Err(PixelsError::malformed(
637            "png",
638            "PLTE has more than 256 entries",
639        ));
640    }
641    Ok(data
642        .chunks_exact(3)
643        .map(|rgb| {
644            [
645                rgb.first().copied().unwrap_or(0),
646                rgb.get(1).copied().unwrap_or(0),
647                rgb.get(2).copied().unwrap_or(0),
648            ]
649        })
650        .collect())
651}
652
653/// Copy one pixel's bits between rasters, handling sub-byte depths.
654fn copy_pixel_bits(
655    source_row: &[u8],
656    source_x: usize,
657    out: &mut [u8],
658    row_start: usize,
659    dest_x: usize,
660    bits_per_pixel: usize,
661) {
662    if bits_per_pixel >= 8 {
663        let bytes = bits_per_pixel / 8;
664        for byte in 0..bytes {
665            let value = source_row
666                .get(source_x * bytes + byte)
667                .copied()
668                .unwrap_or(0);
669            if let Some(slot) = out.get_mut(row_start + dest_x * bytes + byte) {
670                *slot = value;
671            }
672        }
673        return;
674    }
675    // Sub-byte: read the packed field and write it at the destination offset.
676    let value = read_bits(source_row, source_x, bits_per_pixel);
677    write_bits(out, row_start, dest_x, bits_per_pixel, value);
678}
679
680/// Read a packed sub-byte field, most-significant-first within each byte.
681fn read_bits(row: &[u8], index: usize, bits: usize) -> u8 {
682    let per_byte = 8 / bits;
683    let byte = row.get(index / per_byte).copied().unwrap_or(0);
684    let shift = 8 - bits * (index % per_byte + 1);
685    (byte >> shift) & ((1 << bits) - 1) as u8
686}
687
688/// Write a packed sub-byte field.
689fn write_bits(out: &mut [u8], row_start: usize, index: usize, bits: usize, value: u8) {
690    let per_byte = 8 / bits;
691    let offset = row_start + index / per_byte;
692    let shift = 8 - bits * (index % per_byte + 1);
693    let mask = ((1 << bits) - 1) as u8;
694    if let Some(slot) = out.get_mut(offset) {
695        *slot = (*slot & !(mask << shift)) | ((value & mask) << shift);
696    }
697}
698
699/// Read sample `index` of a row at `depth` bits.
700fn read_sample(row: &[u8], index: usize, depth: u8) -> u16 {
701    match depth {
702        16 => {
703            // PNG samples are big-endian (§7.1).
704            let high = row.get(index * 2).copied().unwrap_or(0);
705            let low = row.get(index * 2 + 1).copied().unwrap_or(0);
706            u16::from_be_bytes([high, low])
707        }
708        8 => u16::from(row.get(index).copied().unwrap_or(0)),
709        bits => u16::from(read_bits(row, index, bits as usize)),
710    }
711}
712
713/// Scale a sample from `max` to the full 8-bit range.
714///
715/// The spec's rule: the value is replicated, not shifted, so 1-bit 1 becomes
716/// 255 rather than 128 (§13.13).
717const fn scale8(value: u16, max: u16) -> u8 {
718    if max == 0 {
719        return 0;
720    }
721    ((value as u32 * 255 + max as u32 / 2) / max as u32) as u8
722}
723
724/// Write one output pixel, converting from PNG's layout.
725#[allow(
726    clippy::too_many_arguments,
727    reason = "one pixel conversion needs all of it"
728)]
729fn write_pixel(
730    out: &mut [u8],
731    at: &mut usize,
732    format: PixelFormat,
733    color_type: ColorType,
734    channel: &[u16; 4],
735    depth: u8,
736    max: u16,
737    palette: Option<&[[u8; 3]]>,
738    transparency: Option<&Transparency>,
739) -> Result<()> {
740    /// Append one byte.
741    fn push(out: &mut [u8], at: &mut usize, value: u8) {
742        if let Some(slot) = out.get_mut(*at) {
743            *slot = value;
744        }
745        *at += 1;
746    }
747    /// Append one native-endian 16-bit sample.
748    fn push16(out: &mut [u8], at: &mut usize, value: u16) {
749        for byte in value.to_ne_bytes() {
750            push(out, at, byte);
751        }
752    }
753
754    match color_type {
755        ColorType::Palette => {
756            let index = channel[0] as usize;
757            let entries = palette
758                .ok_or_else(|| PixelsError::malformed("png", "palette image without a palette"))?;
759            let rgb = entries.get(index).copied().ok_or_else(|| {
760                PixelsError::malformed(
761                    "png",
762                    format!(
763                        "palette index {index} is beyond the {}-entry palette",
764                        entries.len()
765                    ),
766                )
767            })?;
768            push(out, at, rgb[0]);
769            push(out, at, rgb[1]);
770            push(out, at, rgb[2]);
771            if format == PixelFormat::Rgba8 {
772                let alpha = match transparency {
773                    Some(Transparency::Palette(alphas)) => {
774                        // Entries past the tRNS list are fully opaque.
775                        alphas.get(index).copied().unwrap_or(255)
776                    }
777                    _ => 255,
778                };
779                push(out, at, alpha);
780            }
781        }
782        ColorType::Grayscale => {
783            let transparent =
784                matches!(transparency, Some(Transparency::Gray(key)) if *key == channel[0]);
785            match format {
786                PixelFormat::Gray8 => push(out, at, scale8(channel[0], max)),
787                PixelFormat::Gray16 => push16(out, at, channel[0]),
788                PixelFormat::GrayA8 => {
789                    push(out, at, scale8(channel[0], max));
790                    push(out, at, if transparent { 0 } else { 255 });
791                }
792                PixelFormat::Rgba16 => {
793                    let value = if depth == 16 {
794                        channel[0]
795                    } else {
796                        channel[0] * 257
797                    };
798                    push16(out, at, value);
799                    push16(out, at, value);
800                    push16(out, at, value);
801                    push16(out, at, if transparent { 0 } else { u16::MAX });
802                }
803                other => {
804                    return Err(PixelsError::unsupported(format!(
805                        "greyscale cannot be written as {other}"
806                    )));
807                }
808            }
809        }
810        ColorType::GrayscaleAlpha => match format {
811            PixelFormat::GrayA8 => {
812                push(out, at, scale8(channel[0], max));
813                push(out, at, scale8(channel[1], max));
814            }
815            PixelFormat::Rgba16 => {
816                push16(out, at, channel[0]);
817                push16(out, at, channel[0]);
818                push16(out, at, channel[0]);
819                push16(out, at, channel[1]);
820            }
821            other => {
822                return Err(PixelsError::unsupported(format!(
823                    "grey+alpha cannot be written as {other}"
824                )));
825            }
826        },
827        ColorType::Rgb => {
828            let transparent = matches!(
829                transparency,
830                Some(Transparency::Rgb(r, g, b))
831                    if *r == channel[0] && *g == channel[1] && *b == channel[2]
832            );
833            match format {
834                PixelFormat::Rgb8 => {
835                    for &value in channel.iter().take(3) {
836                        push(out, at, scale8(value, max));
837                    }
838                }
839                PixelFormat::Rgb16 => {
840                    for &value in channel.iter().take(3) {
841                        push16(out, at, value);
842                    }
843                }
844                PixelFormat::Rgba8 => {
845                    for &value in channel.iter().take(3) {
846                        push(out, at, scale8(value, max));
847                    }
848                    push(out, at, if transparent { 0 } else { 255 });
849                }
850                PixelFormat::Rgba16 => {
851                    for &value in channel.iter().take(3) {
852                        push16(out, at, value);
853                    }
854                    push16(out, at, if transparent { 0 } else { u16::MAX });
855                }
856                other => {
857                    return Err(PixelsError::unsupported(format!(
858                        "RGB cannot be written as {other}"
859                    )));
860                }
861            }
862        }
863        ColorType::Rgba => match format {
864            PixelFormat::Rgba8 => {
865                for &value in channel {
866                    push(out, at, scale8(value, max));
867                }
868            }
869            PixelFormat::Rgba16 => {
870                for &value in channel {
871                    push16(out, at, value);
872                }
873            }
874            other => {
875                return Err(PixelsError::unsupported(format!(
876                    "RGBA cannot be written as {other}"
877                )));
878            }
879        },
880    }
881    Ok(())
882}
883
884/// Parse a `tRNS` payload for `color_type`.
885fn parse_trns(data: &[u8], color_type: ColorType) -> Result<Transparency> {
886    /// Read a big-endian `u16` at `offset`.
887    fn be16(data: &[u8], offset: usize) -> u16 {
888        u16::from_be_bytes([
889            data.get(offset).copied().unwrap_or(0),
890            data.get(offset + 1).copied().unwrap_or(0),
891        ])
892    }
893    match color_type {
894        ColorType::Grayscale => {
895            if data.len() != 2 {
896                return Err(PixelsError::malformed(
897                    "png",
898                    format!("greyscale tRNS must be 2 bytes, got {}", data.len()),
899                ));
900            }
901            Ok(Transparency::Gray(be16(data, 0)))
902        }
903        ColorType::Rgb => {
904            if data.len() != 6 {
905                return Err(PixelsError::malformed(
906                    "png",
907                    format!("RGB tRNS must be 6 bytes, got {}", data.len()),
908                ));
909            }
910            Ok(Transparency::Rgb(
911                be16(data, 0),
912                be16(data, 2),
913                be16(data, 4),
914            ))
915        }
916        ColorType::Palette => {
917            if data.len() > 256 {
918                return Err(PixelsError::malformed(
919                    "png",
920                    format!(
921                        "palette tRNS has {} entries, over the 256 maximum",
922                        data.len()
923                    ),
924                ));
925            }
926            Ok(Transparency::Palette(data.to_vec()))
927        }
928        // §11.3.2.1: tRNS is forbidden where alpha is already present.
929        ColorType::GrayscaleAlpha | ColorType::Rgba => Err(PixelsError::malformed(
930            "png",
931            "tRNS is not allowed for colour types that already carry alpha",
932        )),
933    }
934}
935
936impl<S: Source + std::fmt::Debug> Decoder for PngDecoder<S> {
937    fn descriptor(&self) -> ImageDescriptor {
938        self.descriptor
939    }
940
941    /// From an `eXIf` chunk before the image data. One after it is not
942    /// seen: by then the pipeline has been built, and §5.6 puts `eXIf` first.
943    fn orientation(&self) -> Orientation {
944        self.orientation
945    }
946
947    /// From an `iCCP` chunk before the image data.
948    fn icc_profile(&self) -> Option<&[u8]> {
949        self.icc.as_deref()
950    }
951
952    fn capability(&self) -> DecodeCapability {
953        // The raster is fully materialized before the first row is served, so
954        // any region could in principle be answered. Declaring `Sequential`
955        // keeps the streaming contract of ADR-0005 and costs nothing, since
956        // the scheduler pulls rows in order anyway.
957        DecodeCapability::Sequential
958    }
959
960    fn read_row(&mut self, out: &mut [u8]) -> Result<()> {
961        // Interlaced images need every pass before any row is final, so they
962        // take the buffered path; everything else streams.
963        if self.header.interlaced {
964            return self.read_row_buffered(out);
965        }
966        if self.stream.is_none() {
967            let started = self.begin_streaming()?;
968            self.stream = Some(Box::new(started));
969        }
970        if self.row >= self.descriptor.height {
971            return Err(PixelsError::invalid_argument(
972                "out",
973                format!("all {} rows have already been read", self.descriptor.height),
974            ));
975        }
976        let row_bytes = self.descriptor.row_bytes();
977        if out.len() != row_bytes {
978            return Err(PixelsError::invalid_argument(
979                "out",
980                format!("row buffer is {} bytes, expected {row_bytes}", out.len()),
981            ));
982        }
983
984        let sample_bytes = self.header.row_bytes(self.header.width);
985        let stride = self.header.filter_stride();
986        let Some(stream) = self.stream.as_mut() else {
987            return Err(PixelsError::graph("png stream vanished after starting"));
988        };
989        let samples = stream.next_scanline(sample_bytes, stride)?;
990        let palette = stream.palette.clone();
991        let transparency = stream.transparency.clone();
992
993        self.expand_row(&samples, palette.as_deref(), transparency.as_ref(), out)?;
994        self.row += 1;
995
996        // The checksum and IEND follow the last scanline, so the stream is
997        // only fully verified once it has been read to its end.
998        if self.row == self.descriptor.height {
999            if let Some(stream) = self.stream.as_mut() {
1000                stream.finalize()?;
1001            }
1002        }
1003        Ok(())
1004    }
1005}
1006
1007/// Whether `prefix` starts with the PNG signature.
1008///
1009/// Detection is by magic bytes only (SPEC §Formats).
1010#[must_use]
1011pub fn probe(prefix: &[u8]) -> bool {
1012    prefix.get(..8) == Some(&SIGNATURE[..])
1013}
1014
1015/// The PNG entry in a sniffing registry.
1016///
1017/// Format detection is by magic bytes only (SPEC §Formats); PNG's eight-byte
1018/// signature is deliberately designed to survive — and detect — the transfer
1019/// corruptions its §5.2 enumerates, so there is nothing else worth consulting.
1020#[derive(Debug, Clone, Copy, Default)]
1021pub struct PngCodec;
1022
1023impl Codec for PngCodec {
1024    fn format(&self) -> Format {
1025        Format::Png
1026    }
1027
1028    fn magic_len(&self) -> usize {
1029        SIGNATURE.len()
1030    }
1031
1032    fn probe(&self, prefix: &[u8]) -> bool {
1033        probe(prefix)
1034    }
1035}