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        if depth == 8 {
454            // 8-bit samples already in the output layout are the common
455            // case, and the per-pixel conversion below is the identity for
456            // them (`scale8` of an 8-bit value is that value).
457            let same_layout = matches!(
458                (self.header.color_type, format),
459                (ColorType::Rgb, PixelFormat::Rgb8)
460                    | (ColorType::Rgba, PixelFormat::Rgba8)
461                    | (ColorType::Grayscale, PixelFormat::Gray8)
462                    | (ColorType::GrayscaleAlpha, PixelFormat::GrayA8)
463            );
464            if same_layout && samples.len() == out.len() {
465                out.copy_from_slice(samples);
466                return Ok(());
467            }
468            if matches!(self.header.color_type, ColorType::Palette) {
469                return expand_palette_row(samples, format, palette, transparency, out);
470            }
471        }
472
473        let mut at = 0;
474        for x in 0..width {
475            let mut channel = [0_u16; 4];
476            for (c, slot) in channel.iter_mut().take(channels).enumerate() {
477                *slot = read_sample(samples, x * channels + c, depth);
478            }
479            write_pixel(
480                out,
481                &mut at,
482                format,
483                self.header.color_type,
484                &channel,
485                depth,
486                max,
487                palette,
488                transparency,
489            )?;
490        }
491        Ok(())
492    }
493
494    /// The buffered path, used for interlaced images.
495    fn read_row_buffered(&mut self, out: &mut [u8]) -> Result<()> {
496        if self.raster.is_none() {
497            self.raster = Some(self.decode_image()?);
498        }
499        let Some(raster) = self.raster.as_ref() else {
500            return Err(PixelsError::graph("raster vanished after decoding"));
501        };
502        if self.row >= self.descriptor.height {
503            return Err(PixelsError::invalid_argument(
504                "out",
505                format!("all {} rows have already been read", self.descriptor.height),
506            ));
507        }
508        let row_bytes = self.descriptor.row_bytes();
509        if out.len() != row_bytes {
510            return Err(PixelsError::invalid_argument(
511                "out",
512                format!("row buffer is {} bytes, expected {row_bytes}", out.len()),
513            ));
514        }
515        let start = self.row as usize * row_bytes;
516        let row = raster
517            .get(start..start + row_bytes)
518            .ok_or_else(|| PixelsError::malformed("png", "decoded raster is short"))?;
519        out.copy_from_slice(row);
520        self.row += 1;
521        Ok(())
522    }
523}
524
525impl<S: Source> Streaming<S> {
526    /// Pull more compressed bytes and decompress them.
527    ///
528    /// Returns `false` once the stream is exhausted, so a caller asking for a
529    /// scanline that never arrives gets a truncation error rather than a hang.
530    fn refill(&mut self) -> Result<bool> {
531        if self.input_done {
532            return Ok(false);
533        }
534        let mut buffer = vec![0_u8; READ_CHUNK];
535
536        // Walk forward until this feed produced some output, or the stream
537        // ends. An IDAT boundary or a run of ancillary chunks can legitimately
538        // yield nothing, so one pass is not enough.
539        loop {
540            if self.chunks.payload_done() {
541                self.chunks.close()?;
542                // Find the next IDAT, skipping whatever sits between them.
543                loop {
544                    let kind = self.chunks.open_next()?;
545                    match &kind {
546                        b"IDAT" => break,
547                        b"IEND" => {
548                            // Closed, not just recognised: IEND carries a CRC
549                            // like any other chunk, and a stream truncated
550                            // inside it is still a truncated stream.
551                            self.chunks.skip_payload()?;
552                            self.chunks.close()?;
553                            self.input_done = true;
554                            let tail = self.zlib.finish().map_err(crate::compress_error)?;
555                            self.filtered.extend_from_slice(&tail);
556                            return Ok(!tail.is_empty());
557                        }
558                        _ => {
559                            if !self.chunks.is_ancillary() {
560                                return Err(PixelsError::malformed(
561                                    "png",
562                                    format!("unknown critical chunk `{}`", self.chunks.name()),
563                                ));
564                            }
565                            self.chunks.skip_payload()?;
566                            self.chunks.close()?;
567                        }
568                    }
569                }
570            }
571
572            let read = self.chunks.read_payload(&mut buffer)?;
573            if read == 0 {
574                continue;
575            }
576            let produced = self
577                .zlib
578                .push(buffer.get(..read).unwrap_or(&[]))
579                .map_err(crate::compress_error)?;
580            if !produced.is_empty() {
581                self.filtered.extend_from_slice(&produced);
582                return Ok(true);
583            }
584        }
585    }
586
587    /// Consume whatever follows the last scanline, so the stream is verified.
588    ///
589    /// The zlib Adler-32 and the trailing `IEND` both sit *after* the final
590    /// row. A caller that stops reading at the last row would otherwise never
591    /// reach them, and a corrupt checksum would pass unnoticed — which is
592    /// exactly what PngSuite's `xcsn0g01` checks.
593    fn finalize(&mut self) -> Result<()> {
594        while self.refill()? {}
595        if !self.input_done {
596            return Err(PixelsError::malformed(
597                "png",
598                "stream ends without an IEND chunk",
599            ));
600        }
601        Ok(())
602    }
603
604    /// Reconstruct the next scanline, returning it in PNG sample layout.
605    fn next_scanline(&mut self, row_bytes: usize, stride: usize) -> Result<Vec<u8>> {
606        let want = row_bytes + 1;
607        while self.filtered.len() - self.at < want {
608            if !self.refill()? {
609                return Err(PixelsError::malformed(
610                    "png",
611                    "image data ends before the last scanline",
612                ));
613            }
614        }
615
616        let filter_byte = self
617            .filtered
618            .get(self.at)
619            .copied()
620            .ok_or_else(|| PixelsError::malformed("png", "scanline ends early"))?;
621        let filter = Filter::from_byte(filter_byte)?;
622        let start = self.at + 1;
623        let mut current = self
624            .filtered
625            .get(start..start + row_bytes)
626            .ok_or_else(|| PixelsError::malformed("png", "scanline ends early"))?
627            .to_vec();
628        self.at = start + row_bytes;
629
630        // Consumed bytes are dropped rather than accumulated, which is what
631        // keeps this buffer at one scanline rather than one image.
632        if self.at >= self.filtered.len() {
633            self.filtered.clear();
634            self.at = 0;
635        } else if self.at > READ_CHUNK {
636            self.filtered.drain(..self.at);
637            self.at = 0;
638        }
639
640        unfilter(filter, &mut current, &self.previous, stride)?;
641        self.previous.clear();
642        self.previous.extend_from_slice(&current);
643        Ok(current)
644    }
645}
646
647/// Parse a `PLTE` payload into RGB triples.
648fn parse_plte(data: &[u8]) -> Result<Vec<[u8; 3]>> {
649    if data.len() % 3 != 0 || data.is_empty() {
650        return Err(PixelsError::malformed(
651            "png",
652            format!("PLTE length {} is not a positive multiple of 3", data.len()),
653        ));
654    }
655    if data.len() / 3 > 256 {
656        return Err(PixelsError::malformed(
657            "png",
658            "PLTE has more than 256 entries",
659        ));
660    }
661    Ok(data
662        .chunks_exact(3)
663        .map(|rgb| {
664            [
665                rgb.first().copied().unwrap_or(0),
666                rgb.get(1).copied().unwrap_or(0),
667                rgb.get(2).copied().unwrap_or(0),
668            ]
669        })
670        .collect())
671}
672
673/// Copy one pixel's bits between rasters, handling sub-byte depths.
674fn copy_pixel_bits(
675    source_row: &[u8],
676    source_x: usize,
677    out: &mut [u8],
678    row_start: usize,
679    dest_x: usize,
680    bits_per_pixel: usize,
681) {
682    if bits_per_pixel >= 8 {
683        let bytes = bits_per_pixel / 8;
684        for byte in 0..bytes {
685            let value = source_row
686                .get(source_x * bytes + byte)
687                .copied()
688                .unwrap_or(0);
689            if let Some(slot) = out.get_mut(row_start + dest_x * bytes + byte) {
690                *slot = value;
691            }
692        }
693        return;
694    }
695    // Sub-byte: read the packed field and write it at the destination offset.
696    let value = read_bits(source_row, source_x, bits_per_pixel);
697    write_bits(out, row_start, dest_x, bits_per_pixel, value);
698}
699
700/// Read a packed sub-byte field, most-significant-first within each byte.
701fn read_bits(row: &[u8], index: usize, bits: usize) -> u8 {
702    let per_byte = 8 / bits;
703    let byte = row.get(index / per_byte).copied().unwrap_or(0);
704    let shift = 8 - bits * (index % per_byte + 1);
705    (byte >> shift) & ((1 << bits) - 1) as u8
706}
707
708/// Write a packed sub-byte field.
709fn write_bits(out: &mut [u8], row_start: usize, index: usize, bits: usize, value: u8) {
710    let per_byte = 8 / bits;
711    let offset = row_start + index / per_byte;
712    let shift = 8 - bits * (index % per_byte + 1);
713    let mask = ((1 << bits) - 1) as u8;
714    if let Some(slot) = out.get_mut(offset) {
715        *slot = (*slot & !(mask << shift)) | ((value & mask) << shift);
716    }
717}
718
719/// Read sample `index` of a row at `depth` bits.
720fn read_sample(row: &[u8], index: usize, depth: u8) -> u16 {
721    match depth {
722        16 => {
723            // PNG samples are big-endian (§7.1).
724            let high = row.get(index * 2).copied().unwrap_or(0);
725            let low = row.get(index * 2 + 1).copied().unwrap_or(0);
726            u16::from_be_bytes([high, low])
727        }
728        8 => u16::from(row.get(index).copied().unwrap_or(0)),
729        bits => u16::from(read_bits(row, index, bits as usize)),
730    }
731}
732
733/// Scale a sample from `max` to the full 8-bit range.
734///
735/// The spec's rule: the value is replicated, not shifted, so 1-bit 1 becomes
736/// 255 rather than 128 (§13.13).
737const fn scale8(value: u16, max: u16) -> u8 {
738    if max == 0 {
739        return 0;
740    }
741    ((value as u32 * 255 + max as u32 / 2) / max as u32) as u8
742}
743
744/// Expand one row of 8-bit palette indices, a byte per pixel.
745///
746/// The per-pixel path handles this too; this one looks each index up once
747/// and writes the entry whole, with the same errors.
748fn expand_palette_row(
749    indices: &[u8],
750    format: PixelFormat,
751    palette: Option<&[[u8; 3]]>,
752    transparency: Option<&Transparency>,
753    out: &mut [u8],
754) -> Result<()> {
755    let entries =
756        palette.ok_or_else(|| PixelsError::malformed("png", "palette image without a palette"))?;
757    let alphas = match transparency {
758        Some(Transparency::Palette(alphas)) => alphas.as_slice(),
759        _ => &[],
760    };
761    let channels = if format == PixelFormat::Rgba8 { 4 } else { 3 };
762    for (&index, pixel) in indices.iter().zip(out.chunks_exact_mut(channels)) {
763        let index = index as usize;
764        let rgb = entries.get(index).ok_or_else(|| {
765            PixelsError::malformed(
766                "png",
767                format!(
768                    "palette index {index} is beyond the {}-entry palette",
769                    entries.len()
770                ),
771            )
772        })?;
773        for (slot, &value) in pixel.iter_mut().zip(rgb) {
774            *slot = value;
775        }
776        if let Some(alpha) = pixel.get_mut(3) {
777            // Entries past the tRNS list are fully opaque.
778            *alpha = alphas.get(index).copied().unwrap_or(255);
779        }
780    }
781    Ok(())
782}
783
784/// Write one output pixel, converting from PNG's layout.
785#[allow(
786    clippy::too_many_arguments,
787    reason = "one pixel conversion needs all of it"
788)]
789fn write_pixel(
790    out: &mut [u8],
791    at: &mut usize,
792    format: PixelFormat,
793    color_type: ColorType,
794    channel: &[u16; 4],
795    depth: u8,
796    max: u16,
797    palette: Option<&[[u8; 3]]>,
798    transparency: Option<&Transparency>,
799) -> Result<()> {
800    /// Append one byte.
801    fn push(out: &mut [u8], at: &mut usize, value: u8) {
802        if let Some(slot) = out.get_mut(*at) {
803            *slot = value;
804        }
805        *at += 1;
806    }
807    /// Append one native-endian 16-bit sample.
808    fn push16(out: &mut [u8], at: &mut usize, value: u16) {
809        for byte in value.to_ne_bytes() {
810            push(out, at, byte);
811        }
812    }
813
814    match color_type {
815        ColorType::Palette => {
816            let index = channel[0] as usize;
817            let entries = palette
818                .ok_or_else(|| PixelsError::malformed("png", "palette image without a palette"))?;
819            let rgb = entries.get(index).copied().ok_or_else(|| {
820                PixelsError::malformed(
821                    "png",
822                    format!(
823                        "palette index {index} is beyond the {}-entry palette",
824                        entries.len()
825                    ),
826                )
827            })?;
828            push(out, at, rgb[0]);
829            push(out, at, rgb[1]);
830            push(out, at, rgb[2]);
831            if format == PixelFormat::Rgba8 {
832                let alpha = match transparency {
833                    Some(Transparency::Palette(alphas)) => {
834                        // Entries past the tRNS list are fully opaque.
835                        alphas.get(index).copied().unwrap_or(255)
836                    }
837                    _ => 255,
838                };
839                push(out, at, alpha);
840            }
841        }
842        ColorType::Grayscale => {
843            let transparent =
844                matches!(transparency, Some(Transparency::Gray(key)) if *key == channel[0]);
845            match format {
846                PixelFormat::Gray8 => push(out, at, scale8(channel[0], max)),
847                PixelFormat::Gray16 => push16(out, at, channel[0]),
848                PixelFormat::GrayA8 => {
849                    push(out, at, scale8(channel[0], max));
850                    push(out, at, if transparent { 0 } else { 255 });
851                }
852                PixelFormat::Rgba16 => {
853                    let value = if depth == 16 {
854                        channel[0]
855                    } else {
856                        channel[0] * 257
857                    };
858                    push16(out, at, value);
859                    push16(out, at, value);
860                    push16(out, at, value);
861                    push16(out, at, if transparent { 0 } else { u16::MAX });
862                }
863                other => {
864                    return Err(PixelsError::unsupported(format!(
865                        "greyscale cannot be written as {other}"
866                    )));
867                }
868            }
869        }
870        ColorType::GrayscaleAlpha => match format {
871            PixelFormat::GrayA8 => {
872                push(out, at, scale8(channel[0], max));
873                push(out, at, scale8(channel[1], max));
874            }
875            PixelFormat::Rgba16 => {
876                push16(out, at, channel[0]);
877                push16(out, at, channel[0]);
878                push16(out, at, channel[0]);
879                push16(out, at, channel[1]);
880            }
881            other => {
882                return Err(PixelsError::unsupported(format!(
883                    "grey+alpha cannot be written as {other}"
884                )));
885            }
886        },
887        ColorType::Rgb => {
888            let transparent = matches!(
889                transparency,
890                Some(Transparency::Rgb(r, g, b))
891                    if *r == channel[0] && *g == channel[1] && *b == channel[2]
892            );
893            match format {
894                PixelFormat::Rgb8 => {
895                    for &value in channel.iter().take(3) {
896                        push(out, at, scale8(value, max));
897                    }
898                }
899                PixelFormat::Rgb16 => {
900                    for &value in channel.iter().take(3) {
901                        push16(out, at, value);
902                    }
903                }
904                PixelFormat::Rgba8 => {
905                    for &value in channel.iter().take(3) {
906                        push(out, at, scale8(value, max));
907                    }
908                    push(out, at, if transparent { 0 } else { 255 });
909                }
910                PixelFormat::Rgba16 => {
911                    for &value in channel.iter().take(3) {
912                        push16(out, at, value);
913                    }
914                    push16(out, at, if transparent { 0 } else { u16::MAX });
915                }
916                other => {
917                    return Err(PixelsError::unsupported(format!(
918                        "RGB cannot be written as {other}"
919                    )));
920                }
921            }
922        }
923        ColorType::Rgba => match format {
924            PixelFormat::Rgba8 => {
925                for &value in channel {
926                    push(out, at, scale8(value, max));
927                }
928            }
929            PixelFormat::Rgba16 => {
930                for &value in channel {
931                    push16(out, at, value);
932                }
933            }
934            other => {
935                return Err(PixelsError::unsupported(format!(
936                    "RGBA cannot be written as {other}"
937                )));
938            }
939        },
940    }
941    Ok(())
942}
943
944/// Parse a `tRNS` payload for `color_type`.
945fn parse_trns(data: &[u8], color_type: ColorType) -> Result<Transparency> {
946    /// Read a big-endian `u16` at `offset`.
947    fn be16(data: &[u8], offset: usize) -> u16 {
948        u16::from_be_bytes([
949            data.get(offset).copied().unwrap_or(0),
950            data.get(offset + 1).copied().unwrap_or(0),
951        ])
952    }
953    match color_type {
954        ColorType::Grayscale => {
955            if data.len() != 2 {
956                return Err(PixelsError::malformed(
957                    "png",
958                    format!("greyscale tRNS must be 2 bytes, got {}", data.len()),
959                ));
960            }
961            Ok(Transparency::Gray(be16(data, 0)))
962        }
963        ColorType::Rgb => {
964            if data.len() != 6 {
965                return Err(PixelsError::malformed(
966                    "png",
967                    format!("RGB tRNS must be 6 bytes, got {}", data.len()),
968                ));
969            }
970            Ok(Transparency::Rgb(
971                be16(data, 0),
972                be16(data, 2),
973                be16(data, 4),
974            ))
975        }
976        ColorType::Palette => {
977            if data.len() > 256 {
978                return Err(PixelsError::malformed(
979                    "png",
980                    format!(
981                        "palette tRNS has {} entries, over the 256 maximum",
982                        data.len()
983                    ),
984                ));
985            }
986            Ok(Transparency::Palette(data.to_vec()))
987        }
988        // §11.3.2.1: tRNS is forbidden where alpha is already present.
989        ColorType::GrayscaleAlpha | ColorType::Rgba => Err(PixelsError::malformed(
990            "png",
991            "tRNS is not allowed for colour types that already carry alpha",
992        )),
993    }
994}
995
996impl<S: Source + std::fmt::Debug> Decoder for PngDecoder<S> {
997    fn descriptor(&self) -> ImageDescriptor {
998        self.descriptor
999    }
1000
1001    /// From an `eXIf` chunk before the image data. One after it is not
1002    /// seen: by then the pipeline has been built, and §5.6 puts `eXIf` first.
1003    fn orientation(&self) -> Orientation {
1004        self.orientation
1005    }
1006
1007    /// From an `iCCP` chunk before the image data.
1008    fn icc_profile(&self) -> Option<&[u8]> {
1009        self.icc.as_deref()
1010    }
1011
1012    fn capability(&self) -> DecodeCapability {
1013        // The raster is fully materialized before the first row is served, so
1014        // any region could in principle be answered. Declaring `Sequential`
1015        // keeps the streaming contract of ADR-0005 and costs nothing, since
1016        // the scheduler pulls rows in order anyway.
1017        DecodeCapability::Sequential
1018    }
1019
1020    fn read_row(&mut self, out: &mut [u8]) -> Result<()> {
1021        // Interlaced images need every pass before any row is final, so they
1022        // take the buffered path; everything else streams.
1023        if self.header.interlaced {
1024            return self.read_row_buffered(out);
1025        }
1026        if self.stream.is_none() {
1027            let started = self.begin_streaming()?;
1028            self.stream = Some(Box::new(started));
1029        }
1030        if self.row >= self.descriptor.height {
1031            return Err(PixelsError::invalid_argument(
1032                "out",
1033                format!("all {} rows have already been read", self.descriptor.height),
1034            ));
1035        }
1036        let row_bytes = self.descriptor.row_bytes();
1037        if out.len() != row_bytes {
1038            return Err(PixelsError::invalid_argument(
1039                "out",
1040                format!("row buffer is {} bytes, expected {row_bytes}", out.len()),
1041            ));
1042        }
1043
1044        let sample_bytes = self.header.row_bytes(self.header.width);
1045        let stride = self.header.filter_stride();
1046        let Some(stream) = self.stream.as_mut() else {
1047            return Err(PixelsError::graph("png stream vanished after starting"));
1048        };
1049        let samples = stream.next_scanline(sample_bytes, stride)?;
1050        let Some(stream) = self.stream.as_deref() else {
1051            return Err(PixelsError::graph("png stream vanished after starting"));
1052        };
1053        self.expand_row(
1054            &samples,
1055            stream.palette.as_deref(),
1056            stream.transparency.as_ref(),
1057            out,
1058        )?;
1059        self.row += 1;
1060
1061        // The checksum and IEND follow the last scanline, so the stream is
1062        // only fully verified once it has been read to its end.
1063        if self.row == self.descriptor.height {
1064            if let Some(stream) = self.stream.as_mut() {
1065                stream.finalize()?;
1066            }
1067        }
1068        Ok(())
1069    }
1070}
1071
1072/// Whether `prefix` starts with the PNG signature.
1073///
1074/// Detection is by magic bytes only (SPEC §Formats).
1075#[must_use]
1076pub fn probe(prefix: &[u8]) -> bool {
1077    prefix.get(..8) == Some(&SIGNATURE[..])
1078}
1079
1080/// The PNG entry in a sniffing registry.
1081///
1082/// Format detection is by magic bytes only (SPEC §Formats); PNG's eight-byte
1083/// signature is deliberately designed to survive — and detect — the transfer
1084/// corruptions its §5.2 enumerates, so there is nothing else worth consulting.
1085#[derive(Debug, Clone, Copy, Default)]
1086pub struct PngCodec;
1087
1088impl Codec for PngCodec {
1089    fn format(&self) -> Format {
1090        Format::Png
1091    }
1092
1093    fn magic_len(&self) -> usize {
1094        SIGNATURE.len()
1095    }
1096
1097    fn probe(&self, prefix: &[u8]) -> bool {
1098        probe(prefix)
1099    }
1100}