Skip to main content

dinamika_cpu/pixmap/
decode.rs

1//! Decoding PNG into a [`Pixmap`].
2//!
3//! PNG stores **straight** alpha, while [`Pixmap`] stores **premultiplied**
4//! alpha, so on reading the color components are multiplied by alpha. Palette,
5//! grayscale and 16-bit channels are reduced to 8-bit RGBA.
6
7use std::fs::File;
8use std::io::{self, BufReader, Read};
9use std::path::Path;
10
11use ::png::ColorType;
12
13use crate::color::ColorU8;
14use crate::pixmap::Pixmap;
15
16impl Pixmap {
17    /// Decodes PNG from an arbitrary stream into a [`Pixmap`] (premultiplied
18    /// RGBA). Palette/grayscale/16-bit are reduced to 8-bit RGBA.
19    pub fn decode_png<R: Read>(reader: R) -> io::Result<Pixmap> {
20        let mut decoder = ::png::Decoder::new(reader);
21        // EXPAND: palette → RGB, gray <8 bit → 8 bit, tRNS → alpha channel.
22        // STRIP_16: 16-bit channels → 8 bit.
23        decoder.set_transformations(
24            ::png::Transformations::EXPAND | ::png::Transformations::STRIP_16,
25        );
26
27        let mut reader = decoder.read_info().map_err(decoding_err)?;
28        let mut buf = vec![0; reader.output_buffer_size()];
29        let info = reader.next_frame(&mut buf).map_err(decoding_err)?;
30
31        let mut pixmap = Pixmap::new(info.width, info.height)
32            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "zero-size PNG"))?;
33        let src = &buf[..info.buffer_size()];
34        let dst = pixmap.data_mut();
35        let n = info.width as usize * info.height as usize;
36
37        // Writes a straight pixel, multiplying by alpha (the pixmap storage format).
38        let put = |dst: &mut [u8], di: usize, r: u8, g: u8, b: u8, a: u8| {
39            let p = ColorU8::from_rgba(r, g, b, a).premultiply();
40            dst[di] = p.red();
41            dst[di + 1] = p.green();
42            dst[di + 2] = p.blue();
43            dst[di + 3] = p.alpha();
44        };
45
46        match info.color_type {
47            ColorType::Rgba => {
48                for i in 0..n {
49                    let s = i * 4;
50                    put(dst, i * 4, src[s], src[s + 1], src[s + 2], src[s + 3]);
51                }
52            }
53            ColorType::Rgb => {
54                for i in 0..n {
55                    let s = i * 3;
56                    put(dst, i * 4, src[s], src[s + 1], src[s + 2], 255);
57                }
58            }
59            ColorType::GrayscaleAlpha => {
60                for i in 0..n {
61                    let s = i * 2;
62                    let v = src[s];
63                    put(dst, i * 4, v, v, v, src[s + 1]);
64                }
65            }
66            ColorType::Grayscale => {
67                for (i, &v) in src.iter().take(n).enumerate() {
68                    put(dst, i * 4, v, v, v, 255);
69                }
70            }
71            // EXPAND unfolds the palette, so Indexed does not reach here.
72            ColorType::Indexed => {
73                return Err(io::Error::new(
74                    io::ErrorKind::InvalidData,
75                    "indexed PNG was not expanded",
76                ));
77            }
78        }
79
80        Ok(pixmap)
81    }
82
83    /// Loads a [`Pixmap`] from a PNG file.
84    pub fn from_png_file(path: impl AsRef<Path>) -> io::Result<Pixmap> {
85        Pixmap::decode_png(BufReader::new(File::open(path)?))
86    }
87}
88
89/// Turns a PNG decoder error into an [`io::Error`].
90fn decoding_err(e: ::png::DecodingError) -> io::Error {
91    match e {
92        ::png::DecodingError::IoError(e) => e,
93        other => io::Error::other(other),
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use crate::color::Color;
100    use crate::pixmap::Pixmap;
101
102    /// Encoding → decoding returns a close pixel.
103    #[test]
104    fn png_round_trip() {
105        let mut pm = Pixmap::new(3, 2).unwrap();
106        pm.fill(Color::from_rgba8(200, 100, 50, 255));
107        let mut buf = Vec::new();
108        pm.encode_png(&mut buf).unwrap();
109
110        let decoded = Pixmap::decode_png(&buf[..]).unwrap();
111        assert_eq!(decoded.width(), 3);
112        assert_eq!(decoded.height(), 2);
113        let p = decoded.pixel(1, 1).unwrap();
114        assert_eq!((p.red(), p.green(), p.blue(), p.alpha()), (200, 100, 50, 255));
115    }
116
117    /// A semi-transparent pixel survives a round-trip (accounting for rounding).
118    #[test]
119    fn png_round_trip_semitransparent() {
120        let mut pm = Pixmap::new(1, 1).unwrap();
121        pm.fill(Color::from_rgba8(255, 0, 0, 128));
122        let mut buf = Vec::new();
123        pm.encode_png(&mut buf).unwrap();
124
125        let decoded = Pixmap::decode_png(&buf[..]).unwrap();
126        let p = decoded.pixel(0, 0).unwrap();
127        assert_eq!(p.alpha(), 128);
128        // Premultiplied red ≈ 128.
129        assert!((p.red() as i32 - 128).abs() <= 2, "r={}", p.red());
130    }
131}