Skip to main content

dinamika_cpu/pixmap/
encode.rs

1//! Encoding a [`Pixmap`] into PNG.
2//!
3//! [`Pixmap`] stores **premultiplied** alpha, while PNG expects straight
4//! (non-premultiplied) — so before writing the color components are divided
5//! back by alpha.
6
7use std::fs::File;
8use std::io::{self, BufWriter, Write};
9use std::path::Path;
10
11use crate::pixmap::Pixmap;
12
13impl Pixmap {
14    /// Encodes the image into PNG (8-bit, RGBA with straight alpha) to an
15    /// arbitrary stream.
16    pub fn encode_png<W: Write>(&self, writer: W) -> io::Result<()> {
17        let mut encoder = ::png::Encoder::new(writer, self.width(), self.height());
18        encoder.set_color(::png::ColorType::Rgba);
19        encoder.set_depth(::png::BitDepth::Eight);
20
21        let mut writer = encoder.write_header().map_err(encoding_err)?;
22        writer.write_image_data(&self.to_straight_rgba()).map_err(encoding_err)?;
23        Ok(())
24    }
25
26    /// Saves the image to a PNG file, creating parent directories if necessary.
27    pub fn save_png(&self, path: impl AsRef<Path>) -> io::Result<()> {
28        let path = path.as_ref();
29        if let Some(parent) = path.parent() {
30            if !parent.as_os_str().is_empty() {
31                std::fs::create_dir_all(parent)?;
32            }
33        }
34        let file = BufWriter::new(File::create(path)?);
35        self.encode_png(file)
36    }
37
38    /// Unfolds premultiplied alpha back into straight (RGBA, 4 bytes per
39    /// pixel) — the format that PNG expects.
40    fn to_straight_rgba(&self) -> Vec<u8> {
41        let data = self.data();
42        let mut out = Vec::with_capacity(data.len());
43        for px in data.chunks_exact(4) {
44            let a = px[3];
45            match a {
46                0 => out.extend_from_slice(&[0, 0, 0, 0]),
47                255 => out.extend_from_slice(px),
48                _ => {
49                    let unpremul = |c: u8| {
50                        // Round to nearest and clamp: at boundary values
51                        // c may end up slightly larger than a.
52                        (((c as u32 * 255) + a as u32 / 2) / a as u32).min(255) as u8
53                    };
54                    out.push(unpremul(px[0]));
55                    out.push(unpremul(px[1]));
56                    out.push(unpremul(px[2]));
57                    out.push(a);
58                }
59            }
60        }
61        out
62    }
63}
64
65/// Turns a PNG encoder error into an [`io::Error`].
66fn encoding_err(e: ::png::EncodingError) -> io::Error {
67    match e {
68        ::png::EncodingError::IoError(e) => e,
69        other => io::Error::other(other),
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use crate::color::Color;
76    use crate::pixmap::Pixmap;
77
78    /// The PNG bytes must start with the PNG signature.
79    #[test]
80    fn encodes_png_signature() {
81        let mut pm = Pixmap::new(4, 4).unwrap();
82        pm.fill(Color::from_rgba8(10, 20, 30, 255));
83        let mut buf = Vec::new();
84        pm.encode_png(&mut buf).unwrap();
85        assert_eq!(&buf[0..8], &[0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]);
86    }
87
88    /// A semi-transparent pixel is unfolded from premultiplied alpha into straight.
89    #[test]
90    fn unpremultiplies_alpha_roundtrip() {
91        let mut pm = Pixmap::new(1, 1).unwrap();
92        // Straight red with alpha 0.5 -> premultiplied (128, 0, 0, 128).
93        pm.fill(Color::from_rgba8(255, 0, 0, 128));
94        let straight = pm.to_straight_rgba();
95        assert_eq!(straight[3], 128); // alpha unchanged
96        assert!((straight[0] as i32 - 255).abs() <= 2, "r={}", straight[0]);
97        assert_eq!(straight[1], 0);
98        assert_eq!(straight[2], 0);
99    }
100}