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