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
11pub 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 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 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; data[i + 1] = (y * 255 / height) as u8; data[i + 2] = 128; data[i + 3] = 255; }
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 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 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}