Skip to main content

wzlib_rs/image/
encode.rs

1//! Pixel format encoding and PNG data compression — the reverse of the decode path.
2//!
3//! `encode_pixels` converts RGBA8888 → target WZ pixel format.
4//! `compress_png_data` zlib-compresses raw pixel data for storage in WZ Canvas nodes.
5
6use crate::wz::error::{WzError, WzResult};
7use crate::wz::types::WzPngFormat;
8
9// DXT/BC block-compressed formats are not supported for encoding —
10// use Bgra8888 as a lossless default for imported images.
11pub fn encode_pixels(
12    rgba: &[u8],
13    width: u32,
14    height: u32,
15    format: WzPngFormat,
16) -> WzResult<Vec<u8>> {
17    let pixel_count = (width * height) as usize;
18    if rgba.len() < pixel_count * 4 {
19        return Err(WzError::Custom(format!(
20            "RGBA data too short: need {} bytes, got {}",
21            pixel_count * 4,
22            rgba.len()
23        )));
24    }
25
26    match format {
27        WzPngFormat::Bgra4444 => Ok(rgba_to_bgra4444(rgba, pixel_count)),
28        WzPngFormat::Bgra8888 => Ok(rgba_to_bgra8888(rgba, pixel_count)),
29        WzPngFormat::Argb1555 => Ok(rgba_to_argb1555(rgba, pixel_count)),
30        WzPngFormat::Rgb565 => Ok(rgba_to_rgb565(rgba, pixel_count)),
31        WzPngFormat::R16 => Ok(rgba_to_r16(rgba, pixel_count)),
32        WzPngFormat::A8 => Ok(rgba_to_a8(rgba, pixel_count)),
33        WzPngFormat::Rgba1010102 => Ok(rgba_to_rgba1010102(rgba, pixel_count)),
34        WzPngFormat::Rgba32Float => Ok(rgba_to_rgba32float(rgba, pixel_count)),
35        _ => Err(WzError::Custom(format!(
36            "Encoding not supported for format {:?} — use Bgra8888 instead",
37            format
38        ))),
39    }
40}
41
42pub fn compress_png_data(raw: &[u8]) -> WzResult<Vec<u8>> {
43    use flate2::write::ZlibEncoder;
44    use flate2::Compression;
45    use std::io::Write;
46
47    let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
48    encoder
49        .write_all(raw)
50        .map_err(|e| WzError::Custom(format!("Zlib compression failed: {}", e)))?;
51    encoder
52        .finish()
53        .map_err(|e| WzError::Custom(format!("Zlib compression finish failed: {}", e)))
54}
55
56// ── Per-format encoders ─────────────────────────────────────────────
57
58fn rgba_to_bgra4444(rgba: &[u8], pixel_count: usize) -> Vec<u8> {
59    let mut out = vec![0u8; pixel_count * 2];
60    for i in 0..pixel_count {
61        let (r, g, b, a) = (
62            rgba[i * 4],
63            rgba[i * 4 + 1],
64            rgba[i * 4 + 2],
65            rgba[i * 4 + 3],
66        );
67        let r4 = r >> 4;
68        let g4 = g >> 4;
69        let b4 = b >> 4;
70        let a4 = a >> 4;
71        // lo = [B3..B0 | G3..G0], hi = [R3..R0 | A3..A0]
72        out[i * 2] = b4 | (g4 << 4);
73        out[i * 2 + 1] = r4 | (a4 << 4);
74    }
75    out
76}
77
78fn rgba_to_bgra8888(rgba: &[u8], pixel_count: usize) -> Vec<u8> {
79    let mut out = vec![0u8; pixel_count * 4];
80    for i in 0..pixel_count {
81        out[i * 4] = rgba[i * 4 + 2]; // B
82        out[i * 4 + 1] = rgba[i * 4 + 1]; // G
83        out[i * 4 + 2] = rgba[i * 4]; // R
84        out[i * 4 + 3] = rgba[i * 4 + 3]; // A
85    }
86    out
87}
88
89fn rgba_to_argb1555(rgba: &[u8], pixel_count: usize) -> Vec<u8> {
90    let mut out = vec![0u8; pixel_count * 2];
91    for i in 0..pixel_count {
92        let (r, g, b, a) = (
93            rgba[i * 4],
94            rgba[i * 4 + 1],
95            rgba[i * 4 + 2],
96            rgba[i * 4 + 3],
97        );
98        let r5 = (r as u16 >> 3) & 0x1F;
99        let g5 = (g as u16 >> 3) & 0x1F;
100        let b5 = (b as u16 >> 3) & 0x1F;
101        let a1: u16 = if a >= 128 { 1 } else { 0 };
102        let val = (a1 << 15) | (r5 << 10) | (g5 << 5) | b5;
103        out[i * 2..i * 2 + 2].copy_from_slice(&val.to_le_bytes());
104    }
105    out
106}
107
108fn rgba_to_rgb565(rgba: &[u8], pixel_count: usize) -> Vec<u8> {
109    let mut out = vec![0u8; pixel_count * 2];
110    for i in 0..pixel_count {
111        let (r, g, b) = (rgba[i * 4], rgba[i * 4 + 1], rgba[i * 4 + 2]);
112        let r5 = (r as u16 >> 3) & 0x1F;
113        let g6 = (g as u16 >> 2) & 0x3F;
114        let b5 = (b as u16 >> 3) & 0x1F;
115        let val = (r5 << 11) | (g6 << 5) | b5;
116        out[i * 2..i * 2 + 2].copy_from_slice(&val.to_le_bytes());
117    }
118    out
119}
120
121fn rgba_to_r16(rgba: &[u8], pixel_count: usize) -> Vec<u8> {
122    let mut out = vec![0u8; pixel_count * 2];
123    for i in 0..pixel_count {
124        let r = rgba[i * 4];
125        // High byte = red channel, low byte = 0
126        out[i * 2] = 0;
127        out[i * 2 + 1] = r;
128    }
129    out
130}
131
132fn rgba_to_a8(rgba: &[u8], pixel_count: usize) -> Vec<u8> {
133    let mut out = vec![0u8; pixel_count];
134    for i in 0..pixel_count {
135        out[i] = rgba[i * 4 + 3]; // alpha channel
136    }
137    out
138}
139
140fn rgba_to_rgba1010102(rgba: &[u8], pixel_count: usize) -> Vec<u8> {
141    let mut out = vec![0u8; pixel_count * 4];
142    for i in 0..pixel_count {
143        let (r, g, b, a) = (
144            rgba[i * 4],
145            rgba[i * 4 + 1],
146            rgba[i * 4 + 2],
147            rgba[i * 4 + 3],
148        );
149        // 8-bit → 10-bit: (val << 2) | (val >> 6)
150        let r10 = ((r as u32) << 2) | ((r as u32) >> 6);
151        let g10 = ((g as u32) << 2) | ((g as u32) >> 6);
152        let b10 = ((b as u32) << 2) | ((b as u32) >> 6);
153        // 8-bit → 2-bit: val / 85
154        let a2 = (a as u32) / 85;
155        let val = r10 | (g10 << 10) | (b10 << 20) | (a2 << 30);
156        out[i * 4..i * 4 + 4].copy_from_slice(&val.to_le_bytes());
157    }
158    out
159}
160
161fn rgba_to_rgba32float(rgba: &[u8], pixel_count: usize) -> Vec<u8> {
162    let mut out = vec![0u8; pixel_count * 16];
163    for i in 0..pixel_count {
164        let r = rgba[i * 4] as f32 / 255.0;
165        let g = rgba[i * 4 + 1] as f32 / 255.0;
166        let b = rgba[i * 4 + 2] as f32 / 255.0;
167        let a = rgba[i * 4 + 3] as f32 / 255.0;
168        out[i * 16..i * 16 + 4].copy_from_slice(&r.to_le_bytes());
169        out[i * 16 + 4..i * 16 + 8].copy_from_slice(&g.to_le_bytes());
170        out[i * 16 + 8..i * 16 + 12].copy_from_slice(&b.to_le_bytes());
171        out[i * 16 + 12..i * 16 + 16].copy_from_slice(&a.to_le_bytes());
172    }
173    out
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use crate::image::{decode_pixels, decompress_png_data};
180
181    // ── Roundtrip: encode → decode should recover original RGBA ──────
182
183    #[test]
184    fn test_bgra8888_roundtrip() {
185        let rgba = vec![0xFF, 0x80, 0x00, 0xC0, 0x10, 0x20, 0x30, 0x40];
186        let encoded = encode_pixels(&rgba, 2, 1, WzPngFormat::Bgra8888).unwrap();
187        let decoded = decode_pixels(&encoded, 2, 1, WzPngFormat::Bgra8888).unwrap();
188        assert_eq!(decoded, rgba);
189    }
190
191    #[test]
192    fn test_bgra4444_roundtrip_lossy() {
193        // 4-bit quantization is lossy — high nibble should survive
194        let rgba = vec![0xF0, 0x80, 0x30, 0xA0];
195        let encoded = encode_pixels(&rgba, 1, 1, WzPngFormat::Bgra4444).unwrap();
196        let decoded = decode_pixels(&encoded, 1, 1, WzPngFormat::Bgra4444).unwrap();
197        // Each channel: val >> 4 then (nibble << 4) | nibble
198        assert_eq!(decoded[0], 0xFF); // 0xF0 >> 4 = 0xF → 0xFF
199        assert_eq!(decoded[1], 0x88); // 0x80 >> 4 = 0x8 → 0x88
200        assert_eq!(decoded[2], 0x33); // 0x30 >> 4 = 0x3 → 0x33
201        assert_eq!(decoded[3], 0xAA); // 0xA0 >> 4 = 0xA → 0xAA
202    }
203
204    #[test]
205    fn test_argb1555_roundtrip_lossy() {
206        // 5-bit quantization + 1-bit alpha
207        let rgba = vec![0xFF, 0xFF, 0xFF, 0xFF];
208        let encoded = encode_pixels(&rgba, 1, 1, WzPngFormat::Argb1555).unwrap();
209        let decoded = decode_pixels(&encoded, 1, 1, WzPngFormat::Argb1555).unwrap();
210        assert_eq!(decoded, vec![0xFF, 0xFF, 0xFF, 0xFF]);
211    }
212
213    #[test]
214    fn test_rgb565_roundtrip_lossy() {
215        let rgba = vec![0xFF, 0xFF, 0xFF, 0xFF];
216        let encoded = encode_pixels(&rgba, 1, 1, WzPngFormat::Rgb565).unwrap();
217        let decoded = decode_pixels(&encoded, 1, 1, WzPngFormat::Rgb565).unwrap();
218        assert_eq!(decoded, vec![0xFF, 0xFF, 0xFF, 0xFF]);
219    }
220
221    #[test]
222    fn test_a8_roundtrip() {
223        let rgba = vec![0xFF, 0xFF, 0xFF, 0x80];
224        let encoded = encode_pixels(&rgba, 1, 1, WzPngFormat::A8).unwrap();
225        assert_eq!(encoded, vec![0x80]);
226        let decoded = decode_pixels(&encoded, 1, 1, WzPngFormat::A8).unwrap();
227        assert_eq!(decoded[3], 0x80);
228    }
229
230    #[test]
231    fn test_r16_roundtrip() {
232        let rgba = vec![0xAB, 0x00, 0x00, 0xFF];
233        let encoded = encode_pixels(&rgba, 1, 1, WzPngFormat::R16).unwrap();
234        let decoded = decode_pixels(&encoded, 1, 1, WzPngFormat::R16).unwrap();
235        assert_eq!(decoded[0], 0xAB);
236    }
237
238    #[test]
239    fn test_rgba1010102_roundtrip_lossy() {
240        let rgba = vec![0xFF, 0xFF, 0xFF, 0xFF];
241        let encoded = encode_pixels(&rgba, 1, 1, WzPngFormat::Rgba1010102).unwrap();
242        let decoded = decode_pixels(&encoded, 1, 1, WzPngFormat::Rgba1010102).unwrap();
243        assert_eq!(decoded, vec![0xFF, 0xFF, 0xFF, 0xFF]);
244    }
245
246    #[test]
247    fn test_dxt_encoding_unsupported() {
248        let rgba = vec![0; 64]; // 4x4
249        assert!(encode_pixels(&rgba, 4, 4, WzPngFormat::Dxt3).is_err());
250        assert!(encode_pixels(&rgba, 4, 4, WzPngFormat::Dxt5).is_err());
251        assert!(encode_pixels(&rgba, 4, 4, WzPngFormat::Bc7).is_err());
252    }
253
254    #[test]
255    fn test_short_input_rejected() {
256        assert!(encode_pixels(&[0; 3], 1, 1, WzPngFormat::Bgra8888).is_err());
257    }
258
259    // ── compress → decompress roundtrip ──────────────────────────────
260
261    #[test]
262    fn test_compress_decompress_roundtrip() {
263        let raw = vec![0xAA; 1024];
264        let compressed = compress_png_data(&raw).unwrap();
265        let decompressed = decompress_png_data(&compressed, None).unwrap();
266        assert_eq!(decompressed, raw);
267    }
268
269    #[test]
270    fn test_compress_actually_compresses() {
271        // Repetitive data should compress significantly
272        let raw = vec![0x42; 4096];
273        let compressed = compress_png_data(&raw).unwrap();
274        assert!(compressed.len() < raw.len() / 2);
275    }
276}