image_dds/
lib.rs

1//! # image_dds
2//! image_dds enables converting uncompressed image data to and from compressed formats.
3//!
4//! Start converting image data by creating a [Surface] and using one of the provided methods.
5//!
6//! # Usage
7//! The main conversion functions [image_from_dds] and [dds_from_image] convert between [ddsfile] and [image].
8//! For working with floating point images like EXR files, use [imagef32_from_dds] and [dds_from_imagef32].
9//!
10//! These functions are wrappers over conversion methods for [Surface], [SurfaceRgba8], and [SurfaceRgba32Float].
11//! These methods are ideal for internal conversions in libraries
12//! or applications that want to use [Surface] instead of DDS as an intermediate format.
13//!
14//! Surfaces may use owned or borrowed data depending on whether the operation is lossless or not.
15//! A [SurfaceRgba8] can represent a view over an [image::RgbaImage] without any copies, for example.
16//!
17//! For working with custom texture file formats like in video games,
18//! consider defining conversion methods to and from [Surface] to enable chaining operations.
19//! These methods may need to return an error if not all texture formats are supported by [ImageFormat].
20//!
21//! ```rust no_run
22//! # struct CustomTex;
23//! # impl CustomTex {
24//! #     fn to_surface(&self) -> Result<image_dds::Surface<Vec<u8>>, Box<dyn std::error::Error>> {
25//! #         todo!()
26//! #     }
27//! #     fn from_surface<T: AsRef<[u8]>>(
28//! #         surface: image_dds::Surface<T>,
29//! #     ) -> Result<image_dds::Surface<Vec<u8>>, Box<dyn std::error::Error>> {
30//! #         todo!()
31//! #     }
32//! # }
33//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
34//! # let custom_tex = CustomTex;
35//! let dds = custom_tex.to_surface()?.to_dds()?;
36//!
37//! let image = image::open("cat.png").unwrap().to_rgba8();
38//! let surface = image_dds::SurfaceRgba8::from_image(&image).encode(
39//!     image_dds::ImageFormat::BC7RgbaUnorm,
40//!     image_dds::Quality::Normal,
41//!     image_dds::Mipmaps::GeneratedAutomatic,
42//! )?;
43//! let new_custom_tex = CustomTex::from_surface(surface)?;
44//! # Ok(()) }
45//! ```
46//!
47//! # Features
48//! Despite the name, neither the `ddsfile` nor `image` crates are required
49//! and can be disabled in the Cargo.toml by setting `default-features = false`.
50//! The `"ddsfile"` and `"image"` features can then be enabled individually.
51//! The `"encode"` feature is enabled by default but can be disabled
52//! to resolve compilation errors on some targets if not needed.
53//!
54//! # Direct Draw Surface (DDS)
55//! DDS can store GPU texture data in a variety of formats.
56//! This includes compressed formats like [ImageFormat::BC7RgbaUnorm] or uncompressed formats like [ImageFormat::Rgba8Unorm].
57//! Libraries and applications for working with custom GPU texture file formats often support DDS.
58//! This makes DDS a good interchange format for texture conversion workflows.
59//!
60//! DDS has more limited application support compared to
61//! standard formats like TIFF or PNG especially on Linux and MacOS.
62//! GPU compression formats tend to be lossy, which makes it a poor choice for archival purposes.
63//! For this reason, it's often more convenient to work with texture data in an uncompressed format.
64//!
65//! image_dds enables safe and efficient compressed GPU texture conversion across platforms.
66//! A conversion pipeline may look like GPU Texture <-> DDS <-> image with the
67//! conversions to and from image and DDS provided by image_dds.
68//!
69//! Although widely supported by modern desktop and console hardware, not all contexts
70//! support compressed texture formats. DDS plugins for image editors may not support newer
71//! compression formats like BC7. Rendering APIs may not support some compressed formats or only make it available
72//! via an extension such as in the browser.
73//! image_dds supports decoding surfaces to RGBA `u8` or `f32` for
74//! better compatibility at the cost of increased memory usage.
75//!
76//! # Limitations
77//! Not all targets will compile by default due to intel-tex-rs-2 using the Intel ISPC compiler
78//! and lacking precompiled kernels for all targets.
79//! Disable the `"encode"` feature if not needed.
80
81mod bcn;
82mod rgba;
83mod surface;
84
85use rgba::convert::Channel;
86pub use surface::{Surface, SurfaceRgba32Float, SurfaceRgba8};
87
88pub mod error;
89use error::*;
90
91#[cfg(feature = "ddsfile")]
92pub use ddsfile;
93
94#[cfg(feature = "image")]
95pub use image;
96
97mod decode;
98
99#[cfg(feature = "encode")]
100mod encode;
101
102#[cfg(feature = "ddsfile")]
103mod dds;
104#[cfg(feature = "ddsfile")]
105pub use dds::*;
106
107/// The conversion quality when encoding to compressed formats.
108///
109/// Higher quality settings run significantly slower.
110/// Block compressed formats like BC7 use a fixed compression ratio,
111/// so lower quality settings do not use less space than slower ones.
112#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
113#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
114#[cfg_attr(
115    feature = "strum",
116    derive(strum::EnumString, strum::Display, strum::EnumIter)
117)]
118#[derive(Debug, PartialEq, Eq, Clone, Copy)]
119pub enum Quality {
120    /// Faster exports with slightly lower quality.
121    Fast,
122    /// Normal export speed and quality.
123    Normal,
124    /// Slower exports for slightly higher quality.
125    Slow,
126}
127
128/// Options for how many mipmaps to generate.
129/// Mipmaps are counted starting from the base level,
130/// so a surface with only the full resolution base level has 1 mipmap.
131#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
132#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
133#[cfg_attr(
134    feature = "strum",
135    derive(strum::EnumString, strum::Display, strum::EnumIter)
136)]
137#[derive(Debug, PartialEq, Eq, Clone, Copy)]
138pub enum Mipmaps {
139    /// No mipmapping. Only the base mip level will be used.
140    Disabled,
141    /// Use the number of mipmaps specified in the input surface.
142    FromSurface,
143    /// Generate mipmaps to create a surface with a desired number of mipmaps.
144    /// A value of `0` or `1` is equivalent to [Mipmaps::Disabled].
145    GeneratedExact(u32),
146    /// Generate mipmaps starting from the base level
147    /// until dimensions can be reduced no further.
148    GeneratedAutomatic,
149}
150
151/// Supported image formats for encoding and decoding.
152///
153/// Not all DDS formats are supported,
154/// but all current variants for [ImageFormat] are supported by some version of DDS.
155#[non_exhaustive]
156#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
157#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
158#[cfg_attr(
159    feature = "strum",
160    derive(strum::EnumString, strum::Display, strum::EnumIter)
161)]
162#[derive(Debug, PartialEq, Eq, Clone, Copy)]
163pub enum ImageFormat {
164    R8Unorm,
165    R8Snorm,
166    Rg8Unorm,
167    Rg8Snorm,
168    Rgba8Unorm,
169    Rgba8UnormSrgb,
170    Rgba16Float,
171    Rgba32Float,
172    Bgr8Unorm,
173    Bgra8Unorm,
174    Bgra8UnormSrgb,
175    Bgra4Unorm,
176    /// DXT1
177    BC1RgbaUnorm,
178    BC1RgbaUnormSrgb,
179    /// DXT3
180    BC2RgbaUnorm,
181    BC2RgbaUnormSrgb,
182    /// DXT5
183    BC3RgbaUnorm,
184    BC3RgbaUnormSrgb,
185    /// RGTC1
186    BC4RUnorm,
187    BC4RSnorm,
188    /// RGTC2
189    BC5RgUnorm,
190    BC5RgSnorm,
191    /// BPTC (float)
192    BC6hRgbUfloat,
193    BC6hRgbSfloat,
194    /// BPTC (unorm)
195    BC7RgbaUnorm,
196    BC7RgbaUnormSrgb,
197    Rgba8Snorm,
198    R16Unorm,
199    R16Snorm,
200    Rg16Unorm,
201    Rg16Snorm,
202    Rgba16Unorm,
203    Rgba16Snorm,
204    R16Float,
205    Rg16Float,
206    R32Float,
207    Rg32Float,
208    Rgb32Float,
209    Bgr5A1Unorm,
210}
211
212impl ImageFormat {
213    // TODO: Is it worth making these public?
214    fn block_dimensions(&self) -> (u32, u32, u32) {
215        match self {
216            ImageFormat::BC1RgbaUnorm => (4, 4, 1),
217            ImageFormat::BC1RgbaUnormSrgb => (4, 4, 1),
218            ImageFormat::BC2RgbaUnorm => (4, 4, 1),
219            ImageFormat::BC2RgbaUnormSrgb => (4, 4, 1),
220            ImageFormat::BC3RgbaUnorm => (4, 4, 1),
221            ImageFormat::BC3RgbaUnormSrgb => (4, 4, 1),
222            ImageFormat::BC4RUnorm => (4, 4, 1),
223            ImageFormat::BC4RSnorm => (4, 4, 1),
224            ImageFormat::BC5RgUnorm => (4, 4, 1),
225            ImageFormat::BC5RgSnorm => (4, 4, 1),
226            ImageFormat::BC6hRgbUfloat => (4, 4, 1),
227            ImageFormat::BC6hRgbSfloat => (4, 4, 1),
228            ImageFormat::BC7RgbaUnorm => (4, 4, 1),
229            ImageFormat::BC7RgbaUnormSrgb => (4, 4, 1),
230            _ => (1, 1, 1),
231        }
232    }
233
234    fn block_size_in_bytes(&self) -> usize {
235        // Size of a block if compressed or pixel if uncompressed.
236        match self {
237            ImageFormat::R8Unorm => 1,
238            ImageFormat::R8Snorm => 1,
239            ImageFormat::Rg8Unorm => 2,
240            ImageFormat::Rg8Snorm => 2,
241            ImageFormat::Rgba8Unorm => 4,
242            ImageFormat::Rgba8UnormSrgb => 4,
243            ImageFormat::Rgba16Float => 8,
244            ImageFormat::Rgba32Float => 16,
245            ImageFormat::Bgra8Unorm => 4,
246            ImageFormat::Bgra8UnormSrgb => 4,
247            ImageFormat::BC1RgbaUnorm => 8,
248            ImageFormat::BC1RgbaUnormSrgb => 8,
249            ImageFormat::BC2RgbaUnorm => 16,
250            ImageFormat::BC2RgbaUnormSrgb => 16,
251            ImageFormat::BC3RgbaUnorm => 16,
252            ImageFormat::BC3RgbaUnormSrgb => 16,
253            ImageFormat::BC4RUnorm => 8,
254            ImageFormat::BC4RSnorm => 8,
255            ImageFormat::BC5RgUnorm => 16,
256            ImageFormat::BC5RgSnorm => 16,
257            ImageFormat::BC6hRgbUfloat => 16,
258            ImageFormat::BC6hRgbSfloat => 16,
259            ImageFormat::BC7RgbaUnorm => 16,
260            ImageFormat::BC7RgbaUnormSrgb => 16,
261            ImageFormat::Bgra4Unorm => 2,
262            ImageFormat::Bgr8Unorm => 3,
263            ImageFormat::R16Unorm => 2,
264            ImageFormat::R16Snorm => 2,
265            ImageFormat::Rg16Unorm => 4,
266            ImageFormat::Rg16Snorm => 4,
267            ImageFormat::Rgba16Unorm => 8,
268            ImageFormat::Rgba16Snorm => 8,
269            ImageFormat::Rg16Float => 4,
270            ImageFormat::Rg32Float => 8,
271            ImageFormat::R16Float => 2,
272            ImageFormat::R32Float => 4,
273            ImageFormat::Rgba8Snorm => 4,
274            ImageFormat::Rgb32Float => 12,
275            ImageFormat::Bgr5A1Unorm => 2,
276        }
277    }
278}
279
280fn max_mipmap_count(max_dimension: u32) -> u32 {
281    // log2(x) + 1
282    u32::BITS - max_dimension.leading_zeros()
283}
284
285/// The reduced value for `base_dimension` at level `mipmap`.
286pub fn mip_dimension(base_dimension: u32, mipmap: u32) -> u32 {
287    // Halve for each mip level.
288    (base_dimension >> mipmap).max(1)
289}
290
291fn downsample_rgba<T: Channel>(
292    new_width: usize,
293    new_height: usize,
294    new_depth: usize,
295    width: usize,
296    height: usize,
297    depth: usize,
298    data: &[T],
299) -> Vec<T> {
300    // Halve the width and height by averaging pixels.
301    // This is faster than resizing using the image crate.
302    let mut new_data = vec![T::ZERO; new_width * new_height * new_depth * 4];
303    for z in 0..new_depth {
304        for x in 0..new_width {
305            for y in 0..new_height {
306                let new_index = (z * new_width * new_height) + y * new_width + x;
307
308                // Average a 2x2x2 pixel region from data into a 1x1x1 pixel region.
309                // This is equivalent to a 3D convolution or pooling operation over the pixels.
310                for c in 0..4 {
311                    let mut sum = 0.0;
312                    let mut count = 0u64;
313                    for z2 in 0..2 {
314                        let sampled_z = (z * 2) + z2;
315                        if sampled_z < depth {
316                            for y2 in 0..2 {
317                                let sampled_y = (y * 2) + y2;
318                                if sampled_y < height {
319                                    for x2 in 0..2 {
320                                        let sampled_x = (x * 2) + x2;
321                                        if sampled_x < width {
322                                            let index = (sampled_z * width * height)
323                                                + (sampled_y * width)
324                                                + sampled_x;
325                                            sum += data[index * 4 + c].to_f32();
326                                            count += 1;
327                                        }
328                                    }
329                                }
330                            }
331                        }
332                    }
333                    new_data[new_index * 4 + c] = T::from_f32(sum / count.max(1) as f32);
334                }
335            }
336        }
337    }
338
339    new_data
340}
341
342fn calculate_offset(
343    layer: u32,
344    depth_level: u32,
345    mipmap: u32,
346    dimensions: (u32, u32, u32),
347    block_dimensions: (u32, u32, u32),
348    block_size_in_bytes: usize,
349    mipmaps_per_layer: u32,
350) -> Option<usize> {
351    // Surfaces typically use a row-major memory layout like surface[layer][mipmap][z][y][x].
352    // Not all mipmaps are the same size, so the offset calculation is slightly more complex.
353    let (width, height, depth) = dimensions;
354    let (block_width, block_height, block_depth) = block_dimensions;
355
356    let mip_sizes = (0..mipmaps_per_layer)
357        .map(|i| {
358            let mip_width = mip_dimension(width, i) as usize;
359            let mip_height = mip_dimension(height, i) as usize;
360            let mip_depth = mip_dimension(depth, i) as usize;
361
362            mip_size(
363                mip_width,
364                mip_height,
365                mip_depth,
366                block_width as usize,
367                block_height as usize,
368                block_depth as usize,
369                block_size_in_bytes,
370            )
371        })
372        .collect::<Option<Vec<_>>>()?;
373
374    // Each depth level adds another rounded 2D slice.
375    let mip_width = mip_dimension(width, mipmap) as usize;
376    let mip_height = mip_dimension(height, mipmap) as usize;
377    let mip_size2d = mip_size(
378        mip_width,
379        mip_height,
380        1,
381        block_width as usize,
382        block_height as usize,
383        block_depth as usize,
384        block_size_in_bytes,
385    )?;
386
387    // Assume mipmaps are tightly packed.
388    // This is the case for DDS surface data.
389    let layer_size: usize = mip_sizes.iter().sum();
390
391    // Each layer should have the same number of mipmaps.
392    let layer_offset = layer as usize * layer_size;
393    let mip_offset: usize = mip_sizes.get(0..mipmap as usize)?.iter().sum();
394    let depth_offset = mip_size2d * depth_level as usize;
395    Some(layer_offset + mip_offset + depth_offset)
396}
397
398fn mip_size(
399    width: usize,
400    height: usize,
401    depth: usize,
402    block_width: usize,
403    block_height: usize,
404    block_depth: usize,
405    block_size_in_bytes: usize,
406) -> Option<usize> {
407    width
408        .div_ceil(block_width)
409        .checked_mul(height.div_ceil(block_height))
410        .and_then(|v| v.checked_mul(depth.div_ceil(block_depth)))
411        .and_then(|v| v.checked_mul(block_size_in_bytes))
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417
418    #[test]
419    fn max_mipmap_count_zero() {
420        assert_eq!(0, max_mipmap_count(0));
421    }
422
423    #[test]
424    fn max_mipmap_count_1() {
425        assert_eq!(1, max_mipmap_count(1));
426    }
427
428    #[test]
429    fn max_mipmap_count_4() {
430        assert_eq!(4, max_mipmap_count(12));
431    }
432
433    #[test]
434    fn downsample_rgba8_4x4() {
435        // Test that a checkerboard is averaged.
436        let original: Vec<_> = std::iter::repeat([0u8, 0u8, 0u8, 0u8, 255u8, 255u8, 255u8, 255u8])
437            .take(4 * 4 / 2)
438            .flatten()
439            .collect();
440        assert_eq!(
441            vec![127u8; 2 * 2 * 1 * 4],
442            downsample_rgba(2, 2, 1, 4, 4, 1, &original)
443        );
444    }
445
446    #[test]
447    fn downsample_rgba8_3x3() {
448        // Test that a checkerboard is averaged.
449        let original: Vec<_> = std::iter::repeat([
450            0u8, 0u8, 0u8, 0u8, 255u8, 255u8, 255u8, 255u8, 0u8, 0u8, 0u8, 0u8,
451        ])
452        .take(3 * 3 / 3)
453        .flatten()
454        .collect();
455        assert_eq!(
456            vec![127u8; 1 * 1 * 4],
457            downsample_rgba(1, 1, 1, 3, 3, 1, &original)
458        );
459    }
460
461    #[test]
462    fn downsample_rgba8_2x2x2() {
463        // Test that two slices of 2x2 pixels are averaged.
464        let original = vec![
465            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255,
466            255, 255, 255, 255, 255, 255, 255, 255,
467        ];
468        assert_eq!(
469            vec![127u8; 1 * 1 * 1 * 4],
470            downsample_rgba(1, 1, 1, 2, 2, 2, &original)
471        );
472    }
473
474    #[test]
475    fn downsample_rgba8_0x0() {
476        assert_eq!(vec![0u8; 4], downsample_rgba(1, 1, 1, 0, 0, 1, &[]));
477    }
478
479    #[test]
480    fn downsample_rgbaf32_4x4() {
481        // Test that a checkerboard is averaged.
482        let original: Vec<_> = std::iter::repeat([
483            0.0f32, 0.0f32, 0.0f32, 0.0f32, 1.0f32, 1.0f32, 1.0f32, 1.0f32,
484        ])
485        .take(4 * 4 / 2)
486        .flatten()
487        .collect();
488        assert_eq!(
489            vec![0.5; 2 * 2 * 1 * 4],
490            downsample_rgba(2, 2, 1, 4, 4, 1, &original)
491        );
492    }
493
494    #[test]
495    fn downsample_rgbaf32_3x3() {
496        // Test that a checkerboard is averaged.
497        let original: Vec<_> = std::iter::repeat([
498            0.0f32, 0.0f32, 0.0f32, 0.0f32, 1.0f32, 1.0f32, 1.0f32, 1.0f32, 0.0f32, 0.0f32, 0.0f32,
499            0.0f32,
500        ])
501        .take(3 * 3 / 3)
502        .flatten()
503        .collect();
504        assert_eq!(
505            vec![0.5; 1 * 1 * 4],
506            downsample_rgba(1, 1, 1, 3, 3, 1, &original)
507        );
508    }
509
510    #[test]
511    fn downsample_rgbaf32_2x2x2() {
512        // Test that two slices of 2x2 pixels are averaged.
513        let original = vec![
514            0.0f32, 0.0f32, 0.0f32, 0.0f32, 0.0f32, 0.0f32, 0.0f32, 0.0f32, 0.0f32, 0.0f32, 0.0f32,
515            0.0f32, 0.0f32, 0.0f32, 0.0f32, 0.0f32, 1.0f32, 1.0f32, 1.0f32, 1.0f32, 1.0f32, 1.0f32,
516            1.0f32, 1.0f32, 1.0f32, 1.0f32, 1.0f32, 1.0f32, 1.0f32, 1.0f32, 1.0f32, 1.0f32,
517        ];
518        assert_eq!(
519            vec![0.5; 1 * 1 * 1 * 4],
520            downsample_rgba(1, 1, 1, 2, 2, 2, &original)
521        );
522    }
523
524    #[test]
525    fn downsample_rgbaf32_0x0() {
526        assert_eq!(vec![0.0f32; 4], downsample_rgba(1, 1, 1, 0, 0, 1, &[]));
527    }
528
529    #[test]
530    fn calculate_offset_layer0_mip0() {
531        assert_eq!(
532            0,
533            calculate_offset(0, 0, 0, (8, 8, 8), (4, 4, 4), 16, 4).unwrap()
534        );
535    }
536
537    #[test]
538    fn calculate_offset_layer0_mip2() {
539        // The sum of the first 2 mipmaps.
540        assert_eq!(
541            128 + 16,
542            calculate_offset(0, 0, 2, (8, 8, 8), (4, 4, 4), 16, 4).unwrap()
543        );
544    }
545
546    #[test]
547    fn calculate_offset_layer2_mip0() {
548        // The sum of the first 2 array layers.
549        // Each mipmap must have at least a full block of data.
550        assert_eq!(
551            (128 + 16 + 16 + 16) * 2,
552            calculate_offset(2, 0, 0, (8, 8, 8), (4, 4, 4), 16, 4).unwrap()
553        );
554    }
555
556    #[test]
557    fn calculate_offset_layer2_mip2() {
558        // The sum of the first two layers and two more mipmaps.
559        // Each mipmap must have at least a full block of data.
560        assert_eq!(
561            (128 + 16 + 16 + 16) * 2 + 128 + 16,
562            calculate_offset(2, 0, 2, (8, 8, 8), (4, 4, 4), 16, 4).unwrap()
563        );
564    }
565
566    #[test]
567    fn calculate_offset_level2() {
568        // Each 2D level is rounded up to 16x16 pixels.
569        assert_eq!(
570            16 * 16 * 2,
571            calculate_offset(0, 2, 0, (15, 15, 15), (4, 4, 4), 16, 1).unwrap()
572        );
573    }
574
575    #[test]
576    fn calculate_offset_level3() {
577        // Each 2D level is 16x16 pixels.
578        assert_eq!(
579            16 * 16 * 3 * 4,
580            calculate_offset(0, 3, 0, (16, 16, 16), (1, 1, 1), 4, 1).unwrap()
581        );
582    }
583}