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