Coverage Report

Created: 2026-02-26 07:34

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/image/src/codecs/dxt.rs
Line
Count
Source
1
//!  Decoding of DXT (S3TC) compression
2
//!
3
//!  DXT is an image format that supports lossy compression
4
//!
5
//!  # Related Links
6
//!  * <https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_texture_compression_s3tc.txt> - Description of the DXT compression OpenGL extensions.
7
//!
8
//!  Note: this module only implements bare DXT encoding/decoding, it does not parse formats that can contain DXT files like .dds
9
10
use std::io::{self, Read};
11
12
use crate::color::ColorType;
13
use crate::error::{ImageError, ImageResult, ParameterError, ParameterErrorKind};
14
use crate::io::ReadExt;
15
use crate::ImageDecoder;
16
17
/// What version of DXT compression are we using?
18
/// Note that DXT2 and DXT4 are left away as they're
19
/// just DXT3 and DXT5 with premultiplied alpha
20
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21
pub(crate) enum DxtVariant {
22
    /// The DXT1 format. 48 bytes of RGB data in a 4x4 pixel square is
23
    /// compressed into an 8 byte block of DXT1 data
24
    DXT1,
25
    /// The DXT3 format. 64 bytes of RGBA data in a 4x4 pixel square is
26
    /// compressed into a 16 byte block of DXT3 data
27
    DXT3,
28
    /// The DXT5 format. 64 bytes of RGBA data in a 4x4 pixel square is
29
    /// compressed into a 16 byte block of DXT5 data
30
    DXT5,
31
}
32
33
impl DxtVariant {
34
    /// Returns the amount of bytes of raw image data
35
    /// that is encoded in a single DXTn block
36
2.24k
    fn decoded_bytes_per_block(self) -> usize {
37
2.24k
        match self {
38
1.36k
            DxtVariant::DXT1 => 48,
39
873
            DxtVariant::DXT3 | DxtVariant::DXT5 => 64,
40
        }
41
2.24k
    }
42
43
    /// Returns the amount of bytes per block of encoded DXTn data
44
2.13k
    fn encoded_bytes_per_block(self) -> usize {
45
2.13k
        match self {
46
1.32k
            DxtVariant::DXT1 => 8,
47
815
            DxtVariant::DXT3 | DxtVariant::DXT5 => 16,
48
        }
49
2.13k
    }
50
51
    /// Returns the color type that is stored in this DXT variant
52
525
    pub(crate) fn color_type(self) -> ColorType {
53
525
        match self {
54
232
            DxtVariant::DXT1 => ColorType::Rgb8,
55
293
            DxtVariant::DXT3 | DxtVariant::DXT5 => ColorType::Rgba8,
56
        }
57
525
    }
58
}
59
60
/// DXT decoder
61
pub(crate) struct DxtDecoder<R: Read> {
62
    inner: R,
63
    width_blocks: u32,
64
    height_blocks: u32,
65
    variant: DxtVariant,
66
    row: u32,
67
}
68
69
impl<R: Read> DxtDecoder<R> {
70
    /// Create a new DXT decoder that decodes from the stream ```r```.
71
    /// As DXT is often stored as raw buffers with the width/height
72
    /// somewhere else the width and height of the image need
73
    /// to be passed in ```width``` and ```height```, as well as the
74
    /// DXT variant in ```variant```.
75
    /// width and height are required to be powers of 2 and at least 4.
76
    /// otherwise an error will be returned
77
107
    pub(crate) fn new(
78
107
        r: R,
79
107
        width: u32,
80
107
        height: u32,
81
107
        variant: DxtVariant,
82
107
    ) -> Result<DxtDecoder<R>, ImageError> {
83
107
        if !width.is_multiple_of(4) || !height.is_multiple_of(4) {
84
            // TODO: this is actually a bit of a weird case. We could return `DecodingError` but
85
            // it's not really the format that is wrong However, the encoder should surely return
86
            // `EncodingError` so it would be the logical choice for symmetry.
87
2
            return Err(ImageError::Parameter(ParameterError::from_kind(
88
2
                ParameterErrorKind::DimensionMismatch,
89
2
            )));
90
105
        }
91
105
        let width_blocks = width / 4;
92
105
        let height_blocks = height / 4;
93
105
        Ok(DxtDecoder {
94
105
            inner: r,
95
105
            width_blocks,
96
105
            height_blocks,
97
105
            variant,
98
105
            row: 0,
99
105
        })
100
107
    }
101
102
2.24k
    fn scanline_bytes(&self) -> u64 {
103
2.24k
        self.variant.decoded_bytes_per_block() as u64 * u64::from(self.width_blocks)
104
2.24k
    }
105
106
2.13k
    fn read_scanline(&mut self, buf: &mut [u8]) -> io::Result<usize> {
107
2.13k
        assert_eq!(
108
2.13k
            u64::try_from(buf.len()),
109
2.13k
            Ok(
110
2.13k
                #[allow(deprecated)]
111
2.13k
                self.scanline_bytes()
112
2.13k
            )
113
        );
114
115
2.13k
        let len = self.variant.encoded_bytes_per_block() * self.width_blocks as usize;
116
2.13k
        let mut src = Vec::new();
117
2.13k
        self.inner.read_exact_vec(&mut src, len)?;
118
119
2.03k
        match self.variant {
120
1.27k
            DxtVariant::DXT1 => decode_dxt1_row(&src, buf),
121
329
            DxtVariant::DXT3 => decode_dxt3_row(&src, buf),
122
431
            DxtVariant::DXT5 => decode_dxt5_row(&src, buf),
123
        }
124
2.03k
        self.row += 1;
125
2.03k
        Ok(buf.len())
126
2.13k
    }
127
}
128
129
// Note that, due to the way that DXT compression works, a scanline is considered to consist out of
130
// 4 lines of pixels.
131
impl<R: Read> ImageDecoder for DxtDecoder<R> {
132
521
    fn dimensions(&self) -> (u32, u32) {
133
521
        (self.width_blocks * 4, self.height_blocks * 4)
134
521
    }
135
136
417
    fn color_type(&self) -> ColorType {
137
417
        self.variant.color_type()
138
417
    }
139
140
104
    fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> {
141
104
        assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes()));
