diff --git a/Cargo.toml b/Cargo.toml index 2b07410fd5..a279fa247c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ num-traits = { version = "0.2.0" } # Optional dependencies color_quant = { version = "1.1", optional = true } dav1d = { version = "0.10.3", optional = true } +dds = { version = "0.1", optional = true } exr = { version = "1.5.0", optional = true } gif = { version = "0.13.1", optional = true } image-webp = { version = "0.2.0", optional = true } @@ -70,7 +71,7 @@ default = ["rayon", "default-formats"] default-formats = ["avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp"] avif = ["dep:ravif", "dep:rgb"] bmp = [] -dds = [] +dds = ["dep:dds"] exr = ["dep:exr"] ff = [] # Farbfeld image format gif = ["dep:gif", "dep:color_quant"] @@ -85,7 +86,7 @@ tiff = ["dep:tiff"] webp = ["dep:image-webp"] # Other features -rayon = ["dep:rayon", "ravif?/threading"] # Enables multi-threading +rayon = ["dep:rayon", "ravif?/threading", "dds?/rayon"] # Enables multi-threading nasm = ["ravif?/asm"] # Enables use of nasm by rav1e (requires nasm to be installed) color_quant = ["dep:color_quant"] # Enables color quantization avif-native = ["dep:mp4parse", "dep:dav1d"] # Enable native dependency libdav1d diff --git a/README.md b/README.md index 64254a39e4..77d91df116 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ image format encoders and decoders. | -------- | ----------------------------------------- | --------------------------------------- | | AVIF | Yes \* | Yes (lossy only) | | BMP | Yes | Yes | -| DDS | Yes | --- | +| DDS | Yes | Yes \*\* | | Farbfeld | Yes | Yes | | GIF | Yes | Yes | | HDR | Yes | Yes | @@ -61,6 +61,7 @@ image format encoders and decoders. | WebP | Yes | Yes (lossless only) | - \* Requires the `avif-native` feature, uses the libdav1d C library. +- \*\* See the documentation of the [`dds` crate](https://github.com/image-rs/image-dds) for a full list of supported formats. ## Image Types diff --git a/src/codecs/dds.rs b/src/codecs/dds.rs index d66a5d9de4..500da5d07f 100644 --- a/src/codecs/dds.rs +++ b/src/codecs/dds.rs @@ -1,366 +1,806 @@ -//! Decoding of DDS images +//! Decoding and encoding DDS images //! -//! DDS (DirectDraw Surface) is a container format for storing DXT (S3TC) compressed images. +//! DDS (DirectDraw Surface) is a container format for storing DXT (S3TC) compressed images. //! -//! # Related Links -//! * - Description of the DDS format. +//! # Related Links +//! +//! * - Description of the DDS format. -use std::io::Read; -use std::{error, fmt}; +use std::io::{Read, Seek, Write}; -use byteorder_lite::{LittleEndian, ReadBytesExt}; +use dds::{Channels, Precision}; -#[allow(deprecated)] -use crate::codecs::dxt::{DxtDecoder, DxtVariant}; -use crate::color::ColorType; +use crate::color::{ColorType, ExtendedColorType}; use crate::error::{ - DecodingError, ImageError, ImageFormatHint, ImageResult, UnsupportedError, UnsupportedErrorKind, + DecodingError, EncodingError, ImageError, ImageResult, LimitError, LimitErrorKind, + UnsupportedError, UnsupportedErrorKind, }; -use crate::{ImageDecoder, ImageFormat}; - -/// Errors that can occur during decoding and parsing a DDS image -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] -#[allow(clippy::enum_variant_names)] -enum DecoderError { - /// Wrong DDS channel width - PixelFormatSizeInvalid(u32), - /// Wrong DDS header size - HeaderSizeInvalid(u32), - /// Wrong DDS header flags - HeaderFlagsInvalid(u32), - - /// Invalid DXGI format in DX10 header - DxgiFormatInvalid(u32), - /// Invalid resource dimension - ResourceDimensionInvalid(u32), - /// Invalid flags in DX10 header - Dx10FlagsInvalid(u32), - /// Invalid array size in DX10 header - Dx10ArraySizeInvalid(u32), - - /// DDS "DDS " signature invalid or missing - DdsSignatureInvalid, +use crate::{ImageDecoder, ImageDecoderRect, ImageEncoder, ImageFormat}; + +/// DDS decoder. +/// +/// This decoder supports decoding DDS files with a single texture, including +/// cube maps. Texture arrays and volumes are not supported. +/// +/// It's possible to set the color type the image is decoded as using +/// [`DdsDecoder::set_color_type`]. +pub struct DdsDecoder { + inner: dds::Decoder, + is_cubemap: bool, + size: dds::Size, + color: dds::ColorFormat, } -impl fmt::Display for DecoderError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - DecoderError::PixelFormatSizeInvalid(s) => { - f.write_fmt(format_args!("Invalid DDS PixelFormat size: {s}")) - } - DecoderError::HeaderSizeInvalid(s) => { - f.write_fmt(format_args!("Invalid DDS header size: {s}")) - } - DecoderError::HeaderFlagsInvalid(fs) => { - f.write_fmt(format_args!("Invalid DDS header flags: {fs:#010X}")) - } - DecoderError::DxgiFormatInvalid(df) => { - f.write_fmt(format_args!("Invalid DDS DXGI format: {df}")) - } - DecoderError::ResourceDimensionInvalid(d) => { - f.write_fmt(format_args!("Invalid DDS resource dimension: {d}")) +impl DdsDecoder { + /// Create a new decoder that decodes from the stream `r` + pub fn new(r: R) -> ImageResult { + let options = dds::header::ParseOptions::new_permissive(None); + let decoder = + dds::Decoder::new_with_options(r, &options).map_err(ImageError::from_dds_decode)?; + let layout = decoder.layout(); + + // We only support DDS files with: + // - A single main image with any number of mipmaps + // - A texture array of length 1 representing a cube map + match &layout { + dds::DataLayout::Volume(_) => { + return Err(ImageError::Decoding(DecodingError::new( + ImageFormat::Dds.into(), + "DDS volume textures are not supported for decoding", + ))) } - DecoderError::Dx10FlagsInvalid(fs) => { - f.write_fmt(format_args!("Invalid DDS DX10 header flags: {fs:#010X}")) + dds::DataLayout::TextureArray(texture_array) => { + let supported_length = match texture_array.kind() { + dds::TextureArrayKind::Textures => 1, + dds::TextureArrayKind::CubeMaps => 6, + dds::TextureArrayKind::PartialCubeMap(cube_map_faces) => cube_map_faces.count(), + }; + if texture_array.len() != supported_length as usize { + return Err(ImageError::Decoding(DecodingError::new( + ImageFormat::Dds.into(), + "DDS texture arrays are not supported for decoding", + ))); + } } - DecoderError::Dx10ArraySizeInvalid(s) => { - f.write_fmt(format_args!("Invalid DDS DX10 array size: {s}")) + _ => {} + } + + let mut size = decoder.main_size(); + let mut color = decoder.native_color(); + let is_cubemap = layout.is_cube_map(); + + // all cube map faces are read as one RGBA image + if is_cubemap { + if let (Some(width), Some(height)) = + (size.width.checked_mul(4), size.height.checked_mul(3)) + { + size.width = width; + size.height = height; + color.channels = Channels::Rgba; + } else { + return Err(ImageError::Decoding(DecodingError::new( + ImageFormat::Dds.into(), + "DDS cube map faces are too large to decode", + ))); } - DecoderError::DdsSignatureInvalid => f.write_str("DDS signature not found"), } + + Ok(DdsDecoder { + inner: decoder, + is_cubemap, + size, + color, + }) } -} -impl From for ImageError { - fn from(e: DecoderError) -> ImageError { - ImageError::Decoding(DecodingError::new(ImageFormat::Dds.into(), e)) + /// Set the color type for the decoder. + /// + /// The DDS decoder supports decoding images not just in their native color + /// format, but any user-defined color format. This is useful for decoding + /// images that do not cleanly fit into the native formats. E.g. the DDS + /// format `B5G6R5_UNORM` is decoded as [`ColorType::Rgb8`] by default, but + /// you may want to decode it as [`ColorType::Rgb32F`] instead to avoid the + /// rounding error when converting to `u8`. Similarly, your application may + /// only support 8-bit images, while the DDS file is in a 16/32-bit format. + /// Decoding directly into the final color type is more efficient than + /// decoding into the native format and then converting. + /// + /// # Panics + /// + /// [`ColorType::La8`] and [`ColorType::La16`] are not supported for decoding + /// DDS files. This function will panic if you try to set the color type to + /// these formats. + #[track_caller] + pub fn set_color_type(&mut self, color: ColorType) { + self.color = match color { + ColorType::L8 => dds::ColorFormat::GRAYSCALE_U8, + ColorType::Rgb8 => dds::ColorFormat::RGB_U8, + ColorType::Rgba8 => dds::ColorFormat::RGBA_U8, + ColorType::L16 => dds::ColorFormat::GRAYSCALE_U16, + ColorType::Rgb16 => dds::ColorFormat::RGB_U16, + ColorType::Rgba16 => dds::ColorFormat::RGBA_U16, + ColorType::Rgb32F => dds::ColorFormat::RGB_F32, + ColorType::Rgba32F => dds::ColorFormat::RGBA_F32, + ColorType::La8 | ColorType::La16 => { + panic!("La8 and La16 are not supported for decoding DDS files") + } + }; } } -impl error::Error for DecoderError {} - -/// Header used by DDS image files -#[derive(Debug)] -struct Header { - _flags: u32, - height: u32, - width: u32, - _pitch_or_linear_size: u32, - _depth: u32, - _mipmap_count: u32, - pixel_format: PixelFormat, - _caps: u32, - _caps2: u32, -} +impl ImageDecoder for DdsDecoder { + fn dimensions(&self) -> (u32, u32) { + (self.size.width, self.size.height) + } -/// Extended DX10 header used by some DDS image files -#[derive(Debug)] -struct DX10Header { - dxgi_format: u32, - resource_dimension: u32, - misc_flag: u32, - array_size: u32, - misc_flags_2: u32, -} + fn color_type(&self) -> ColorType { + to_color_type(self.color) + } -/// DDS pixel format -#[derive(Debug)] -struct PixelFormat { - flags: u32, - fourcc: [u8; 4], - _rgb_bit_count: u32, - _r_bit_mask: u32, - _g_bit_mask: u32, - _b_bit_mask: u32, - _a_bit_mask: u32, -} + fn original_color_type(&self) -> ExtendedColorType { + use dds::Format; -impl PixelFormat { - fn from_reader(r: &mut dyn Read) -> ImageResult { - let size = r.read_u32::()?; - if size != 32 { - return Err(DecoderError::PixelFormatSizeInvalid(size).into()); + match self.inner.format() { + Format::R1_UNORM => ExtendedColorType::L1, + Format::B4G4R4A4_UNORM | Format::A4B4G4R4_UNORM => ExtendedColorType::Rgba4, + Format::A8_UNORM => ExtendedColorType::A8, + _ => to_color_type(self.inner.native_color()).into(), } - - Ok(Self { - flags: r.read_u32::()?, - fourcc: { - let mut v = [0; 4]; - r.read_exact(&mut v)?; - v - }, - _rgb_bit_count: r.read_u32::()?, - _r_bit_mask: r.read_u32::()?, - _g_bit_mask: r.read_u32::()?, - _b_bit_mask: r.read_u32::()?, - _a_bit_mask: r.read_u32::()?, - }) } -} -impl Header { - fn from_reader(r: &mut dyn Read) -> ImageResult { - let size = r.read_u32::()?; - if size != 124 { - return Err(DecoderError::HeaderSizeInvalid(size).into()); - } + fn set_limits(&mut self, limits: crate::Limits) -> ImageResult<()> { + limits.check_dimensions(self.size.width, self.size.height)?; - const REQUIRED_FLAGS: u32 = 0x1 | 0x2 | 0x4 | 0x1000; - const VALID_FLAGS: u32 = 0x1 | 0x2 | 0x4 | 0x8 | 0x1000 | 0x20000 | 0x80000 | 0x0080_0000; - let flags = r.read_u32::()?; - if flags & (REQUIRED_FLAGS | !VALID_FLAGS) != REQUIRED_FLAGS { - return Err(DecoderError::HeaderFlagsInvalid(flags).into()); + if let Some(max_alloc) = limits.max_alloc { + self.inner.options.memory_limit = max_alloc.try_into().unwrap_or(usize::MAX); } - let height = r.read_u32::()?; - let width = r.read_u32::()?; - let pitch_or_linear_size = r.read_u32::()?; - let depth = r.read_u32::()?; - let mipmap_count = r.read_u32::()?; - // Skip `dwReserved1` - { - let mut skipped = [0; 4 * 11]; - r.read_exact(&mut skipped)?; - } - let pixel_format = PixelFormat::from_reader(r)?; - let caps = r.read_u32::()?; - let caps2 = r.read_u32::()?; - // Skip `dwCaps3`, `dwCaps4`, `dwReserved2` (unused) - { - let mut skipped = [0; 4 + 4 + 4]; - r.read_exact(&mut skipped)?; + Ok(()) + } + + #[track_caller] + fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { + let color = self.color; + let size = self.size; + + let image = dds::ImageViewMut::new(buf, size, color).expect("Invalid buffer length"); + + if self.is_cubemap { + self.inner + .read_cube_map(image) + .map_err(ImageError::from_dds_decode)?; + } else { + self.inner + .read_surface(image) + .map_err(ImageError::from_dds_decode)?; } - Ok(Self { - _flags: flags, - height, - width, - _pitch_or_linear_size: pitch_or_linear_size, - _depth: depth, - _mipmap_count: mipmap_count, - pixel_format, - _caps: caps, - _caps2: caps2, - }) + Ok(()) + } + + fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { + (*self).read_image(buf) } } -impl DX10Header { - fn from_reader(r: &mut dyn Read) -> ImageResult { - let dxgi_format = r.read_u32::()?; - let resource_dimension = r.read_u32::()?; - let misc_flag = r.read_u32::()?; - let array_size = r.read_u32::()?; - let misc_flags_2 = r.read_u32::()?; - - let dx10_header = Self { - dxgi_format, - resource_dimension, - misc_flag, - array_size, - misc_flags_2, - }; - dx10_header.validate()?; +impl ImageDecoderRect for DdsDecoder { + fn read_rect( + &mut self, + x: u32, + y: u32, + width: u32, + height: u32, + buf: &mut [u8], + row_pitch: usize, + ) -> ImageResult<()> { + // reading rectangles is not supported for cube maps + if self.is_cubemap { + return Err(ImageError::Decoding(DecodingError::new( + ImageFormat::Dds.into(), + "Reading rects from cubemaps is not supported", + ))); + } + + self.inner + .read_surface_rect( + buf, + row_pitch, + dds::Rect::new(x, y, width, height), + self.color, + ) + .map_err(ImageError::from_dds_decode)?; + self.inner + .rewind_to_previous_surface() + .map_err(ImageError::from_dds_decode)?; - Ok(dx10_header) + Ok(()) } +} - fn validate(&self) -> Result<(), ImageError> { - // Note: see https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dds-header-dxt10 for info on valid values - if self.dxgi_format > 132 { - // Invalid format - return Err(DecoderError::DxgiFormatInvalid(self.dxgi_format).into()); +/// The format of a DDS header. +/// +/// DDS supports 2 header formats: +/// +/// - DX9: This is the legacy header format that was used before DirectX 10. +/// - DX10: The modern header format that was introduced with DirectX 10. +/// +/// Both formats are widely supported nowadays, but the DX10 format is the +/// preferred format for a few reasons: +/// +/// 1. It supports more features (such as texture arrays). +/// 2. It uses the DXGI format, which is more consistent, easier to work with, +/// and supports more and more varied formats. +/// +/// DX9 has 2 ways to specify the pixel format of a file: FourCC and channel +/// bit masks. Neither are fully standardized, so formats are supported on +/// a best-effort basis. +/// 3. It is better-specified in general, meaning that the problem of two DDS +/// decoders interpreting the same file differently is virtually non-existent. +/// +/// However, if compatibility with older software and hardware is a concern, DX9 +/// may be the only choice. +/// +/// Note that modern software and hardware will generally support both formats. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum HeaderFormat { + /// The legacy header format used in DirectX 9 and earlier. + Dx9, + /// The modern header format used in DirectX 10 and later. + #[default] + Dx10, +} +impl HeaderFormat { + fn from_header(header: &dds::header::Header) -> Self { + match header { + dds::header::Header::Dx9(_) => HeaderFormat::Dx9, + dds::header::Header::Dx10(_) => HeaderFormat::Dx10, } + } +} - if self.resource_dimension < 2 || self.resource_dimension > 4 { - // Invalid dimension - // Only 1D (2), 2D (3) and 3D (4) resource dimensions are allowed - return Err(DecoderError::ResourceDimensionInvalid(self.resource_dimension).into()); - } +/// The speed-quality tradeoff for compression. +/// +/// This will generally only affect compressed formats, such as the BCn formats. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum CompressionQuality { + /// Fastest compression, lowest quality. + Fastest, + /// A good balance between speed and quality. + #[default] + Default, + /// Slowest compression, highest quality. + High, +} - if self.misc_flag != 0x0 && self.misc_flag != 0x4 { - // Invalid flag - // Only no (0x0) and DDS_RESOURCE_MISC_TEXTURECUBE (0x4) flags are allowed - return Err(DecoderError::Dx10FlagsInvalid(self.misc_flag).into()); - } +/// Format used for encoding. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum DdsFormat { + /// The encoder will automatically pick a linear uncompressed format. + #[default] + AutoUncompressed, + + /// 8-bit uncompressed alpha. + A8Unorm, + /// 8-bit uncompressed Grayscale. + R8Unorm, + /// 8-bit uncompressed RG. + R8G8Unorm, + /// 8-bit uncompressed RGBA. + R8G8B8A8Unorm, + /// 8-bit uncompressed RGBA. + /// + /// This format requires a DX10 header. + R8G8B8A8UnormSrgb, + + /// 16-bit uncompressed Grayscale. + R16Unorm, + /// 16-bit uncompressed RG. + R16G16Unorm, + /// 16-bit uncompressed RGBA. + R16G16B16A16Unorm, + + /// 16-bit floating-point uncompressed Grayscale. + R16Float, + /// 16-bit floating-point uncompressed RG. + R16G16Float, + /// 16-bit floating-point uncompressed RGBA. + R16G16B16A16Float, + + /// 32-bit floating-point uncompressed Grayscale. + R32Float, + /// 32-bit floating-point uncompressed RG. + R32G32Float, + /// 32-bit floating-point uncompressed RGB. + /// + /// This format requires a DX10 header. + R32G32B32Float, + /// 32-bit floating-point uncompressed RGBA. + R32G32B32A32Float, + + /// 8-bit uncompressed BGR. + /// + /// This format requires a DX9 header. + B8G8R8Unorm, + /// 4-bit uncompressed BGRA. + B4G4R4A4Unorm, + /// 5-bit uncompressed BGR with 1-bit alpha. + B5G5R5A1Unorm, + /// Uncompressed BGRA with 5 bits for blue, 6 bits for green, and 5 bits for red. + B5G6R5Unorm, + /// 10-bit uncompressed RGB with 2-bit alpha. + R10G10B10A2Unorm, + + /// An HDR format that can store floating-point numbers between 0.0 and 65408.0. + /// + /// This format requires a DX10 header. + R9G9B9E5Float, + /// An HDR format that stores components as unsigned 11/10-bit floating-point numbers. + /// + /// This format requires a DX10 header. + R11G11B10Float, + + /// 8-bit YUV, 4:2:2 sub-sampled. + YUY2, + + /// Linear BC1. This is also called `DXT1` in DX9. + BC1Unorm, + /// sRGB BC1. + /// + /// This format requires a DX10 header. + BC1UnormSrgb, + /// Linear BC2. This is also called `DXT3` in DX9. + BC2Unorm, + /// sRGB BC2. + /// + /// This format requires a DX10 header. + BC2UnormSrgb, + /// Linear BC3. This is also called `DXT5` in DX9. + BC3Unorm, + /// sRGB BC3. + /// + /// This format requires a DX10 header. + BC3UnormSrgb, + /// Unsigned BC4. This is also called `ATI1` or `BC4U` in DX9. + BC4Unorm, + /// Signed BC4. This is also called `BC4S` in DX9. + BC4Snorm, + /// Unsigned BC5. This is also called `ATI2` or `BC5U` in DX9. + BC5Unorm, + /// Signed BC5. This is also called `BC5S` in DX9. + BC5Snorm, +} +impl DdsFormat { + /// Returns whether the format is an sRGB format. + /// + /// Note that sRGB formats are only supported by DX10 headers. + pub fn is_srgb(self) -> bool { + matches!( + self, + DdsFormat::R8G8B8A8UnormSrgb + | DdsFormat::BC1UnormSrgb + | DdsFormat::BC2UnormSrgb + | DdsFormat::BC3UnormSrgb + ) + } - if self.resource_dimension == 4 && self.array_size != 1 { - // Invalid array size - // 3D textures (resource dimension == 4) must have an array size of 1 - return Err(DecoderError::Dx10ArraySizeInvalid(self.array_size).into()); - } + /// Returns the corresponding DDS format. Note that the format will + /// ALWAYS be linear, even for sRGB formats. + fn to_format(self, color: ExtendedColorType) -> dds::Format { + use dds::Format; - if self.misc_flags_2 > 0x4 { - // Invalid alpha flags - return Err(DecoderError::Dx10FlagsInvalid(self.misc_flags_2).into()); - } + match self { + DdsFormat::AutoUncompressed => { + match color { + // formats that can be represented exactly + ExtendedColorType::A8 => Format::A8_UNORM, + ExtendedColorType::L1 => Format::R1_UNORM, + ExtendedColorType::L8 => Format::R8_UNORM, + ExtendedColorType::Rgb8 | ExtendedColorType::Bgr8 => Format::B8G8R8_UNORM, + ExtendedColorType::Rgba8 | ExtendedColorType::Bgra8 => Format::R8G8B8A8_UNORM, + ExtendedColorType::L16 => Format::R16_UNORM, + ExtendedColorType::Rgba16 => Format::R16G16B16A16_UNORM, + ExtendedColorType::Rgb32F => Format::R32G32B32_FLOAT, + ExtendedColorType::Rgba32F => Format::R32G32B32A32_FLOAT, + + // pick a format that can represent all values + ExtendedColorType::La8 => Format::R8G8B8A8_UNORM, + ExtendedColorType::La16 => Format::R16G16B16A16_UNORM, + ExtendedColorType::Rgb16 => Format::R16G16B16A16_UNORM, + ExtendedColorType::L2 | ExtendedColorType::L4 => Format::R8_UNORM, + ExtendedColorType::La1 + | ExtendedColorType::Rgb1 + | ExtendedColorType::Rgba1 + | ExtendedColorType::La2 + | ExtendedColorType::Rgb2 + | ExtendedColorType::Rgba2 + | ExtendedColorType::La4 + | ExtendedColorType::Rgb4 + | ExtendedColorType::Rgba4 => Format::B4G4R4A4_UNORM, + + _ => unreachable!(), + } + } - Ok(()) + DdsFormat::A8Unorm => Format::A8_UNORM, + DdsFormat::R8Unorm => Format::R8_UNORM, + DdsFormat::R8G8Unorm => Format::R8G8_UNORM, + DdsFormat::R8G8B8A8Unorm => Format::R8G8B8A8_UNORM, + DdsFormat::R8G8B8A8UnormSrgb => Format::R8G8B8A8_UNORM, + DdsFormat::R16Unorm => Format::R16_UNORM, + DdsFormat::R16G16Unorm => Format::R16G16_UNORM, + DdsFormat::R16G16B16A16Unorm => Format::R16G16B16A16_UNORM, + DdsFormat::R16Float => Format::R16_FLOAT, + DdsFormat::R16G16Float => Format::R16G16_FLOAT, + DdsFormat::R16G16B16A16Float => Format::R16G16B16A16_FLOAT, + DdsFormat::R32Float => Format::R32_FLOAT, + DdsFormat::R32G32Float => Format::R32G32_FLOAT, + DdsFormat::R32G32B32Float => Format::R32G32B32_FLOAT, + DdsFormat::R32G32B32A32Float => Format::R32G32B32A32_FLOAT, + DdsFormat::B8G8R8Unorm => Format::B8G8R8_UNORM, + DdsFormat::B4G4R4A4Unorm => Format::B4G4R4A4_UNORM, + DdsFormat::B5G5R5A1Unorm => Format::B5G5R5A1_UNORM, + DdsFormat::B5G6R5Unorm => Format::B5G6R5_UNORM, + DdsFormat::R10G10B10A2Unorm => Format::R10G10B10A2_UNORM, + DdsFormat::R9G9B9E5Float => Format::R9G9B9E5_SHAREDEXP, + DdsFormat::R11G11B10Float => Format::R11G11B10_FLOAT, + DdsFormat::YUY2 => Format::YUY2, + + DdsFormat::BC1Unorm => Format::BC1_UNORM, + DdsFormat::BC1UnormSrgb => Format::BC1_UNORM, + DdsFormat::BC2Unorm => Format::BC2_UNORM, + DdsFormat::BC2UnormSrgb => Format::BC2_UNORM, + DdsFormat::BC3Unorm => Format::BC3_UNORM, + DdsFormat::BC3UnormSrgb => Format::BC3_UNORM, + DdsFormat::BC4Unorm => Format::BC4_UNORM, + DdsFormat::BC4Snorm => Format::BC4_SNORM, + DdsFormat::BC5Unorm => Format::BC5_UNORM, + DdsFormat::BC5Snorm => Format::BC5_SNORM, + } } } -/// The representation of a DDS decoder -pub struct DdsDecoder { - #[allow(deprecated)] - inner: DxtDecoder, +/// Whether and how many mipmaps to generate. +enum Mipmaps { + None, + Full, + Fixed(u8), } -impl DdsDecoder { - /// Create a new decoder that decodes from the stream `r` - pub fn new(mut r: R) -> ImageResult { - let mut magic = [0; 4]; - r.read_exact(&mut magic)?; - if magic != b"DDS "[..] { - return Err(DecoderError::DdsSignatureInvalid.into()); +/// DDS encoder. +/// +/// Creates DDS files containing a single texture with optional mipmaps. +/// +/// ## Usage +/// +/// ```no_run +/// use image::codecs::dds::*; +/// +/// let mut buffer = Vec::new(); +/// let encoder = DdsEncoder::new(&mut buffer) +/// .with_format(DdsFormat::BC1Unorm) +/// .with_preferred_header(HeaderFormat::Dx9) +/// .with_compression_quality(CompressionQuality::High) +/// .with_mipmaps(); +/// ``` +/// +/// An encoder is created from a writer and configured using the builder +/// methods. To encode an image, use the [`ImageEncoder::write_image`] method. +/// +/// ## Options +/// +/// ### Format +/// +/// The DDS format to encode the image with can be set using the +/// [`DdsEncoder::with_format`] method. See [`DdsFormat`] for the list of all +/// supported formats. By default, [`DdsFormat::AutoUncompressed`] is used, +/// which will automatically pick an uncompressed format based on the color type +/// of the image. +/// +/// The header format of the DDS file can be set using the +/// [`DdsEncoder::with_preferred_header`] method. See [`HeaderFormat`] for more +/// information about DDS headers. The default header format is DX10. +/// +/// Note that the specified DDS format takes precedence over the preferred +/// header format. If a DDS format does not support the preferred header format, +/// the encoder will use the header format required by the DDS format. Use +/// [`DdsEncoder::header_format`] to get the actual header format that will be used +/// for encoding. +/// +/// ### Mipmaps +/// +/// The generate mipmaps, you [`DdsEncoder::with_mipmaps`] to generate a full +/// mipmap chain, or [`DdsEncoder::with_mipmap_count`] to generate a specific +/// number of mipmaps. By default, no mipmaps are generated. +/// +/// If mipmaps are generated and the image contains an alpha channel, the alpha +/// channel will be treated as straight alpha/transparency by default. +/// If the alpha channel is something else (e.g. premultiplied alpha or a +/// different channel packed into alpha), use [`DdsEncoder::with_separate_alpha`] +/// to specify that the alpha channel should be treated as a separate channel +/// when resizing the image to generate mipmaps. +/// +/// ### Quality +/// +/// [`DdsEncoder::with_compression_quality`] can be used to set the compression +/// quality for BCn formats. This option trades off compression speed and +/// quality. +/// +/// [`DdsEncoder::with_perceptual_error_metric`] can further improve quality for +/// albedo, diffuse, and photographic textures by using a perceptual error +/// metric when compressing BCn formats. +/// +/// [`DdsEncoder::with_dithering`] enables dithering to reduce quantization and +/// banding artifacts when encoding images with a higher bit depth. +pub struct DdsEncoder { + writer: W, + format: DdsFormat, + preferred_header: HeaderFormat, + quality: dds::CompressionQuality, + error_metric: dds::ErrorMetric, + dithering: dds::Dithering, + straight_alpha: bool, + mipmaps: Mipmaps, +} + +impl DdsEncoder { + /// Create a new encoder that encodes to the stream `w` + pub fn new(w: W) -> Self { + DdsEncoder { + writer: w, + format: DdsFormat::AutoUncompressed, + preferred_header: HeaderFormat::Dx10, + quality: dds::CompressionQuality::Normal, + error_metric: dds::ErrorMetric::Uniform, + dithering: dds::Dithering::None, + straight_alpha: true, + mipmaps: Mipmaps::None, } + } - let header = Header::from_reader(&mut r)?; - - if header.pixel_format.flags & 0x4 != 0 { - #[allow(deprecated)] - let variant = match &header.pixel_format.fourcc { - b"DXT1" => DxtVariant::DXT1, - b"DXT3" => DxtVariant::DXT3, - b"DXT5" => DxtVariant::DXT5, - b"DX10" => { - let dx10_header = DX10Header::from_reader(&mut r)?; - // Format equivalents were taken from https://docs.microsoft.com/en-us/windows/win32/direct3d11/texture-block-compression-in-direct3d-11 - // The enum integer values were taken from https://docs.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format - // DXT1 represents the different BC1 variants, DTX3 represents the different BC2 variants and DTX5 represents the different BC3 variants - match dx10_header.dxgi_format { - 70..=72 => DxtVariant::DXT1, // DXGI_FORMAT_BC1_TYPELESS, DXGI_FORMAT_BC1_UNORM or DXGI_FORMAT_BC1_UNORM_SRGB - 73..=75 => DxtVariant::DXT3, // DXGI_FORMAT_BC2_TYPELESS, DXGI_FORMAT_BC2_UNORM or DXGI_FORMAT_BC2_UNORM_SRGB - 76..=78 => DxtVariant::DXT5, // DXGI_FORMAT_BC3_TYPELESS, DXGI_FORMAT_BC3_UNORM or DXGI_FORMAT_BC3_UNORM_SRGB - _ => { - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Dds.into(), - UnsupportedErrorKind::GenericFeature(format!( - "DDS DXGI Format {}", - dx10_header.dxgi_format - )), - ), - )) - } - } - } - fourcc => { - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Dds.into(), - UnsupportedErrorKind::GenericFeature(format!("DDS FourCC {fourcc:?}")), - ), - )) - } - }; + /// Create a DDS file with the given format. + pub fn with_format(mut self, format: DdsFormat) -> Self { + self.format = format; + self + } - #[allow(deprecated)] - let bytes_per_pixel = variant.color_type().bytes_per_pixel(); + /// Create a DDS file with the given header format. + /// + /// Note that the specified DDS format takes precedence over the preferred + /// header format. So if a DDS format does not support the preferred header + /// format, the encoder will use the header format specified by the DDS + /// format. + /// + /// By default, the encoder will use the DX10 header format. + pub fn with_preferred_header(mut self, header: HeaderFormat) -> Self { + self.preferred_header = header; + self + } - if crate::utils::check_dimension_overflow(header.width, header.height, bytes_per_pixel) - { - return Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Dds.into(), - UnsupportedErrorKind::GenericFeature(format!( - "Image dimensions ({}x{}) are too large", - header.width, header.height - )), - ), - )); - } + /// Set the compression quality for the encoder. + /// + /// By default, the compression quality is set to [`CompressionQuality::Default`]. + /// + /// Currently, this option only affects BC1-BC5 formats. + pub fn with_compression_quality(mut self, quality: CompressionQuality) -> Self { + self.quality = match quality { + CompressionQuality::Fastest => dds::CompressionQuality::Fast, + CompressionQuality::Default => dds::CompressionQuality::Normal, + CompressionQuality::High => dds::CompressionQuality::High, + }; + self + } + + /// Use a perceptual error metric for compression. + /// + /// By default, the encoder uses a uniform error metric for compression, + /// meaning that it tries to minimize the mean squared error (MSE) between the + /// original and compressed image. + /// + /// Better visual results can be achieved by using a perceptual error + /// metric for albedo, diffuse, and photographic textures. This metric + /// accounts for how the human eye perceives color, and will produce better + /// results for these types of images. + /// + /// However, a perceptual error metric is not recommended for normal maps, + /// specular maps, and height maps, as it will produce worse artifacts. + /// + /// Note: The perceptual error metric assumes that image data is in sRGB, + /// irrespective of whether the DDS format is linear or sRGB. Do not use the + /// perceptual error metric if the image data is in linear RGB or any other + /// color space. + /// + /// Currently, this option only affects BC1-BC3 formats. + pub fn with_perceptual_error_metric(mut self) -> Self { + self.error_metric = dds::ErrorMetric::Perceptual; + self + } + + /// Enables dithering when encoding images with a higher bit depth to a + /// lower bit depth. + /// + /// Note: Not all formats support dithering. The encoder will ignore this option + /// if the format does not support dithering. + pub fn with_dithering(mut self) -> Self { + self.dithering = dds::Dithering::ColorAndAlpha; + self + } + + /// Create a DDS file with a full mip chain. + /// + /// Mipmaps will be automatically generated from the main image. + /// + /// To set a specific number of mipmaps, use [`DdsEncoder::with_mipmap_count`]. + /// + /// Note: If the image contains an alpha channel that is **not** straight + /// alpha, you should use also use [`DdsEncoder::with_separate_alpha`] to + /// ensure that the other channels are not affected by the alpha channel. + pub fn with_mipmaps(mut self) -> Self { + self.mipmaps = Mipmaps::Full; + self + } + + /// Create a DDS file with the given number of mipmaps. + /// + /// Unless necessary, it is recommended to use [`DdsEncoder::with_mipmaps`] + /// instead, to automatically generate the full mipmap chain. + /// + /// Note: Setting 0 or 1 mipmaps is equivalent to not generating any mipmaps + /// at all. + pub fn with_mipmap_count(mut self, mipmap_count: u8) -> Self { + self.mipmaps = Mipmaps::Fixed(mipmap_count); + self + } + + /// Separate alpha from the other channels when resizing to generate mipmaps. + /// + /// By default, the encoder assumes straight alpha. This is necessary to + /// prevent color bleeding when resizing the image to generate mipmaps. + /// However, this also means that pixels for which the alpha channel is 0 + /// will lose any and all color information. This is not a problem if the + /// alpha channels contains transparency information, but it is a problem + /// if the alpha channel is used for other purposes (e.g. a mask or height + /// map). In this case, use this function to specify that the alpha channel + /// should be treated as a separate channel and not as transparency. + pub fn with_separate_alpha(mut self) -> Self { + self.straight_alpha = false; + self + } + + /// Returns the header format that will be used for encoding. + /// + /// This is useful, because the preferred header format may not be possible + /// for the given DDS format. + pub fn header_format(&self, color_type: ExtendedColorType) -> HeaderFormat { + let (header, _) = self.get_dds_header_and_format(16, 16, color_type); + HeaderFormat::from_header(&header) + } - #[allow(deprecated)] - let inner = DxtDecoder::new(r, header.width, header.height, variant)?; - Ok(Self { inner }) + fn get_dds_header_and_format( + &self, + width: u32, + height: u32, + color_type: ExtendedColorType, + ) -> (dds::header::Header, dds::Format) { + use dds::header::Header; + + let is_srgb = self.format.is_srgb(); + let format = self.format.to_format(color_type); + + let mut header: Header = if is_srgb { + // force DX10 header + let dxgi = dds::header::DxgiFormat::try_from(format).unwrap(); + dds::header::Dx10Header::new_image(width, height, dxgi.to_srgb()).into() } else { - // For now, supports only DXT variants - Err(ImageError::Unsupported( - UnsupportedError::from_format_and_kind( - ImageFormat::Dds.into(), - UnsupportedErrorKind::Format(ImageFormatHint::Name("DDS".to_string())), - ), - )) + let header = Header::new_image(width, height, format); + match self.preferred_header { + HeaderFormat::Dx9 => header.to_dx9().map(Header::from).unwrap_or(header), + HeaderFormat::Dx10 => header.to_dx10().map(Header::from).unwrap_or(header), + } + }; + + match self.mipmaps { + Mipmaps::None => {} + Mipmaps::Full => header = header.with_mipmaps(), + Mipmaps::Fixed(n) => { + if n > 1 { + header = header.with_mipmap_count(n as u32); + } + } } + + (header, format) } } -impl ImageDecoder for DdsDecoder { - fn dimensions(&self) -> (u32, u32) { - self.inner.dimensions() - } +impl ImageEncoder for DdsEncoder { + fn write_image( + self, + buf: &[u8], + width: u32, + height: u32, + color_type: ExtendedColorType, + ) -> ImageResult<()> { + let color = to_dds_color(color_type).ok_or_else(|| { + ImageError::Unsupported(UnsupportedError::from_format_and_kind( + ImageFormat::Dds.into(), + UnsupportedErrorKind::Color(color_type), + )) + })?; + let image = dds::ImageView::new(buf, dds::Size::new(width, height), color) + .expect("Invalid buffer length"); + + // create the header + let (header, format) = self.get_dds_header_and_format(width, height, color_type); + + // encode the image + let mut encoder = + dds::Encoder::new(self.writer, format, &header).map_err(ImageError::from_dds_encode)?; + encoder.options.dithering = self.dithering; + encoder.options.error_metric = self.error_metric; + encoder.options.quality = self.quality; + + let options = dds::WriteOptions { + generate_mipmaps: true, + resize_straight_alpha: self.straight_alpha, + ..Default::default() + }; - fn color_type(&self) -> ColorType { - self.inner.color_type() + encoder + .write_surface_with(image, None, &options) + .map_err(ImageError::from_dds_encode)?; + encoder.finish().map_err(ImageError::from_dds_encode)?; + + Ok(()) } +} - fn read_image(self, buf: &mut [u8]) -> ImageResult<()> { - self.inner.read_image(buf) +fn to_color_type(color: dds::ColorFormat) -> ColorType { + match (color.channels, color.precision) { + (Channels::Alpha | Channels::Rgba, Precision::U8) => ColorType::Rgba8, + (Channels::Alpha | Channels::Rgba, Precision::U16) => ColorType::Rgba16, + (Channels::Alpha | Channels::Rgba, Precision::F32) => ColorType::Rgba32F, + (Channels::Rgb, Precision::U8) => ColorType::Rgb8, + (Channels::Rgb, Precision::U16) => ColorType::Rgb16, + (Channels::Rgb, Precision::F32) => ColorType::Rgb32F, + (Channels::Grayscale, Precision::U8) => ColorType::L8, + (Channels::Grayscale, Precision::U16) => ColorType::L16, + (Channels::Grayscale, Precision::F32) => ColorType::Rgb32F, } +} - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) +fn to_dds_color(color: ExtendedColorType) -> Option { + match color { + ExtendedColorType::A8 => Some(dds::ColorFormat::ALPHA_U8), + + ExtendedColorType::L8 => Some(dds::ColorFormat::GRAYSCALE_U8), + ExtendedColorType::L16 => Some(dds::ColorFormat::GRAYSCALE_U16), + + ExtendedColorType::Rgb8 => Some(dds::ColorFormat::RGB_U8), + ExtendedColorType::Rgb16 => Some(dds::ColorFormat::RGB_U16), + ExtendedColorType::Rgb32F => Some(dds::ColorFormat::RGB_F32), + + ExtendedColorType::Rgba8 => Some(dds::ColorFormat::RGBA_U8), + ExtendedColorType::Rgba16 => Some(dds::ColorFormat::RGBA_U16), + ExtendedColorType::Rgba32F => Some(dds::ColorFormat::RGBA_F32), + + _ => None, } } -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn dimension_overflow() { - // A DXT1 header set to 0xFFFF_FFFC width and height (the highest u32%4 == 0) - let header = [ - 0x44, 0x44, 0x53, 0x20, 0x7C, 0x0, 0x0, 0x0, 0x7, 0x10, 0x8, 0x0, 0xFC, 0xFF, 0xFF, - 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, 0x0, 0xC0, 0x12, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, - 0x0, 0x49, 0x4D, 0x41, 0x47, 0x45, 0x4D, 0x41, 0x47, 0x49, 0x43, 0x4B, 0x0, 0x0, 0x0, - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, - 0x4, 0x0, 0x0, 0x0, 0x44, 0x58, 0x54, 0x31, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - ]; - - assert!(DdsDecoder::new(&header[..]).is_err()); +impl ImageError { + fn from_dds_decode(e: dds::DecodingError) -> Self { + match e { + dds::DecodingError::Io(e) => ImageError::IoError(e), + dds::DecodingError::MemoryLimitExceeded => { + ImageError::Limits(LimitError::from_kind(LimitErrorKind::DimensionError)) + } + _ => ImageError::Decoding(DecodingError::new(ImageFormat::Dds.into(), e)), + } + } + fn from_dds_encode(e: dds::EncodingError) -> ImageError { + match e { + dds::EncodingError::Io(e) => ImageError::IoError(e), + _ => ImageError::Encoding(EncodingError::new(ImageFormat::Dds.into(), e)), + } } } diff --git a/src/codecs/dxt.rs b/src/codecs/dxt.rs deleted file mode 100644 index be2010b578..0000000000 --- a/src/codecs/dxt.rs +++ /dev/null @@ -1,349 +0,0 @@ -//! Decoding of DXT (S3TC) compression -//! -//! DXT is an image format that supports lossy compression -//! -//! # Related Links -//! * - Description of the DXT compression OpenGL extensions. -//! -//! Note: this module only implements bare DXT encoding/decoding, it does not parse formats that can contain DXT files like .dds - -use std::io::{self, Read}; - -use crate::color::ColorType; -use crate::error::{ImageError, ImageResult, ParameterError, ParameterErrorKind}; -use crate::io::ReadExt; -use crate::ImageDecoder; - -/// What version of DXT compression are we using? -/// Note that DXT2 and DXT4 are left away as they're -/// just DXT3 and DXT5 with premultiplied alpha -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum DxtVariant { - /// The DXT1 format. 48 bytes of RGB data in a 4x4 pixel square is - /// compressed into an 8 byte block of DXT1 data - DXT1, - /// The DXT3 format. 64 bytes of RGBA data in a 4x4 pixel square is - /// compressed into a 16 byte block of DXT3 data - DXT3, - /// The DXT5 format. 64 bytes of RGBA data in a 4x4 pixel square is - /// compressed into a 16 byte block of DXT5 data - DXT5, -} - -impl DxtVariant { - /// Returns the amount of bytes of raw image data - /// that is encoded in a single DXTn block - fn decoded_bytes_per_block(self) -> usize { - match self { - DxtVariant::DXT1 => 48, - DxtVariant::DXT3 | DxtVariant::DXT5 => 64, - } - } - - /// Returns the amount of bytes per block of encoded DXTn data - fn encoded_bytes_per_block(self) -> usize { - match self { - DxtVariant::DXT1 => 8, - DxtVariant::DXT3 | DxtVariant::DXT5 => 16, - } - } - - /// Returns the color type that is stored in this DXT variant - pub(crate) fn color_type(self) -> ColorType { - match self { - DxtVariant::DXT1 => ColorType::Rgb8, - DxtVariant::DXT3 | DxtVariant::DXT5 => ColorType::Rgba8, - } - } -} - -/// DXT decoder -pub(crate) struct DxtDecoder { - inner: R, - width_blocks: u32, - height_blocks: u32, - variant: DxtVariant, - row: u32, -} - -impl DxtDecoder { - /// Create a new DXT decoder that decodes from the stream ```r```. - /// As DXT is often stored as raw buffers with the width/height - /// somewhere else the width and height of the image need - /// to be passed in ```width``` and ```height```, as well as the - /// DXT variant in ```variant```. - /// width and height are required to be powers of 2 and at least 4. - /// otherwise an error will be returned - pub(crate) fn new( - r: R, - width: u32, - height: u32, - variant: DxtVariant, - ) -> Result, ImageError> { - if width % 4 != 0 || height % 4 != 0 { - // TODO: this is actually a bit of a weird case. We could return `DecodingError` but - // it's not really the format that is wrong However, the encoder should surely return - // `EncodingError` so it would be the logical choice for symmetry. - return Err(ImageError::Parameter(ParameterError::from_kind( - ParameterErrorKind::DimensionMismatch, - ))); - } - let width_blocks = width / 4; - let height_blocks = height / 4; - Ok(DxtDecoder { - inner: r, - width_blocks, - height_blocks, - variant, - row: 0, - }) - } - - fn scanline_bytes(&self) -> u64 { - self.variant.decoded_bytes_per_block() as u64 * u64::from(self.width_blocks) - } - - fn read_scanline(&mut self, buf: &mut [u8]) -> io::Result { - assert_eq!( - u64::try_from(buf.len()), - Ok( - #[allow(deprecated)] - self.scanline_bytes() - ) - ); - - let len = self.variant.encoded_bytes_per_block() * self.width_blocks as usize; - let mut src = Vec::new(); - self.inner.read_exact_vec(&mut src, len)?; - - match self.variant { - DxtVariant::DXT1 => decode_dxt1_row(&src, buf), - DxtVariant::DXT3 => decode_dxt3_row(&src, buf), - DxtVariant::DXT5 => decode_dxt5_row(&src, buf), - } - self.row += 1; - Ok(buf.len()) - } -} - -// Note that, due to the way that DXT compression works, a scanline is considered to consist out of -// 4 lines of pixels. -impl ImageDecoder for DxtDecoder { - fn dimensions(&self) -> (u32, u32) { - (self.width_blocks * 4, self.height_blocks * 4) - } - - fn color_type(&self) -> ColorType { - self.variant.color_type() - } - - fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { - assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); - - #[allow(deprecated)] - for chunk in buf.chunks_mut(self.scanline_bytes().max(1) as usize) { - self.read_scanline(chunk)?; - } - Ok(()) - } - - fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { - (*self).read_image(buf) - } -} - -/** - * Actual encoding/decoding logic below. - */ -type Rgb = [u8; 3]; - -/// decodes a 5-bit R, 6-bit G, 5-bit B 16-bit packed color value into 8-bit RGB -/// mapping is done so min/max range values are preserved. So for 5-bit -/// values 0x00 -> 0x00 and 0x1F -> 0xFF -fn enc565_decode(value: u16) -> Rgb { - let red = (value >> 11) & 0x1F; - let green = (value >> 5) & 0x3F; - let blue = (value) & 0x1F; - [ - (red * 0xFF / 0x1F) as u8, - (green * 0xFF / 0x3F) as u8, - (blue * 0xFF / 0x1F) as u8, - ] -} - -/* - * Functions for decoding DXT compression - */ - -/// Constructs the DXT5 alpha lookup table from the two alpha entries -/// if alpha0 > alpha1, constructs a table of [a0, a1, 6 linearly interpolated values from a0 to a1] -/// if alpha0 <= alpha1, constructs a table of [a0, a1, 4 linearly interpolated values from a0 to a1, 0, 0xFF] -fn alpha_table_dxt5(alpha0: u8, alpha1: u8) -> [u8; 8] { - let mut table = [alpha0, alpha1, 0, 0, 0, 0, 0, 0xFF]; - if alpha0 > alpha1 { - for i in 2..8u16 { - table[i as usize] = - (((8 - i) * u16::from(alpha0) + (i - 1) * u16::from(alpha1)) / 7) as u8; - } - } else { - for i in 2..6u16 { - table[i as usize] = - (((6 - i) * u16::from(alpha0) + (i - 1) * u16::from(alpha1)) / 5) as u8; - } - } - table -} - -/// decodes an 8-byte dxt color block into the RGB channels of a 16xRGB or 16xRGBA block. -/// source should have a length of 8, dest a length of 48 (RGB) or 64 (RGBA) -fn decode_dxt_colors(source: &[u8], dest: &mut [u8], is_dxt1: bool) { - // sanity checks, also enable the compiler to elide all following bound checks - assert!(source.len() == 8 && (dest.len() == 48 || dest.len() == 64)); - // calculate pitch to store RGB values in dest (3 for RGB, 4 for RGBA) - let pitch = dest.len() / 16; - - // extract color data - let color0 = u16::from(source[0]) | (u16::from(source[1]) << 8); - let color1 = u16::from(source[2]) | (u16::from(source[3]) << 8); - let color_table = u32::from(source[4]) - | (u32::from(source[5]) << 8) - | (u32::from(source[6]) << 16) - | (u32::from(source[7]) << 24); - // let color_table = source[4..8].iter().rev().fold(0, |t, &b| (t << 8) | b as u32); - - // decode the colors to rgb format - let mut colors = [[0; 3]; 4]; - colors[0] = enc565_decode(color0); - colors[1] = enc565_decode(color1); - - // determine color interpolation method - if color0 > color1 || !is_dxt1 { - // linearly interpolate the other two color table entries - for i in 0..3 { - colors[2][i] = ((u16::from(colors[0][i]) * 2 + u16::from(colors[1][i]) + 1) / 3) as u8; - colors[3][i] = ((u16::from(colors[0][i]) + u16::from(colors[1][i]) * 2 + 1) / 3) as u8; - } - } else { - // linearly interpolate one other entry, keep the other at 0 - for i in 0..3 { - colors[2][i] = (u16::from(colors[0][i]) + u16::from(colors[1][i])).div_ceil(2) as u8; - } - } - - // serialize the result. Every color is determined by looking up - // two bits in color_table which identify which color to actually pick from the 4 possible colors - for i in 0..16 { - dest[i * pitch..i * pitch + 3] - .copy_from_slice(&colors[(color_table >> (i * 2)) as usize & 3]); - } -} - -/// Decodes a 16-byte bock of dxt5 data to a 16xRGBA block -fn decode_dxt5_block(source: &[u8], dest: &mut [u8]) { - assert!(source.len() == 16 && dest.len() == 64); - - // extract alpha index table (stored as little endian 64-bit value) - let alpha_table = source[2..8] - .iter() - .rev() - .fold(0, |t, &b| (t << 8) | u64::from(b)); - - // alpha level decode - let alphas = alpha_table_dxt5(source[0], source[1]); - - // serialize alpha - for i in 0..16 { - dest[i * 4 + 3] = alphas[(alpha_table >> (i * 3)) as usize & 7]; - } - - // handle colors - decode_dxt_colors(&source[8..16], dest, false); -} - -/// Decodes a 16-byte bock of dxt3 data to a 16xRGBA block -fn decode_dxt3_block(source: &[u8], dest: &mut [u8]) { - assert!(source.len() == 16 && dest.len() == 64); - - // extract alpha index table (stored as little endian 64-bit value) - let alpha_table = source[0..8] - .iter() - .rev() - .fold(0, |t, &b| (t << 8) | u64::from(b)); - - // serialize alpha (stored as 4-bit values) - for i in 0..16 { - dest[i * 4 + 3] = ((alpha_table >> (i * 4)) as u8 & 0xF) * 0x11; - } - - // handle colors - decode_dxt_colors(&source[8..16], dest, false); -} - -/// Decodes a 8-byte bock of dxt5 data to a 16xRGB block -fn decode_dxt1_block(source: &[u8], dest: &mut [u8]) { - assert!(source.len() == 8 && dest.len() == 48); - decode_dxt_colors(source, dest, true); -} - -/// Decode a row of DXT1 data to four rows of RGB data. -/// `source.len()` should be a multiple of 8, otherwise this panics. -fn decode_dxt1_row(source: &[u8], dest: &mut [u8]) { - assert!(source.len() % 8 == 0); - let block_count = source.len() / 8; - assert!(dest.len() >= block_count * 48); - - // contains the 16 decoded pixels per block - let mut decoded_block = [0u8; 48]; - - for (x, encoded_block) in source.chunks(8).enumerate() { - decode_dxt1_block(encoded_block, &mut decoded_block); - - // copy the values from the decoded block to linewise RGB layout - for line in 0..4 { - let offset = (block_count * line + x) * 12; - dest[offset..offset + 12].copy_from_slice(&decoded_block[line * 12..(line + 1) * 12]); - } - } -} - -/// Decode a row of DXT3 data to four rows of RGBA data. -/// `source.len()` should be a multiple of 16, otherwise this panics. -fn decode_dxt3_row(source: &[u8], dest: &mut [u8]) { - assert!(source.len() % 16 == 0); - let block_count = source.len() / 16; - assert!(dest.len() >= block_count * 64); - - // contains the 16 decoded pixels per block - let mut decoded_block = [0u8; 64]; - - for (x, encoded_block) in source.chunks(16).enumerate() { - decode_dxt3_block(encoded_block, &mut decoded_block); - - // copy the values from the decoded block to linewise RGB layout - for line in 0..4 { - let offset = (block_count * line + x) * 16; - dest[offset..offset + 16].copy_from_slice(&decoded_block[line * 16..(line + 1) * 16]); - } - } -} - -/// Decode a row of DXT5 data to four rows of RGBA data. -/// `source.len()` should be a multiple of 16, otherwise this panics. -fn decode_dxt5_row(source: &[u8], dest: &mut [u8]) { - assert!(source.len() % 16 == 0); - let block_count = source.len() / 16; - assert!(dest.len() >= block_count * 64); - - // contains the 16 decoded pixels per block - let mut decoded_block = [0u8; 64]; - - for (x, encoded_block) in source.chunks(16).enumerate() { - decode_dxt5_block(encoded_block, &mut decoded_block); - - // copy the values from the decoded block to linewise RGB layout - for line in 0..4 { - let offset = (block_count * line + x) * 16; - dest[offset..offset + 16].copy_from_slice(&decoded_block[line * 16..(line + 1) * 16]); - } - } -} diff --git a/src/io/format.rs b/src/io/format.rs index 5844b064dc..48487a537f 100644 --- a/src/io/format.rs +++ b/src/io/format.rs @@ -234,7 +234,7 @@ impl ImageFormat { ImageFormat::WebP => true, ImageFormat::Tiff => true, ImageFormat::Tga => true, - ImageFormat::Dds => false, + ImageFormat::Dds => true, ImageFormat::Bmp => true, ImageFormat::Ico => true, ImageFormat::Hdr => true, @@ -266,7 +266,7 @@ impl ImageFormat { ImageFormat::WebP => true, ImageFormat::Hdr => true, ImageFormat::OpenExr => true, - ImageFormat::Dds => false, + ImageFormat::Dds => true, ImageFormat::Qoi => true, ImageFormat::Pcx => false, } @@ -324,7 +324,7 @@ impl ImageFormat { ImageFormat::Avif => cfg!(feature = "avif"), ImageFormat::Qoi => cfg!(feature = "qoi"), ImageFormat::Pcx => cfg!(feature = "pcx"), - ImageFormat::Dds => false, + ImageFormat::Dds => cfg!(feature = "dds"), } } @@ -348,7 +348,7 @@ impl ImageFormat { ImageFormat::Qoi => cfg!(feature = "qoi"), ImageFormat::Hdr => cfg!(feature = "hdr"), ImageFormat::Pcx => false, - ImageFormat::Dds => false, + ImageFormat::Dds => cfg!(feature = "dds"), } } @@ -448,7 +448,7 @@ mod tests { cfg!(feature = "ff"), ImageFormat::Farbfeld.reading_enabled() ); - assert!(!ImageFormat::Dds.reading_enabled()); + assert_eq!(cfg!(feature = "dds"), ImageFormat::Dds.reading_enabled()); } #[test] @@ -458,6 +458,6 @@ mod tests { cfg!(feature = "ff"), ImageFormat::Farbfeld.writing_enabled() ); - assert!(!ImageFormat::Dds.writing_enabled()); + assert_eq!(cfg!(feature = "dds"), ImageFormat::Dds.writing_enabled()); } } diff --git a/src/io/free_functions.rs b/src/io/free_functions.rs index b5c6d665f4..c334d6d64c 100644 --- a/src/io/free_functions.rs +++ b/src/io/free_functions.rs @@ -78,6 +78,8 @@ pub(crate) fn encoder_for_format<'a, W: Write + Seek>( ImageFormat::Farbfeld => Box::new(farbfeld::FarbfeldEncoder::new(buffered_write)), #[cfg(feature = "tga")] ImageFormat::Tga => Box::new(tga::TgaEncoder::new(buffered_write)), + #[cfg(feature = "dds")] + ImageFormat::Dds => Box::new(dds::DdsEncoder::new(buffered_write)), #[cfg(feature = "exr")] ImageFormat::OpenExr => Box::new(openexr::OpenExrEncoder::new(buffered_write)), #[cfg(feature = "tiff")] diff --git a/src/lib.rs b/src/lib.rs index 7e94d1abd6..a0f4dbdf54 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -213,7 +213,7 @@ pub use crate::images::flat; /// | -------- | ----------------------------------------- | --------------------------------------- | /// | AVIF | Yes \* | Yes (lossy only) | /// | BMP | Yes | Yes | -/// | DDS | Yes | --- | +/// | DDS | Yes | Yes \*\* | /// | Farbfeld | Yes | Yes | /// | GIF | Yes | Yes | /// | HDR | Yes | Yes | @@ -228,6 +228,8 @@ pub use crate::images::flat; /// | WebP | Yes | Yes (lossless only) | /// /// - \* Requires the `avif-native` feature, uses the libdav1d C library. +/// - \*\* See the documentation of the [`dds` crate](https://github.com/image-rs/image-dds) for a +/// full list of supported formats. /// /// ## A note on format specific features /// @@ -283,9 +285,6 @@ pub mod codecs { pub mod tiff; #[cfg(feature = "webp")] pub mod webp; - - #[cfg(feature = "dds")] - mod dxt; } mod animation; diff --git a/tests/images/dds/cubemap/DXT1 Pretoria Gardens DX9.dds b/tests/images/dds/cubemap/DXT1 Pretoria Gardens DX9.dds new file mode 100644 index 0000000000..43c0b099c0 Binary files /dev/null and b/tests/images/dds/cubemap/DXT1 Pretoria Gardens DX9.dds differ diff --git a/tests/images/dds/images/ASTC 6x6.dds b/tests/images/dds/images/ASTC 6x6.dds new file mode 100644 index 0000000000..655adf3e94 Binary files /dev/null and b/tests/images/dds/images/ASTC 6x6.dds differ diff --git a/tests/images/dds/images/DX10 BC7_UNORM.dds b/tests/images/dds/images/DX10 BC7_UNORM.dds new file mode 100644 index 0000000000..4a077335c9 Binary files /dev/null and b/tests/images/dds/images/DX10 BC7_UNORM.dds differ diff --git a/tests/images/dds/images/DX10 R1_UNORM.dds b/tests/images/dds/images/DX10 R1_UNORM.dds new file mode 100644 index 0000000000..2467d56c87 Binary files /dev/null and b/tests/images/dds/images/DX10 R1_UNORM.dds differ diff --git a/tests/images/dds/images/DX10 R8_SNORM.dds b/tests/images/dds/images/DX10 R8_SNORM.dds new file mode 100644 index 0000000000..71767e9567 Binary files /dev/null and b/tests/images/dds/images/DX10 R8_SNORM.dds differ diff --git a/tests/images/dds/images/DX9 B4G4R4A4_UNORM.dds b/tests/images/dds/images/DX9 B4G4R4A4_UNORM.dds new file mode 100644 index 0000000000..39111e67ce Binary files /dev/null and b/tests/images/dds/images/DX9 B4G4R4A4_UNORM.dds differ diff --git a/tests/images/dds/images/DX9 BC1_UNORM.dds b/tests/images/dds/images/DX9 BC1_UNORM.dds new file mode 100644 index 0000000000..748948a01f Binary files /dev/null and b/tests/images/dds/images/DX9 BC1_UNORM.dds differ diff --git a/tests/reference/dds/cubemap/DXT1 Pretoria Gardens DX9.dds.ee1c861a.png b/tests/reference/dds/cubemap/DXT1 Pretoria Gardens DX9.dds.ee1c861a.png new file mode 100644 index 0000000000..524e7e99ed Binary files /dev/null and b/tests/reference/dds/cubemap/DXT1 Pretoria Gardens DX9.dds.ee1c861a.png differ diff --git a/tests/reference/dds/images/ASTC 6x6.dds.12559dbc.png b/tests/reference/dds/images/ASTC 6x6.dds.12559dbc.png new file mode 100644 index 0000000000..db3bbe6426 Binary files /dev/null and b/tests/reference/dds/images/ASTC 6x6.dds.12559dbc.png differ diff --git a/tests/reference/dds/images/DX10 BC7_UNORM.dds.c9f03970.png b/tests/reference/dds/images/DX10 BC7_UNORM.dds.c9f03970.png new file mode 100644 index 0000000000..2153465324 Binary files /dev/null and b/tests/reference/dds/images/DX10 BC7_UNORM.dds.c9f03970.png differ diff --git a/tests/reference/dds/images/DX10 R1_UNORM.dds.c72e7587.png b/tests/reference/dds/images/DX10 R1_UNORM.dds.c72e7587.png new file mode 100644 index 0000000000..5ae7bada0b Binary files /dev/null and b/tests/reference/dds/images/DX10 R1_UNORM.dds.c72e7587.png differ diff --git a/tests/reference/dds/images/DX10 R8_SNORM.dds.f600b35b.png b/tests/reference/dds/images/DX10 R8_SNORM.dds.f600b35b.png new file mode 100644 index 0000000000..1086cd591b Binary files /dev/null and b/tests/reference/dds/images/DX10 R8_SNORM.dds.f600b35b.png differ diff --git a/tests/reference/dds/images/DX9 B4G4R4A4_UNORM.dds.1416aa1f.png b/tests/reference/dds/images/DX9 B4G4R4A4_UNORM.dds.1416aa1f.png new file mode 100644 index 0000000000..3ef1d9335a Binary files /dev/null and b/tests/reference/dds/images/DX9 B4G4R4A4_UNORM.dds.1416aa1f.png differ diff --git a/tests/reference/dds/images/DX9 BC1_UNORM.dds.824ac5cc.png b/tests/reference/dds/images/DX9 BC1_UNORM.dds.824ac5cc.png new file mode 100644 index 0000000000..2ad71889fd Binary files /dev/null and b/tests/reference/dds/images/DX9 BC1_UNORM.dds.824ac5cc.png differ diff --git a/tests/reference_images.rs b/tests/reference_images.rs index a6ff963aa0..26d2185a51 100644 --- a/tests/reference_images.rs +++ b/tests/reference_images.rs @@ -18,7 +18,7 @@ where { let base: PathBuf = BASE_PATH.iter().collect(); let decoders = &[ - "tga", "tiff", "png", "gif", "bmp", "ico", "hdr", "pbm", "webp", "pcx", + "tga", "tiff", "png", "gif", "bmp", "ico", "hdr", "pbm", "webp", "pcx", "dds", ]; for decoder in decoders { let mut path = base.clone(); diff --git a/tests/truncate_images.rs b/tests/truncate_images.rs index 43a44b4a25..57d8f1b0b4 100644 --- a/tests/truncate_images.rs +++ b/tests/truncate_images.rs @@ -16,7 +16,7 @@ where { let base: PathBuf = BASE_PATH.iter().collect(); let decoders = &[ - "tga", "tiff", "png", "gif", "bmp", "ico", "jpg", "hdr", "farbfeld", "exr", "pcx", + "tga", "tiff", "png", "gif", "bmp", "ico", "jpg", "hdr", "farbfeld", "exr", "pcx", "dds", ]; for decoder in decoders { let mut path = base.clone(); @@ -105,3 +105,8 @@ fn truncate_exr() { fn truncate_pcx() { truncate_images("pcx"); } + +#[test] +fn truncate_dds() { + truncate_images("dds"); +}