Skip to main content

slimg_core/codec/
qoi.rs

1use rapid_qoi::{Colors, Qoi};
2
3use crate::error::{Error, Result};
4use crate::format::Format;
5
6use super::{Codec, EncodeOptions, ImageData};
7
8/// QOI codec backed by rapid-qoi. Lossless format — quality is ignored.
9pub struct QoiCodec;
10
11impl Codec for QoiCodec {
12    fn format(&self) -> Format {
13        Format::Qoi
14    }
15
16    fn decode(&self, data: &[u8]) -> Result<ImageData> {
17        let (header, pixels) =
18            Qoi::decode_alloc(data).map_err(|e| Error::Decode(format!("qoi decode: {e}")))?;
19
20        let width = header.width;
21        let height = header.height;
22
23        // Convert to RGBA if the decoded data is RGB (3 channels).
24        let rgba = if header.colors.has_alpha() {
25            pixels
26        } else {
27            pixels
28                .chunks_exact(3)
29                .flat_map(|px| [px[0], px[1], px[2], 255])
30                .collect()
31        };
32
33        Ok(ImageData::new(width, height, rgba))
34    }
35
36    fn encode(&self, image: &ImageData, _options: &EncodeOptions) -> Result<Vec<u8>> {
37        let qoi = Qoi {
38            width: image.width,
39            height: image.height,
40            colors: Colors::SrgbLinA,
41        };
42
43        let encoded = qoi
44            .encode_alloc(&image.data)
45            .map_err(|e| Error::Encode(format!("qoi encode: {e}")))?;
46
47        Ok(encoded)
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    fn create_test_image(width: u32, height: u32) -> ImageData {
56        let size = (width * height * 4) as usize;
57        let mut data = vec![0u8; size];
58        for y in 0..height {
59            for x in 0..width {
60                let i = ((y * width + x) * 4) as usize;
61                data[i] = (x * 255 / width) as u8; // R
62                data[i + 1] = (y * 255 / height) as u8; // G
63                data[i + 2] = 128; // B
64                data[i + 3] = 255; // A
65            }
66        }
67        ImageData::new(width, height, data)
68    }
69
70    #[test]
71    fn encode_and_decode_roundtrip() {
72        let codec = QoiCodec;
73        let original = create_test_image(64, 48);
74        let options = EncodeOptions { quality: 90 };
75
76        let encoded = codec.encode(&original, &options).expect("encode failed");
77
78        // Verify QOI magic bytes "qoif"
79        assert!(
80            encoded.len() >= 4,
81            "encoded data too short: {} bytes",
82            encoded.len()
83        );
84        assert_eq!(&encoded[..4], b"qoif", "missing QOI magic bytes");
85
86        // Decode back and verify lossless roundtrip
87        let decoded = codec.decode(&encoded).expect("decode failed");
88        assert_eq!(decoded.width, original.width);
89        assert_eq!(decoded.height, original.height);
90        assert_eq!(decoded.data, original.data, "QOI should be lossless");
91    }
92}