142
143
        #[allow(deprecated)]
144
2.13k
        for chunk in buf.chunks_mut(self.scanline_bytes().max(1) as usize) {
145
2.13k
            self.read_scanline(chunk)?;
146
        }
147
4
        Ok(())
148
104
    }
149
150
0
    fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> {
151
0
        (*self).read_image(buf)
152
0
    }
153
}
154
155
/**
156
 * Actual encoding/decoding logic below.
157
 */
158
type Rgb = [u8; 3];
159
160
/// decodes a 5-bit R, 6-bit G, 5-bit B 16-bit packed color value into 8-bit RGB
161
/// mapping is done so min/max range values are preserved. So for 5-bit
162
/// values 0x00 -> 0x00 and 0x1F -> 0xFF
163
77.7k
fn enc565_decode(value: u16) -> Rgb {
164
77.7k
    let red = (value >> 11) & 0x1F;
165
77.7k
    let green = (value >> 5) & 0x3F;
166
77.7k
    let blue = (value) & 0x1F;
167
77.7k
    [
168
77.7k
        (red * 0xFF / 0x1F) as u8,
169
77.7k
        (green * 0xFF / 0x3F) as u8,
170
77.7k
        (blue * 0xFF / 0x1F) as u8,
171
77.7k
    ]
172
77.7k
}
173
174
/*
175
 * Functions for decoding DXT compression
176
 */
