Skip to main content

slimg_core/codec/
png.rs

1use std::io::Cursor;
2
3use image::ImageEncoder;
4use image::codecs::png::PngEncoder;
5
6use crate::error::{Error, Result};
7use crate::format::Format;
8
9use super::{Codec, EncodeOptions, ImageData};
10
11/// PNG codec backed by OxiPNG for optimization.
12pub struct PngCodec;
13
14impl Codec for PngCodec {
15    fn format(&self) -> Format {
16        Format::Png
17    }
18
19    fn decode(&self, data: &[u8]) -> Result<ImageData> {
20        let img = image::load_from_memory_with_format(data, image::ImageFormat::Png)
21            .map_err(|e| Error::Decode(format!("png decode: {e}")))?;
22
23        let rgba = img.to_rgba8();
24        let width = rgba.width();
25        let height = rgba.height();
26
27        Ok(ImageData::new(width, height, rgba.into_raw()))
28    }
29
30    fn encode(&self, image: &ImageData, options: &EncodeOptions) -> Result<Vec<u8>> {
31        // First, encode as raw PNG using the image crate's PngEncoder.
32        let mut raw_png = Cursor::new(Vec::new());
33        PngEncoder::new(&mut raw_png)
34            .write_image(
35                &image.data,
36                image.width,
37                image.height,
38                image::ExtendedColorType::Rgba8,
39            )
40            .map_err(|e| Error::Encode(format!("png raw encode: {e}")))?;
41
42        let raw_bytes = raw_png.into_inner();
43
44        // Map quality to oxipng optimization preset.
45        let preset = match options.quality {
46            90..=100 => 1,
47            70..=89 => 2,
48            50..=69 => 3,
49            30..=49 => 4,
50            _ => 6,
51        };
52
53        let opts = oxipng::Options::from_preset(preset);
54        let optimized = oxipng::optimize_from_memory(&raw_bytes, &opts)
55            .map_err(|e| Error::Encode(format!("oxipng optimize: {e}")))?;
56
57        Ok(optimized)
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    fn create_test_image(width: u32, height: u32) -> ImageData {
66        let size = (width * height * 4) as usize;
67        let mut data = vec![0u8; size];
68        for y in 0..height {
69            for x in 0..width {
70                let i = ((y * width + x) * 4) as usize;
71                data[i] = (x * 255 / width) as u8; // R
72                data[i + 1] = (y * 255 / height) as u8; // G
73                data[i + 2] = 128; // B
74                data[i + 3] = 255; // A
75            }
76        }
77        ImageData::new(width, height, data)
78    }
79
80    #[test]
81    fn encode_and_decode_roundtrip() {
82        let codec = PngCodec;
83        let original = create_test_image(64, 48);
84        let options = EncodeOptions { quality: 90 };
85
86        let encoded = codec.encode(&original, &options).expect("encode failed");
87
88        // Verify PNG magic bytes
89        assert!(
90            encoded.len() >= 4,
91            "encoded data too short: {} bytes",
92            encoded.len()
93        );
94        assert_eq!(
95            &encoded[..4],
96            &[0x89, 0x50, 0x4E, 0x47],
97            "missing PNG magic bytes"
98        );
99
100        // Decode back and verify lossless roundtrip
101        let decoded = codec.decode(&encoded).expect("decode failed");
102        assert_eq!(decoded.width, original.width);
103        assert_eq!(decoded.height, original.height);
104        assert_eq!(decoded.data, original.data, "PNG should be lossless");
105    }
106
107    #[test]
108    fn decode_invalid_data_returns_error() {
109        let codec = PngCodec;
110        let result = codec.decode(b"not a png");
111        assert!(result.is_err(), "decoding invalid data should fail");
112    }
113}