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