177
178
/// Constructs the DXT5 alpha lookup table from the two alpha entries
179
/// if alpha0 > alpha1, constructs a table of [a0, a1, 6 linearly interpolated values from a0 to a1]
180
/// if alpha0 <= alpha1, constructs a table of [a0, a1, 4 linearly interpolated values from a0 to a1, 0, 0xFF]
181
947
fn alpha_table_dxt5(alpha0: u8, alpha1: u8) -> [u8; 8] {
182
947
    let mut table = [alpha0, alpha1, 0, 0, 0, 0, 0, 0xFF];
183
947
    if alpha0 > alpha1 {
184
2.37k
        for i in 2..8u16 {
185
2.03k
            table[i as usize] =
186
2.03k
                (((8 - i) * u16::from(alpha0) + (i - 1) * u16::from(alpha1)) / 7) as u8;
187
2.03k
        }
188
    } else {
189
3.04k
        for i in 2..6u16 {
190
2.43k
            table[i as usize] =
191
2.43k
                (((6 - i) * u16::from(alpha0) + (i - 1) * u16::from(alpha1)) / 5) as u8;
192
2.43k
        }
193
    }
194
947
    table
195
947
}
196
197
/// decodes an 8-byte dxt color block into the RGB channels of a 16xRGB or 16xRGBA block.
198
/// source should have a length of 8, dest a length of 48 (RGB) or 64 (RGBA)
199
#[allow(clippy::needless_range_loop)] // False positive, the 0..3 loop is not an enumerate
200
38.8k
fn decode_dxt_colors(source: &[u8], dest: &mut [u8], is_dxt1: bool) {
201
    // sanity checks, also enable the compiler to elide all following bound checks
202
38.8k
    assert!(source.len() == 8 && (dest.len() == 48 || dest.len() == 64));
203
    // calculate pitch to store RGB values in dest (3 for RGB, 4 for RGBA)
204
38.8k
    let pitch = dest.len() / 16;
205
206
    // extract color data
207
38.8k
    let color0 = u16::from(source[0]) | (u16::from(source[1]) << 8);
208
38.8k
    let color1 = u16::from(source[2]) | (u16::from(source[3]) << 8);
209
38.8k
    let color_table = u32::from(source[4])
210
38.8k
        | (u32::from(source[5]) << 8)
211
38.8k
        | (u32::from(source[6]) << 16)
212
38.8k
        | (u32::from(source[7]) << 24);
213
    // let color_table = source[4..8].iter().rev().fold(0, |t, &b| (t << 8) | b as u32);
214
215
    // decode the colors to rgb format
216
38.8k
    let mut colors = [[0; 3]; 4];
217
38.8k
    colors[0] = enc565_decode(color0);
218
38.8k
    colors[1] = enc565_decode(color1);
219
220
    // determine color interpolation method
221
38.8k
    if color0 > color1 || !is_dxt1 {
222
        // linearly interpolate the other two color table entries
223
131k
        for i in 0..3 {
224
98.7k
            colors[2][i] = ((u16::from(colors[0][i]) * 2 + u16::from(colors[1][i]) + 1) / 3) as u8;
225
98.7k
            colors[3][i] = ((u16::from(colors[0][i]) + u16::from(colors[1][i]) * 2 + 1) / 3) as u8;
226
98.7k
        }
227
    } else {
228
        // linearly interpolate one other entry, keep the other at 0
229
23.8k
        for i in 0..3 {
230
17.8k
            colors[2][i] = (u16::from(colors[0][i]) + u16::from(colors[1][i])).div_ceil(2) as u8;
231
17.8k
        }
232
    }
233
234
    // serialize the result. Every color is determined by looking up
235
    // two bits in color_table which identify which color to actually pick from the 4 possible colors
236
661k
    for i in 0..16 {
237
622k
        dest[i * pitch..i * pitch + 3]
238
622k
            .copy_from_slice(&colors[(color_table >> (i * 2)) as usize & 3]);
239
622k
    }
240
38.8k
}
241
242
/// Decodes a 16-byte bock of dxt5 data to a 16xRGBA block
243
947
fn decode_dxt5_block(source: &[u8], dest: &mut [u8]) {
244
947
    assert!(source.len() == 16 && dest.len() == 64);
245
246
    // extract alpha index table (stored as little endian 64-bit value)
247
947
    let alpha_table = source[2..8]
248
947
        .iter()
249
947
        .rev()
250
5.68k
        .fold(0, |t, &b| (t << 8) | u64::from(b));
251
252
    // alpha level decode
253
947
    let alphas = alpha_table_dxt5(source[0], source[1]);
254
255
    // serialize alpha
256
16.0k
    for i in 0..16 {
257
15.1k
        dest[i * 4 + 3] = alphas[(alpha_table >> (i * 3)) as usize & 7];
258
15.1k
    }
259
260
    // handle colors
261
947
    decode_dxt_colors(&source[8..16], dest, false);
262
947
}
263
264
/// Decodes a 16-byte bock of dxt3 data to a 16xRGBA block
265
27.4k
fn decode_dxt3_block(source: &[u8], dest: &mut [u8]) {
266
27.4k
    assert!(source.len() == 16 && dest.len() == 64);
267
268
    // extract alpha index table (stored as little endian 64-bit value)
269
27.4k
    let alpha_table = source[0..8]
270
27.4k
        .iter()
271
27.4k
        .rev()
272
219k
        .fold(0, |t, &b| (t << 8) | u64::from(b));
273
274
    // serialize alpha (stored as 4-bit values)
275
466k
    for i in 0..16 {
276
439k
        dest[i * 4 + 3] = ((alpha_table >> (i * 4)) as u8 & 0xF) * 0x11;
277
439k
    }
278
279
    // handle colors
280
27.4k
    decode_dxt_colors(&source[8..16], dest, false);
281
27.4k
}
282
283
/// Decodes a 8-byte bock of dxt5 data to a 16xRGB block
284
10.4k
fn decode_dxt1_block(source: &[u8], dest: &mut [u8]) {
285
10.4k
    assert!(source.len() == 8 && dest.len() == 48);
286
10.4k
    decode_dxt_colors(source, dest, true);
287
10.4k
}
288
289
/// Decode a row of DXT1 data to four rows of RGB data.
290
/// `source.len()` should be a multiple of 8, otherwise this panics.
291
1.27k
fn decode_dxt1_row(source: &[u8], dest: &mut [u8]) {
292
1.27k
    assert!(source.len().is_multiple_of(8));
293
1.27k
    let block_count = source.len() / 8;
294
1.27k
    assert!(dest.len() >= block_count * 48);
295
296
    // contains the 16 decoded pixels per block
297
1.27k
    let mut decoded_block = [0u8; 48];
298
299
10.4k
    for (x, encoded_block) in source.chunks(8).enumerate() {
300
10.4k
        decode_dxt1_block(encoded_block, &mut decoded_block);
301
302
        // copy the values from the decoded block to linewise RGB layout
303
52.4k
        for line in 0..4 {
304
41.9k
            let offset = (block_count * line + x) * 12;
305
41.9k
            dest[offset..offset + 12].copy_from_slice(&decoded_block[line * 12..(line + 1) * 12]);
306
41.9k
        }
307
    }
308
1.27k
}
309
310
/// Decode a row of DXT3 data to four rows of RGBA data.
311
/// `source.len()` should be a multiple of 16, otherwise this panics.
312
329
fn decode_dxt3_row(source: &[u8], dest: &mut [u8]) {
313
329
    assert!(source.len().is_multiple_of(16));
314
329
    let block_count = source.len() / 16;
315
329
    assert!(dest.len() >= block_count * 64);
316
317
    // contains the 16 decoded pixels per block
318
329
    let mut decoded_block = [0u8; 64];
319
320
27.4k
    for (x, encoded_block) in source.chunks(16).enumerate() {
321
27.4k
        decode_dxt3_block(encoded_block, &mut decoded_block);
322
323
        // copy the values from the decoded block to linewise RGB layout
324
137k
        for line in 0..4 {
325
109k
            let offset = (block_count * line + x) * 16;
326
109k
            dest[offset..offset + 16].copy_from_slice(&decoded_block[line * 16..(line + 1) * 16]);
327
109k
        }
328
    }
329
329
}
330
331
/// Decode a row of DXT5 data to four rows of RGBA data.
332
/// `source.len()` should be a multiple of 16, otherwise this panics.
333
431
fn decode_dxt5_row(source: &[u8], dest: &mut [u8]) {
334
431
    assert!(source.len().is_multiple_of(16));
335
431
    let block_count = source.len() / 16;
336
431
    assert!(dest.len() >= block_count * 64);
337
338
    // contains the 16 decoded pixels per block
339
431
    let mut decoded_block = [0u8; 64];
340
341
947
    for (x, encoded_block) in source.chunks(16).enumerate() {
342
947
        decode_dxt5_block(encoded_block, &mut decoded_block);
343
344
        // copy the values from the decoded block to linewise RGB layout
345
4.73k
        for line in 0..4 {
346
3.78k
            let offset = (block_count * line + x) * 16;
347
3.78k
            dest[offset..offset + 16].copy_from_slice(&decoded_block[line * 16..(line + 1) * 16]);
348
3.78k
        }
349
    }
350
431
}