Skip to main content

otf_pixels_codec_png/
encoder.rs

1//! The PNG encoder.
2//!
3//! # Memory
4//!
5//! Encoding is **internally buffered**: DEFLATE needs the whole scanline
6//! stream before it can emit its Huffman-coded blocks, so filtered rows
7//! accumulate until [`Encoder::finish`]. That is ADR-0005's stated allowance
8//! for codecs that cannot work incrementally, and it is symmetric with the
9//! decoder — see the module docs there for why an incremental inflate and
10//! deflate are deferred rather than absent.
11//!
12//! # What is written
13//!
14//! Non-interlaced PNG: `IHDR`, one `IDAT`, `IEND`, in the image's own colour
15//! type. Adam7 would cost bytes rather than save them, and every decoder must
16//! accept the plain form.
17//!
18//! Asked for a palette (sharp's `palette`), the encoder instead quantizes to
19//! at most 256 colours with the median cut GIF uses, and writes colour type
20//! 3: `PLTE`, a `tRNS` when any entry is translucent, and indices at the
21//! smallest bit depth that holds them, unfiltered, as libpng recommends for
22//! palette images. That is lossy unless the image has no more colours than
23//! the palette, and usually much smaller.
24
25use otf_pixels_core::{
26    EncodeOptions, Encoder, ImageDescriptor, PixelFormat, PixelsError, Result, Sink,
27};
28
29use crate::format::{ColorType, Filter, SIGNATURE, apply_filter, write_chunk};
30use otf_pixels_compress::{Level, build_palette, build_palette_rgba, quantize_with, zlib_compress};
31use otf_pixels_core::{ChannelLayout, SampleKind};
32
33/// Encodes a PNG stream.
34#[derive(Debug)]
35pub struct PngEncoder {
36    level: Level,
37    /// Palette output: the palette size and the dithering level.
38    palette: Option<(usize, f32)>,
39    /// Set by `write_header`; its presence means the header was written.
40    state: Option<State>,
41    /// The ICC profile to write as `iCCP`, if any.
42    icc: Option<Vec<u8>>,
43    /// The EXIF block to write as `eXIf`, if any.
44    exif: Option<Vec<u8>>,
45}
46
47/// Everything fixed once the descriptor is known.
48#[derive(Debug)]
49struct State {
50    descriptor: ImageDescriptor,
51    bit_depth: u8,
52    /// Bytes per pixel, rounded up — the filter's left-neighbour offset.
53    stride: usize,
54    /// Filtered scanlines, each prefixed by its filter byte.
55    filtered: Vec<u8>,
56    /// The previous *unfiltered* row, which filters predict from.
57    previous: Vec<u8>,
58    /// Reusable big-endian staging buffer; empty unless `bit_depth` is 16.
59    swapped: Vec<u8>,
60    /// For palette output, the image as 8-bit RGB or RGBA, gathered until
61    /// `finish` because the palette depends on every pixel.
62    true_colour: Option<Vec<u8>>,
63    rows_written: u32,
64}
65
66impl PngEncoder {
67    /// An encoder at the default compression level.
68    #[must_use]
69    pub const fn new() -> Self {
70        Self::with_level(Level::DEFAULT)
71    }
72
73    /// An encoder at an explicit DEFLATE level.
74    #[must_use]
75    pub const fn with_level(level: Level) -> Self {
76        Self {
77            level,
78            palette: None,
79            state: None,
80            icc: None,
81            exif: None,
82        }
83    }
84
85    /// This encoder writing a palette image of at most `colours` entries,
86    /// diffusing `dither` (0..=1) of the quantization error.
87    ///
88    /// # Errors
89    ///
90    /// Returns [`PixelsError::InvalidArgument`] for `colours` outside
91    /// 2..=256 or `dither` outside 0..=1.
92    pub fn with_palette(mut self, colours: usize, dither: f32) -> Result<Self> {
93        if !(2..=256).contains(&colours) {
94            return Err(PixelsError::invalid_argument(
95                "colours",
96                format!("palette size must be in 2..=256, got {colours}"),
97            ));
98        }
99        if !(0.0..=1.0).contains(&dither) {
100            return Err(PixelsError::invalid_argument(
101                "dither",
102                format!("must be in 0..=1, got {dither}"),
103            ));
104        }
105        self.palette = Some((colours, dither));
106        Ok(self)
107    }
108
109    /// An encoder configured from generic encode options.
110    ///
111    /// [`EncodeOptions::compression_level`] is the DEFLATE level. Without
112    /// one, [`EncodeOptions::quality`], which cannot trade fidelity for size
113    /// in a lossless format, is read as compression *effort*, mapping 1..=100
114    /// onto levels 1..=9.
115    ///
116    /// [`EncodeOptions::wants_palette`] selects palette output, of
117    /// [`EncodeOptions::colours`] entries (256 by default) dithered by
118    /// [`EncodeOptions::dither`] (1 by default).
119    #[must_use]
120    pub fn from_options(options: &EncodeOptions) -> Self {
121        let level = options.compression_level.unwrap_or_else(|| {
122            let quality = u32::from(options.quality.clamp(1, 100));
123            // 1..=100 onto 1..=9, so the default quality of 80 lands on
124            // level 7, just above zlib's own default of 6.
125            ((quality - 1) * 8 / 99 + 1) as u8
126        });
127        let mut encoder = Self::with_level(Level::new(level).unwrap_or(Level::DEFAULT));
128        if options.wants_palette() {
129            let colours = options.colours.map_or(256, usize::from).clamp(2, 256);
130            let dither = options.dither.unwrap_or(1.0).clamp(0.0, 1.0);
131            encoder.palette = Some((colours, dither));
132        }
133        encoder
134    }
135
136    /// The colour type and bit depth for a pixel format.
137    fn png_type_of(format: PixelFormat) -> Result<(ColorType, u8)> {
138        match format {
139            PixelFormat::Gray8 => Ok((ColorType::Grayscale, 8)),
140            PixelFormat::Gray16 => Ok((ColorType::Grayscale, 16)),
141            PixelFormat::GrayA8 => Ok((ColorType::GrayscaleAlpha, 8)),
142            PixelFormat::Rgb8 => Ok((ColorType::Rgb, 8)),
143            PixelFormat::Rgb16 => Ok((ColorType::Rgb, 16)),
144            PixelFormat::Rgba8 => Ok((ColorType::Rgba, 8)),
145            PixelFormat::Rgba16 => Ok((ColorType::Rgba, 16)),
146            // PNG has no float sample type at any bit depth (§11.2.2).
147            other => Err(PixelsError::unsupported(format!(
148                "PNG cannot represent {other}; convert to an integer format first"
149            ))),
150        }
151    }
152}
153
154impl Default for PngEncoder {
155    fn default() -> Self {
156        Self::new()
157    }
158}
159
160impl State {
161    /// Choose a filter for `row` and append it, filter byte first.
162    ///
163    /// Uses the minimum-sum-of-absolute-differences heuristic from the PNG
164    /// specification's §12.8: filtered bytes near zero compress best, and the
165    /// sum of their signed magnitudes estimates that without running DEFLATE
166    /// five times.
167    fn append_filtered(&mut self, row: &[u8]) {
168        let candidates: &[Filter] = &[
169            Filter::None,
170            Filter::Sub,
171            Filter::Up,
172            Filter::Average,
173            Filter::Paeth,
174        ];
175
176        let mut best = Filter::None;
177        let mut best_score = u64::MAX;
178        let mut scratch = Vec::with_capacity(row.len());
179        for &filter in candidates {
180            scratch.clear();
181            apply_filter(filter, row, &self.previous, self.stride, &mut scratch);
182            let score: u64 = scratch
183                .iter()
184                .map(|&byte| u64::from((byte as i8).unsigned_abs()))
185                .sum();
186            if score < best_score {
187                best_score = score;
188                best = filter;
189            }
190        }
191
192        self.filtered.push(best.to_byte());
193        apply_filter(best, row, &self.previous, self.stride, &mut self.filtered);
194        self.previous.clear();
195        self.previous.extend_from_slice(row);
196    }
197}
198
199/// Rewrite native-endian 16-bit samples as the big-endian ones PNG stores.
200///
201/// On a big-endian host this is a copy; the swap is expressed in terms of
202/// `to_be_bytes` rather than a `cfg`, so there is one code path to be wrong.
203fn to_big_endian_16(row: &[u8], out: &mut Vec<u8>) {
204    out.clear();
205    for pair in row.chunks_exact(2) {
206        let value = u16::from_ne_bytes([
207            pair.first().copied().unwrap_or(0),
208            pair.get(1).copied().unwrap_or(0),
209        ]);
210        out.extend_from_slice(&value.to_be_bytes());
211    }
212}
213
214/// Append one row as 8-bit RGB, or RGBA if `format` has alpha.
215fn append_8_bit(row: &[u8], format: PixelFormat, out: &mut Vec<u8>) {
216    let wide = format.sample_kind() == SampleKind::U16;
217    let sample = |pixel: &[u8], index: usize| -> u8 {
218        if wide {
219            let at = index * 2;
220            let v = u16::from_ne_bytes([
221                pixel.get(at).copied().unwrap_or(0),
222                pixel.get(at + 1).copied().unwrap_or(0),
223            ]);
224            // Rounded to nearest, as every narrowing to 8 bits is.
225            ((u32::from(v) * 255 + 32_767) / 65_535) as u8
226        } else {
227            pixel.get(index).copied().unwrap_or(0)
228        }
229    };
230    for pixel in row.chunks_exact(format.bytes_per_pixel()) {
231        match format.layout() {
232            ChannelLayout::Gray => {
233                let g = sample(pixel, 0);
234                out.extend_from_slice(&[g, g, g]);
235            }
236            ChannelLayout::GrayAlpha => {
237                let g = sample(pixel, 0);
238                out.extend_from_slice(&[g, g, g, sample(pixel, 1)]);
239            }
240            ChannelLayout::Rgb => out.extend((0..3).map(|i| sample(pixel, i))),
241            ChannelLayout::Rgba => out.extend((0..4).map(|i| sample(pixel, i))),
242        }
243    }
244}
245
246/// A whole palette PNG for `pixels`, 8-bit RGB or (when `descriptor` has
247/// alpha) RGBA.
248fn encode_palette(
249    descriptor: &ImageDescriptor,
250    pixels: &[u8],
251    colours: usize,
252    dither: f32,
253    level: Level,
254    icc: Option<&[u8]>,
255    exif: Option<&[u8]>,
256) -> Result<Vec<u8>> {
257    let width = descriptor.width as usize;
258    let (entries, indices): (Vec<[u8; 4]>, Vec<u8>) = if descriptor.pixel.layout().has_alpha() {
259        let palette = build_palette_rgba(pixels, colours);
260        let indices = quantize_with(pixels, width, &palette, dither);
261        (palette.entries().to_vec(), indices)
262    } else {
263        let palette = build_palette(pixels, colours);
264        let indices = quantize_with(pixels, width, &palette, dither);
265        let entries = palette
266            .entries()
267            .iter()
268            .map(|&[r, g, b]| [r, g, b, 255])
269            .collect();
270        (entries, indices)
271    };
272
273    // The smallest depth whose indices reach every entry.
274    let bit_depth: u8 = match entries.len() {
275        0..=2 => 1,
276        3..=4 => 2,
277        5..=16 => 4,
278        _ => 8,
279    };
280    let per_byte = 8 / usize::from(bit_depth);
281    let row_bytes = width.div_ceil(per_byte);
282    let mut scanlines = Vec::with_capacity((row_bytes + 1) * descriptor.height as usize);
283    for row in indices.chunks_exact(width) {
284        scanlines.push(Filter::None.to_byte());
285        for group in row.chunks(per_byte) {
286            let mut byte = 0_u8;
287            for (i, &index) in group.iter().enumerate() {
288                // Leftmost pixel in the highest bits (§7.2).
289                byte |= index << (8 - usize::from(bit_depth) * (i + 1));
290            }
291            scanlines.push(byte);
292        }
293    }
294
295    let mut out = SIGNATURE.to_vec();
296    let mut ihdr = Vec::with_capacity(13);
297    ihdr.extend_from_slice(&descriptor.width.to_be_bytes());
298    ihdr.extend_from_slice(&descriptor.height.to_be_bytes());
299    ihdr.extend_from_slice(&[bit_depth, ColorType::Palette.to_byte(), 0, 0, 0]);
300    write_chunk(&mut out, b"IHDR", &ihdr);
301    if let Some(profile) = icc {
302        let mut iccp = b"ICC Profile\0\0".to_vec();
303        iccp.extend(zlib_compress(profile, level).map_err(crate::compress_error)?);
304        write_chunk(&mut out, b"iCCP", &iccp);
305    }
306    if let Some(exif) = exif {
307        write_chunk(&mut out, b"eXIf", exif);
308    }
309    let plte: Vec<u8> = entries.iter().flat_map(|e| [e[0], e[1], e[2]]).collect();
310    write_chunk(&mut out, b"PLTE", &plte);
311    // tRNS lists alphas up to the last translucent entry; the rest are
312    // opaque by default (§11.3.2.1).
313    if let Some(last) = entries.iter().rposition(|e| e[3] != 255) {
314        let alphas: Vec<u8> = entries.iter().take(last + 1).map(|e| e[3]).collect();
315        write_chunk(&mut out, b"tRNS", &alphas);
316    }
317    let compressed = zlib_compress(&scanlines, level).map_err(crate::compress_error)?;
318    write_chunk(&mut out, b"IDAT", &compressed);
319    write_chunk(&mut out, b"IEND", &[]);
320    Ok(out)
321}
322
323impl Encoder for PngEncoder {
324    fn set_icc_profile(&mut self, profile: Option<&[u8]>) -> Result<()> {
325        if self.state.is_some() {
326            return Err(PixelsError::invalid_argument(
327                "profile",
328                "the ICC profile must be set before write_header",
329            ));
330        }
331        self.icc = profile.map(<[u8]>::to_vec);
332        Ok(())
333    }
334
335    fn set_exif(&mut self, exif: Option<&[u8]>) -> Result<()> {
336        if self.state.is_some() {
337            return Err(PixelsError::invalid_argument(
338                "exif",
339                "the EXIF block must be set before write_header",
340            ));
341        }
342        self.exif = exif.map(<[u8]>::to_vec);
343        Ok(())
344    }
345
346    fn write_header(&mut self, desc: &ImageDescriptor, sink: &mut dyn Sink) -> Result<()> {
347        if self.state.is_some() {
348            return Err(PixelsError::invalid_argument(
349                "descriptor",
350                "write_header called more than once",
351            ));
352        }
353        if desc.width == 0 || desc.height == 0 {
354            return Err(PixelsError::invalid_argument(
355                "descriptor",
356                format!(
357                    "PNG dimensions must be non-zero, got {}x{}",
358                    desc.width, desc.height
359                ),
360            ));
361        }
362        let (color_type, bit_depth) = Self::png_type_of(desc.pixel)?;
363
364        if self.palette.is_some() {
365            // Nothing can be written yet: the header's bit depth depends on
366            // how many colours the palette ends up with.
367            let channels = if desc.pixel.layout().has_alpha() {
368                4
369            } else {
370                3
371            };
372            self.state = Some(State {
373                descriptor: *desc,
374                bit_depth: 8,
375                stride: 1,
376                filtered: Vec::new(),
377                previous: Vec::new(),
378                swapped: Vec::new(),
379                true_colour: Some(Vec::with_capacity(
380                    desc.width as usize * desc.height as usize * channels,
381                )),
382                rows_written: 0,
383            });
384            return Ok(());
385        }
386
387        sink.write_all(&SIGNATURE)?;
388        let mut ihdr = Vec::with_capacity(13);
389        ihdr.extend_from_slice(&desc.width.to_be_bytes());
390        ihdr.extend_from_slice(&desc.height.to_be_bytes());
391        ihdr.push(bit_depth);
392        ihdr.push(color_type.to_byte());
393        // Compression 0 (DEFLATE), filter 0 (adaptive), interlace 0 (none) —
394        // the only values the specification defines.
395        ihdr.extend_from_slice(&[0, 0, 0]);
396        let mut chunk = Vec::new();
397        write_chunk(&mut chunk, b"IHDR", &ihdr);
398        if let Some(profile) = &self.icc {
399            // iCCP (§11.3.3.3): a name, NUL, method 0, the zlib stream.
400            let mut iccp = b"ICC Profile\0\0".to_vec();
401            iccp.extend(zlib_compress(profile, self.level).map_err(crate::compress_error)?);
402            write_chunk(&mut chunk, b"iCCP", &iccp);
403        }
404        if let Some(exif) = &self.exif {
405            // eXIf goes before the image data (§11.3.6.1).
406            write_chunk(&mut chunk, b"eXIf", exif);
407        }
408        sink.write_all(&chunk)?;
409
410        let row_bytes = desc.row_bytes();
411        let stride = (color_type.channels() * bit_depth as usize).div_ceil(8);
412        self.state = Some(State {
413            descriptor: *desc,
414            bit_depth,
415            stride,
416            // Each row costs its bytes plus one filter byte.
417            filtered: Vec::with_capacity((row_bytes + 1) * desc.height as usize),
418            previous: vec![0_u8; row_bytes],
419            swapped: Vec::new(),
420            true_colour: None,
421            rows_written: 0,
422        });
423        Ok(())
424    }
425
426    fn write_row(&mut self, row: &[u8], _sink: &mut dyn Sink) -> Result<()> {
427        let Some(state) = self.state.as_mut() else {
428            return Err(PixelsError::invalid_argument(
429                "row",
430                "write_row called before write_header",
431            ));
432        };
433        let expected = state.descriptor.row_bytes();
434        if row.len() != expected {
435            return Err(PixelsError::invalid_argument(
436                "row",
437                format!("row is {} bytes, expected {expected}", row.len()),
438            ));
439        }
440        if state.rows_written >= state.descriptor.height {
441            return Err(PixelsError::invalid_argument(
442                "row",
443                format!("more than {} rows written", state.descriptor.height),
444            ));
445        }
446
447        if let Some(pixels) = state.true_colour.as_mut() {
448            append_8_bit(row, state.descriptor.pixel, pixels);
449        } else if state.bit_depth == 16 {
450            let mut swapped = std::mem::take(&mut state.swapped);
451            to_big_endian_16(row, &mut swapped);
452            state.append_filtered(&swapped);
453            state.swapped = swapped;
454        } else {
455            state.append_filtered(row);
456        }
457        state.rows_written += 1;
458        Ok(())
459    }
460
461    fn finish(&mut self, sink: &mut dyn Sink) -> Result<()> {
462        let Some(state) = self.state.as_mut() else {
463            return Err(PixelsError::malformed(
464                "png",
465                "finish called before write_header",
466            ));
467        };
468        if state.rows_written != state.descriptor.height {
469            return Err(PixelsError::malformed(
470                "png",
471                format!(
472                    "{} of {} rows written; a partial image is never emitted",
473                    state.rows_written, state.descriptor.height
474                ),
475            ));
476        }
477
478        if let (Some(pixels), Some((colours, dither))) = (state.true_colour.take(), self.palette) {
479            let file = encode_palette(
480                &state.descriptor,
481                &pixels,
482                colours,
483                dither,
484                self.level,
485                self.icc.as_deref(),
486                self.exif.as_deref(),
487            )?;
488            sink.write_all(&file)?;
489            return sink.flush();
490        }
491
492        let compressed =
493            zlib_compress(&state.filtered, self.level).map_err(crate::compress_error)?;
494        let mut chunk = Vec::with_capacity(compressed.len() + 12);
495        write_chunk(&mut chunk, b"IDAT", &compressed);
496        write_chunk(&mut chunk, b"IEND", &[]);
497        sink.write_all(&chunk)?;
498        sink.flush()?;
499
500        // Release the raster now rather than at drop; a caller that keeps the
501        // encoder around to inspect it should not keep the image too.
502        state.filtered = Vec::new();
503        state.previous = Vec::new();
504        state.swapped = Vec::new();
505        Ok(())
506    }
507}
508
509#[cfg(test)]
510#[allow(
511    clippy::unwrap_used,
512    clippy::expect_used,
513    clippy::indexing_slicing,
514    clippy::panic,
515    reason = "tests operate on known-good values and assert shapes directly"
516)]
517mod tests {
518    use super::*;
519    use crate::decoder::PngDecoder;
520    use otf_pixels_core::{Decoder, Limits};
521
522    /// Encode a full raster, returning the PNG bytes.
523    fn encode(desc: &ImageDescriptor, raster: &[u8], level: Level) -> Result<Vec<u8>> {
524        let mut encoder = PngEncoder::with_level(level);
525        let mut out: Vec<u8> = Vec::new();
526        encoder.write_header(desc, &mut out)?;
527        for row in raster.chunks_exact(desc.row_bytes()) {
528            encoder.write_row(row, &mut out)?;
529        }
530        encoder.finish(&mut out)?;
531        Ok(out)
532    }
533
534    /// Decode a full raster back.
535    fn decode(bytes: &[u8]) -> Result<(ImageDescriptor, Vec<u8>)> {
536        let mut decoder = PngDecoder::new(bytes, Limits::default())?;
537        let mut row = vec![0_u8; decoder.descriptor().row_bytes()];
538        let mut raster = Vec::new();
539        for _ in 0..decoder.descriptor().height {
540            decoder.read_row(&mut row)?;
541            raster.extend_from_slice(&row);
542        }
543        Ok((decoder.descriptor(), raster))
544    }
545
546    /// A deterministic non-trivial raster: gradients plus a hard edge, so the
547    /// filter heuristic has something to choose between.
548    fn sample(desc: &ImageDescriptor) -> Vec<u8> {
549        let mut raster = vec![0_u8; desc.byte_len().unwrap()];
550        for (index, byte) in raster.iter_mut().enumerate() {
551            let value = if index % 97 < 40 {
552                (index % 251) as u8
553            } else {
554                ((index * 7) % 13) as u8
555            };
556            *byte = value;
557        }
558        raster
559    }
560
561    #[test]
562    fn every_supported_format_round_trips() {
563        for format in [
564            PixelFormat::Gray8,
565            PixelFormat::Gray16,
566            PixelFormat::GrayA8,
567            PixelFormat::Rgb8,
568            PixelFormat::Rgb16,
569            PixelFormat::Rgba8,
570            PixelFormat::Rgba16,
571        ] {
572            let desc = ImageDescriptor::new(23, 17, format).unwrap();
573            let raster = sample(&desc);
574            let bytes = encode(&desc, &raster, Level::DEFAULT)
575                .unwrap_or_else(|e| panic!("encoding {format}: {e}"));
576            let (decoded_desc, decoded) =
577                decode(&bytes).unwrap_or_else(|e| panic!("decoding {format}: {e}"));
578            assert_eq!(decoded_desc.pixel, format, "{format} changed format");
579            assert_eq!(
580                (decoded_desc.width, decoded_desc.height),
581                (23, 17),
582                "{format}"
583            );
584            assert_eq!(decoded, raster, "{format} did not round-trip");
585        }
586    }
587
588    #[test]
589    fn every_level_round_trips_to_the_same_pixels() {
590        let desc = ImageDescriptor::new(19, 11, PixelFormat::Rgba8).unwrap();
591        let raster = sample(&desc);
592        for level in 0..=9 {
593            let level = Level::new(level).unwrap();
594            let bytes = encode(&desc, &raster, level).unwrap();
595            let (_, decoded) = decode(&bytes).unwrap();
596            assert_eq!(decoded, raster, "level {} did not round-trip", level.get());
597        }
598    }
599
600    #[test]
601    fn the_output_starts_with_the_signature_and_ihdr() {
602        let desc = ImageDescriptor::new(4, 4, PixelFormat::Rgb8).unwrap();
603        let bytes = encode(&desc, &sample(&desc), Level::FAST).unwrap();
604        assert_eq!(&bytes[..8], &SIGNATURE, "signature");
605        assert_eq!(&bytes[12..16], b"IHDR", "first chunk");
606        assert_eq!(
607            &bytes[bytes.len() - 8..bytes.len() - 4],
608            b"IEND",
609            "last chunk"
610        );
611    }
612
613    #[test]
614    fn sixteen_bit_samples_are_written_big_endian() {
615        // The one place a host-endianness bug would hide: encode a known
616        // sample and read the two bytes straight out of the decompressed
617        // scanline via a decode, which is byte-order-symmetric, plus assert
618        // the swap helper directly.
619        let mut out = Vec::new();
620        to_big_endian_16(&0x1234_u16.to_ne_bytes(), &mut out);
621        assert_eq!(out, vec![0x12, 0x34]);
622    }
623
624    #[test]
625    fn compression_shrinks_a_compressible_image() {
626        // A flat image is the clearest case: level 0 stores it, level 9 must
627        // do dramatically better, which proves the level reaches DEFLATE.
628        let desc = ImageDescriptor::new(64, 64, PixelFormat::Rgb8).unwrap();
629        let raster = vec![7_u8; desc.byte_len().unwrap()];
630        let stored = encode(&desc, &raster, Level::NONE).unwrap();
631        let packed = encode(&desc, &raster, Level::BEST).unwrap();
632        assert!(
633            packed.len() * 10 < stored.len(),
634            "level 9 produced {} bytes against level 0's {}",
635            packed.len(),
636            stored.len()
637        );
638        assert_eq!(decode(&packed).unwrap().1, raster);
639    }
640
641    #[test]
642    fn a_float_format_is_unsupported_not_a_panic() {
643        let desc = ImageDescriptor::new(2, 2, PixelFormat::RgbaF32).unwrap();
644        let mut encoder = PngEncoder::new();
645        let mut out = Vec::new();
646        let error = encoder.write_header(&desc, &mut out).unwrap_err();
647        assert!(matches!(error, PixelsError::Unsupported { .. }), "{error}");
648        assert!(
649            out.is_empty(),
650            "nothing should be written for a rejected format"
651        );
652    }
653
654    #[test]
655    fn a_short_image_is_an_error_not_a_truncated_png() {
656        let desc = ImageDescriptor::new(4, 4, PixelFormat::Gray8).unwrap();
657        let mut encoder = PngEncoder::new();
658        let mut out = Vec::new();
659        encoder.write_header(&desc, &mut out).unwrap();
660        encoder.write_row(&[0; 4], &mut out).unwrap();
661        let error = encoder.finish(&mut out).unwrap_err();
662        assert!(matches!(error, PixelsError::Malformed { .. }), "{error}");
663    }
664
665    #[test]
666    fn extra_rows_and_wrong_row_lengths_are_errors() {
667        let desc = ImageDescriptor::new(4, 1, PixelFormat::Gray8).unwrap();
668        let mut encoder = PngEncoder::new();
669        let mut out = Vec::new();
670        encoder.write_header(&desc, &mut out).unwrap();
671        assert!(encoder.write_row(&[0; 3], &mut out).is_err(), "short row");
672        assert!(encoder.write_row(&[0; 5], &mut out).is_err(), "long row");
673        encoder.write_row(&[0; 4], &mut out).unwrap();
674        assert!(encoder.write_row(&[0; 4], &mut out).is_err(), "extra row");
675    }
676
677    #[test]
678    fn rows_before_the_header_are_an_error() {
679        let mut encoder = PngEncoder::new();
680        let mut out = Vec::new();
681        assert!(encoder.write_row(&[0; 4], &mut out).is_err());
682        assert!(encoder.finish(&mut out).is_err());
683    }
684
685    #[test]
686    fn a_second_header_is_an_error() {
687        let desc = ImageDescriptor::new(2, 2, PixelFormat::Gray8).unwrap();
688        let mut encoder = PngEncoder::new();
689        let mut out = Vec::new();
690        encoder.write_header(&desc, &mut out).unwrap();
691        assert!(encoder.write_header(&desc, &mut out).is_err());
692    }
693
694    #[test]
695    fn zero_dimensions_never_reach_the_encoder() {
696        // `ImageDescriptor` refuses to hold a zero dimension, so the guard in
697        // `write_header` is defence in depth rather than the only check. Both
698        // matter: PNG's IHDR has no representation for an empty image, and a
699        // future descriptor change must not quietly start emitting one.
700        assert!(ImageDescriptor::new(0, 4, PixelFormat::Gray8).is_err());
701        assert!(ImageDescriptor::new(4, 0, PixelFormat::Gray8).is_err());
702    }
703
704    #[test]
705    fn options_map_quality_onto_compression_effort() {
706        assert_eq!(
707            PngEncoder::from_options(&EncodeOptions::default())
708                .level
709                .get(),
710            7
711        );
712        let lowest = EncodeOptions::with_quality(1).unwrap();
713        assert_eq!(PngEncoder::from_options(&lowest).level.get(), 1);
714        let highest = EncodeOptions::with_quality(100).unwrap();
715        assert_eq!(PngEncoder::from_options(&highest).level.get(), 9);
716    }
717
718    #[test]
719    fn a_single_pixel_image_round_trips() {
720        // The degenerate case where every filter's neighbours are all zero.
721        let desc = ImageDescriptor::new(1, 1, PixelFormat::Rgba8).unwrap();
722        let raster = vec![1, 2, 3, 4];
723        let bytes = encode(&desc, &raster, Level::BEST).unwrap();
724        assert_eq!(decode(&bytes).unwrap().1, raster);
725    }
726
727    /// Encode with `encoder`, returning the PNG bytes.
728    fn encode_with(desc: &ImageDescriptor, raster: &[u8], mut encoder: PngEncoder) -> Vec<u8> {
729        let mut out: Vec<u8> = Vec::new();
730        encoder.write_header(desc, &mut out).unwrap();
731        for row in raster.chunks_exact(desc.row_bytes()) {
732            encoder.write_row(row, &mut out).unwrap();
733        }
734        encoder.finish(&mut out).unwrap();
735        out
736    }
737
738    fn palette(colours: usize, dither: f32) -> PngEncoder {
739        PngEncoder::new().with_palette(colours, dither).unwrap()
740    }
741
742    /// The IHDR bit depth and colour type.
743    fn header(bytes: &[u8]) -> (u8, u8) {
744        (bytes[24], bytes[25])
745    }
746
747    fn has_chunk(bytes: &[u8], kind: &[u8; 4]) -> bool {
748        bytes.windows(4).any(|w| w == kind)
749    }
750
751    #[test]
752    fn a_palette_image_with_few_colours_round_trips_exactly() {
753        let desc = ImageDescriptor::new(13, 7, PixelFormat::Rgba8).unwrap();
754        let colours = [
755            [255, 0, 0, 255],
756            [0, 255, 0, 128],
757            [0, 0, 255, 0],
758            [9, 9, 9, 255],
759        ];
760        let raster: Vec<u8> = (0..13 * 7).flat_map(|i| colours[i % 4]).collect();
761        let bytes = encode_with(&desc, &raster, palette(256, 1.0));
762        assert_eq!(header(&bytes), (2, 3), "four colours fit two bits");
763        assert!(has_chunk(&bytes, b"PLTE") && has_chunk(&bytes, b"tRNS"));
764        let (out, decoded) = decode(&bytes).unwrap();
765        assert_eq!(out.pixel, PixelFormat::Rgba8);
766        // Every invisible pixel is the same entry, whatever colour it hid.
767        let visible: Vec<u8> = raster
768            .chunks_exact(4)
769            .flat_map(|p| {
770                if p[3] == 0 {
771                    [0; 4]
772                } else {
773                    [p[0], p[1], p[2], p[3]]
774                }
775            })
776            .collect();
777        assert_eq!(decoded, visible);
778    }
779
780    #[test]
781    fn the_bit_depth_is_the_smallest_that_holds_the_palette() {
782        for (count, depth) in [
783            (2_usize, 1_u8),
784            (3, 2),
785            (4, 2),
786            (5, 4),
787            (16, 4),
788            (17, 8),
789            (200, 8),
790        ] {
791            // Widths that leave a partial last byte at every depth.
792            let desc = ImageDescriptor::new(11, 3, PixelFormat::Rgb8).unwrap();
793            let raster: Vec<u8> = (0..33)
794                .flat_map(|i| {
795                    let v = (i % count) as u8;
796                    [v, v.wrapping_mul(3), 255 - v]
797                })
798                .collect();
799            let bytes = encode_with(&desc, &raster, palette(256, 1.0));
800            assert_eq!(header(&bytes), (depth, 3), "{count} colours");
801            assert!(!has_chunk(&bytes, b"tRNS"), "an opaque image needs no tRNS");
802            let (out, decoded) = decode(&bytes).unwrap();
803            assert_eq!(out.pixel, PixelFormat::Rgb8);
804            assert_eq!(decoded, raster, "{count} colours");
805        }
806    }
807
808    #[test]
809    fn a_photograph_is_reduced_to_the_palette_size() {
810        let desc = ImageDescriptor::new(64, 64, PixelFormat::Rgb8).unwrap();
811        let raster: Vec<u8> = (0..64 * 64 * 3)
812            .map(|i| ((i * 37 + i / 191) % 256) as u8)
813            .collect();
814        for dither in [0.0, 0.5, 1.0] {
815            let bytes = encode_with(&desc, &raster, palette(16, dither));
816            let plte = bytes.windows(4).position(|w| w == b"PLTE").unwrap();
817            let length = u32::from_be_bytes(bytes[plte - 4..plte].try_into().unwrap());
818            assert!(length <= 16 * 3, "{length} bytes of palette");
819            let (_, decoded) = decode(&bytes).unwrap();
820            let mut distinct: Vec<&[u8]> = decoded.chunks_exact(3).collect();
821            distinct.sort_unstable();
822            distinct.dedup();
823            assert!(distinct.len() <= 16);
824        }
825        // And on photographic noise, which true colour cannot compress, a
826        // palette is much smaller.
827        let mut seed = 12_345_u32;
828        let noisy: Vec<u8> = (0..64 * 64 * 3)
829            .map(|_| {
830                seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
831                (seed >> 16) as u8
832            })
833            .collect();
834        let true_colour = encode(&desc, &noisy, Level::DEFAULT).unwrap();
835        let reduced = encode_with(&desc, &noisy, palette(16, 1.0));
836        assert!(
837            reduced.len() * 2 < true_colour.len(),
838            "{} against {}",
839            reduced.len(),
840            true_colour.len()
841        );
842    }
843
844    #[test]
845    fn grey_and_wide_images_take_a_palette_too() {
846        let desc = ImageDescriptor::new(4, 1, PixelFormat::Gray16).unwrap();
847        let raster: Vec<u8> = [0_u16, 65535, 0, 65535]
848            .iter()
849            .flat_map(|v| v.to_ne_bytes())
850            .collect();
851        let bytes = encode_with(&desc, &raster, palette(256, 1.0));
852        assert_eq!(header(&bytes), (1, 3));
853        assert_eq!(
854            decode(&bytes).unwrap().1,
855            [0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255]
856        );
857    }
858
859    #[test]
860    fn options_choose_the_level_and_the_palette() {
861        let desc = ImageDescriptor::new(32, 32, PixelFormat::Rgb8).unwrap();
862        let raster: Vec<u8> = (0..32 * 32 * 3).map(|i| (i / 7 % 256) as u8).collect();
863        let with = |options: EncodeOptions| {
864            encode_with(&desc, &raster, PngEncoder::from_options(&options))
865        };
866        let stored = with(EncodeOptions::default().with_compression_level(0).unwrap());
867        let best = with(EncodeOptions::default().with_compression_level(9).unwrap());
868        assert!(best.len() < stored.len());
869        // Unset, the level still comes from quality, as it always did.
870        assert_eq!(
871            with(EncodeOptions::default()),
872            encode(&desc, &raster, Level::new(7).unwrap()).unwrap()
873        );
874        for options in [
875            EncodeOptions::default().with_palette(true),
876            EncodeOptions::default().with_colours(64).unwrap(),
877            EncodeOptions::default().with_dither(0.0).unwrap(),
878        ] {
879            assert_eq!(header(&with(options)).1, 3, "{options:?}");
880        }
881        assert_eq!(header(&with(EncodeOptions::default())).1, 2);
882    }
883
884    #[test]
885    fn palette_parameters_are_validated() {
886        assert!(PngEncoder::new().with_palette(1, 1.0).is_err());
887        assert!(PngEncoder::new().with_palette(257, 1.0).is_err());
888        assert!(PngEncoder::new().with_palette(2, 1.5).is_err());
889    }
890}