fastpack_compress/backends/
dxt.rs1use image::GenericImageView;
2
3use crate::{
4 compressor::{CompressInput, CompressOutput, Compressor},
5 error::CompressError,
6};
7
8const DDSD_CAPS: u32 = 0x1;
10const DDSD_HEIGHT: u32 = 0x2;
11const DDSD_WIDTH: u32 = 0x4;
12const DDSD_PIXELFORMAT: u32 = 0x1000;
13const DDSD_LINEARSIZE: u32 = 0x80000;
14const DDPF_FOURCC: u32 = 0x4;
15const DDSCAPS_TEXTURE: u32 = 0x1000;
16
17pub struct Dxt1Compressor;
23
24impl Compressor for Dxt1Compressor {
25 fn compress(&self, input: &CompressInput<'_>) -> Result<CompressOutput, CompressError> {
26 let (w, h) = input.image.dimensions();
27 let rgb = input.image.to_rgb8();
28 let blocks = encode_bc1(rgb.as_raw(), w, h);
29 Ok(CompressOutput {
30 data: build_dds(w, h, b"DXT1", &blocks),
31 })
32 }
33
34 fn format_id(&self) -> &'static str {
35 "dxt1"
36 }
37
38 fn file_extension(&self) -> &'static str {
39 "dds"
40 }
41}
42
43pub struct Dxt5Compressor;
48
49impl Compressor for Dxt5Compressor {
50 fn compress(&self, input: &CompressInput<'_>) -> Result<CompressOutput, CompressError> {
51 let (w, h) = input.image.dimensions();
52 let rgba = input.image.to_rgba8();
53 let blocks = encode_bc3(rgba.as_raw(), w, h);
54 Ok(CompressOutput {
55 data: build_dds(w, h, b"DXT5", &blocks),
56 })
57 }
58
59 fn format_id(&self) -> &'static str {
60 "dxt5"
61 }
62
63 fn file_extension(&self) -> &'static str {
64 "dds"
65 }
66}
67
68fn write_u32(buf: &mut Vec<u8>, v: u32) {
69 buf.extend_from_slice(&v.to_le_bytes());
70}
71
72fn build_dds(w: u32, h: u32, fourcc: &[u8; 4], blocks: &[u8]) -> Vec<u8> {
73 let mut dds = Vec::with_capacity(4 + 124 + blocks.len());
74 dds.extend_from_slice(b"DDS ");
75 write_u32(&mut dds, 124); write_u32(
77 &mut dds,
78 DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT | DDSD_LINEARSIZE,
79 ); write_u32(&mut dds, h); write_u32(&mut dds, w); write_u32(&mut dds, blocks.len() as u32); write_u32(&mut dds, 0); write_u32(&mut dds, 1); for _ in 0..11 {
86 write_u32(&mut dds, 0); }
88 write_u32(&mut dds, 32); write_u32(&mut dds, DDPF_FOURCC); dds.extend_from_slice(fourcc); for _ in 0..5 {
93 write_u32(&mut dds, 0); }
95 write_u32(&mut dds, DDSCAPS_TEXTURE); for _ in 0..4 {
97 write_u32(&mut dds, 0); }
99 dds.extend_from_slice(blocks);
100 dds
101}
102
103fn encode_bc1(rgb: &[u8], w: u32, h: u32) -> Vec<u8> {
104 let bw = w.div_ceil(4);
105 let bh = h.div_ceil(4);
106 let mut out = Vec::with_capacity((bw * bh * 8) as usize);
107 for by in 0..bh {
108 for bx in 0..bw {
109 let block = extract_block_rgb(rgb, w, h, bx * 4, by * 4);
110 out.extend_from_slice(&encode_bc1_block(&block));
111 }
112 }
113 out
114}
115
116fn encode_bc3(rgba: &[u8], w: u32, h: u32) -> Vec<u8> {
117 let bw = w.div_ceil(4);
118 let bh = h.div_ceil(4);
119 let mut out = Vec::with_capacity((bw * bh * 16) as usize);
120 for by in 0..bh {
121 for bx in 0..bw {
122 let block = extract_block_rgba(rgba, w, h, bx * 4, by * 4);
123 out.extend_from_slice(&encode_bc3_alpha_block(&block));
124 let rgb_block: [[u8; 3]; 16] =
125 std::array::from_fn(|i| [block[i][0], block[i][1], block[i][2]]);
126 out.extend_from_slice(&encode_bc1_block(&rgb_block));
127 }
128 }
129 out
130}
131
132fn extract_block_rgb(rgb: &[u8], img_w: u32, img_h: u32, bx: u32, by: u32) -> [[u8; 3]; 16] {
133 let mut block = [[0u8; 3]; 16];
134 for y in 0..4u32 {
135 for x in 0..4u32 {
136 let px = (bx + x).min(img_w - 1) as usize;
137 let py = (by + y).min(img_h - 1) as usize;
138 let i = py * img_w as usize + px;
139 block[(y * 4 + x) as usize] = [rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]];
140 }
141 }
142 block
143}
144
145fn extract_block_rgba(rgba: &[u8], img_w: u32, img_h: u32, bx: u32, by: u32) -> [[u8; 4]; 16] {
146 let mut block = [[0u8; 4]; 16];
147 for y in 0..4u32 {
148 for x in 0..4u32 {
149 let px = (bx + x).min(img_w - 1) as usize;
150 let py = (by + y).min(img_h - 1) as usize;
151 let i = py * img_w as usize + px;
152 block[(y * 4 + x) as usize] = [
153 rgba[i * 4],
154 rgba[i * 4 + 1],
155 rgba[i * 4 + 2],
156 rgba[i * 4 + 3],
157 ];
158 }
159 }
160 block
161}
162
163fn rgb_to_565(r: u8, g: u8, b: u8) -> u16 {
164 ((r as u16 >> 3) << 11) | ((g as u16 >> 2) << 5) | (b as u16 >> 3)
165}
166
167fn rgb_from_565(v: u16) -> [u8; 3] {
168 let r5 = (v >> 11) as u8;
169 let g6 = ((v >> 5) & 0x3f) as u8;
170 let b5 = (v & 0x1f) as u8;
171 [
172 (r5 << 3) | (r5 >> 2),
173 (g6 << 2) | (g6 >> 4),
174 (b5 << 3) | (b5 >> 2),
175 ]
176}
177
178fn color_dist_sq(a: [u8; 3], b: [u8; 3]) -> u32 {
179 let dr = a[0] as i32 - b[0] as i32;
180 let dg = a[1] as i32 - b[1] as i32;
181 let db = a[2] as i32 - b[2] as i32;
182 (dr * dr + dg * dg + db * db) as u32
183}
184
185fn encode_bc1_block(pixels: &[[u8; 3]; 16]) -> [u8; 8] {
186 let (rmin, rmax, gmin, gmax, bmin, bmax) = pixels.iter().fold(
187 (255u8, 0u8, 255u8, 0u8, 255u8, 0u8),
188 |(rn, rx, gn, gx, bn, bx), p| {
189 (
190 rn.min(p[0]),
191 rx.max(p[0]),
192 gn.min(p[1]),
193 gx.max(p[1]),
194 bn.min(p[2]),
195 bx.max(p[2]),
196 )
197 },
198 );
199
200 let mut c0 = rgb_to_565(rmax, gmax, bmax);
201 let mut c1 = rgb_to_565(rmin, gmin, bmin);
202
203 if c0 < c1 {
205 std::mem::swap(&mut c0, &mut c1);
206 } else if c0 == c1 {
207 if c0 > 0 {
210 c1 -= 1;
211 } else {
212 c0 += 1;
213 }
214 }
215
216 let col0 = rgb_from_565(c0);
217 let col1 = rgb_from_565(c1);
218 let col2 = [
219 ((2 * col0[0] as u32 + col1[0] as u32 + 1) / 3) as u8,
220 ((2 * col0[1] as u32 + col1[1] as u32 + 1) / 3) as u8,
221 ((2 * col0[2] as u32 + col1[2] as u32 + 1) / 3) as u8,
222 ];
223 let col3 = [
224 ((col0[0] as u32 + 2 * col1[0] as u32 + 1) / 3) as u8,
225 ((col0[1] as u32 + 2 * col1[1] as u32 + 1) / 3) as u8,
226 ((col0[2] as u32 + 2 * col1[2] as u32 + 1) / 3) as u8,
227 ];
228
229 let mut indices = 0u32;
230 for (i, p) in pixels.iter().enumerate() {
231 let d0 = color_dist_sq(*p, col0);
232 let d1 = color_dist_sq(*p, col1);
233 let d2 = color_dist_sq(*p, col2);
234 let d3 = color_dist_sq(*p, col3);
235 let idx = if d0 <= d1 && d0 <= d2 && d0 <= d3 {
236 0u32
237 } else if d1 <= d2 && d1 <= d3 {
238 1
239 } else if d2 <= d3 {
240 2
241 } else {
242 3
243 };
244 indices |= idx << (i * 2);
245 }
246
247 let mut out = [0u8; 8];
248 out[0..2].copy_from_slice(&c0.to_le_bytes());
249 out[2..4].copy_from_slice(&c1.to_le_bytes());
250 out[4..8].copy_from_slice(&indices.to_le_bytes());
251 out
252}
253
254fn encode_bc3_alpha_block(pixels: &[[u8; 4]; 16]) -> [u8; 8] {
255 let a0 = pixels.iter().map(|p| p[3]).max().unwrap_or(255);
256 let a1 = pixels.iter().map(|p| p[3]).min().unwrap_or(0);
257
258 let refs: [u8; 8] = if a0 > a1 {
260 [
261 a0,
262 a1,
263 ((6 * a0 as u16 + a1 as u16 + 3) / 7) as u8,
264 ((5 * a0 as u16 + 2 * a1 as u16 + 3) / 7) as u8,
265 ((4 * a0 as u16 + 3 * a1 as u16 + 3) / 7) as u8,
266 ((3 * a0 as u16 + 4 * a1 as u16 + 3) / 7) as u8,
267 ((2 * a0 as u16 + 5 * a1 as u16 + 3) / 7) as u8,
268 ((a0 as u16 + 6 * a1 as u16 + 3) / 7) as u8,
269 ]
270 } else {
271 [a0; 8]
273 };
274
275 let mut bits: u64 = 0;
276 for (i, p) in pixels.iter().enumerate() {
277 let a = p[3];
278 let idx = refs
279 .iter()
280 .enumerate()
281 .min_by_key(|&(_, r)| (*r as i16 - a as i16).unsigned_abs())
282 .map(|(idx, _)| idx)
283 .unwrap_or(0) as u64;
284 bits |= idx << (i * 3);
285 }
286
287 let mut out = [0u8; 8];
288 out[0] = a0;
289 out[1] = a1;
290 out[2..8].copy_from_slice(&bits.to_le_bytes()[0..6]);
291 out
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn bc1_block_solid_colour() {
300 let pixels: [[u8; 3]; 16] = [[255, 0, 0]; 16];
301 let block = encode_bc1_block(&pixels);
302 let c0 = u16::from_le_bytes([block[0], block[1]]);
304 let c1 = u16::from_le_bytes([block[2], block[3]]);
305 assert!(c0 > c1, "4-colour mode: c0({c0}) must be > c1({c1})");
306 }
307
308 #[test]
309 fn bc3_alpha_block_full_range() {
310 let mut pixels = [[0u8; 4]; 16];
311 for (i, p) in pixels.iter_mut().enumerate() {
312 p[3] = (i * 17) as u8;
313 }
314 let block = encode_bc3_alpha_block(&pixels);
315 assert_eq!(block[0], 255); assert_eq!(block[1], 0); }
318
319 #[test]
320 fn dds_header_magic_and_size() {
321 let blocks = vec![0u8; 8];
322 let dds = build_dds(4, 4, b"DXT1", &blocks);
323 assert_eq!(&dds[0..4], b"DDS ");
324 assert_eq!(u32::from_le_bytes(dds[4..8].try_into().unwrap()), 124);
325 }
326
327 #[test]
328 fn dxt1_output_byte_count() {
329 let compressor = Dxt1Compressor;
330 let img = image::DynamicImage::new_rgb8(8, 8);
331 let out = compressor
332 .compress(&CompressInput {
333 image: &img,
334 pack_mode: fastpack_core::types::config::PackMode::Fast,
335 quality: 0,
336 })
337 .unwrap();
338 assert_eq!(out.data.len(), 4 + 124 + 4 * 8);
340 }
341
342 #[test]
343 fn dxt5_output_byte_count() {
344 let compressor = Dxt5Compressor;
345 let img = image::DynamicImage::new_rgba8(4, 4);
346 let out = compressor
347 .compress(&CompressInput {
348 image: &img,
349 pack_mode: fastpack_core::types::config::PackMode::Fast,
350 quality: 0,
351 })
352 .unwrap();
353 assert_eq!(out.data.len(), 4 + 124 + 16);
355 }
356}