Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ publish = false
include = ["src", "tests/reference.rs"]

[features]
default = ["ora", "otb", "pcx", "sgi", "xbm", "xpm", "wbmp"]
default = ["dds", "ora", "otb", "pcx", "sgi", "xbm", "xpm", "wbmp"]

# Each feature below is the file extension for the file format encoder/decoder
# it enables.
dds = ["dep:dds"]
ora = ["image/png", "dep:zip", "dep:ouroboros"]
otb = []
sgi = []
Expand All @@ -26,6 +27,7 @@ xpm = []
image = { version = "0.25.8", default-features = false }

# Optional dependencies
dds = { version = "0.2.0", optional = true }
ouroboros = { version = "0.18.5", optional = true }
pcx = { version = "0.2.4", optional = true }
wbmp = { version = "0.1.2", optional = true }
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ image_extras = { version = "0.1", features = ["pcx"], default-features = false }

| Feature | Format
| ------- | ------
| `dds` | DirectDraw Surface [\[spec\]](https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dx-graphics-dds-pguide)
| `ora` | OpenRaster [\[spec\]](https://www.openraster.org/)
| `otb` | OTA Bitmap (Over The Air Bitmap) [\[spec\]](https://www.wapforum.org/what/technical/SPEC-WAESpec-19990524.pdf)
| `pcx` | PCX (ZSoft Paintbrush bitmap/PiCture eXchange) [\[desc\]](https://en.wikipedia.org/wiki/PCX#PCX_file_format)
Expand Down
295 changes: 295 additions & 0 deletions src/dds.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
//! Decoding and encoding DDS images
//!
//! DDS (DirectDraw Surface) is a container format for storing uncompressed and
//! BCn/DXT (S3TC) compressed images for use in graphics applications.
//!
//! # Related Links
//!
//! * <https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dx-graphics-dds-pguide> - Description of the DDS format.
//! * <https://microsoft.github.io/DirectX-Specs/d3d/archive/D3D11_3_FunctionalSpec.htm> - Direct3D 11.3 Functional Specification

use std::io::{Read, Seek};

use dds::header::ParseOptions;
use dds::{
Channels, ColorFormat, DataLayout, Decoder, Format, ImageViewMut, Offset, Size,
TextureArrayKind,
};

use image::error::{
DecodingError, ImageError, ImageFormatHint, ImageResult, LimitError, LimitErrorKind,
UnsupportedError, UnsupportedErrorKind,
};
use image::{ColorType, ExtendedColorType, ImageDecoder, ImageDecoderRect};

/// 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<R> {
inner: Decoder<R>,
is_cubemap: bool,
size: Size,
color: SupportedColor,
}

impl<R: Read> DdsDecoder<R> {
/// Create a new decoder that decodes from the stream `r`
pub fn new(r: R) -> ImageResult<Self> {
let options = ParseOptions::new_permissive(None);
let decoder = Decoder::new_with_options(r, &options).map_err(to_image_error)?;
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 {
DataLayout::Volume(_) => {
return Err(ImageError::Decoding(DecodingError::new(
format_hint(),
"DDS volume textures are not supported for decoding",
)))
}
DataLayout::TextureArray(texture_array) => {
let supported_length = match texture_array.kind() {
TextureArrayKind::Textures => 1,
TextureArrayKind::CubeMaps => 6,
TextureArrayKind::PartialCubeMap(cube_map_faces) => cube_map_faces.count(),
};
if texture_array.len() != supported_length as usize {
return Err(ImageError::Decoding(DecodingError::new(
format_hint(),
"DDS texture arrays are not supported for decoding",
)));
}
}
DataLayout::Texture(_) => {}
}

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(
format_hint(),
"DDS cube map faces are too large to decode",
)));
}
}

Ok(DdsDecoder {
inner: decoder,
is_cubemap,
size,
color: SupportedColor::from_dds_widen(color),
})
}

/// 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.
///
/// # Errors
///
/// Currently, [`ColorType::La8`] and [`ColorType::La16`] are not supported
/// for decoding DDS files. If these color types (or other unsupported types)
/// are provided, this function will return [`ImageError::Unsupported`] with
/// [`UnsupportedErrorKind::Color`].
pub fn set_color_type(&mut self, color: ColorType) -> ImageResult<()> {
let Some(supported_color) = SupportedColor::from_image_exact(color) else {
return Err(ImageError::Unsupported(
UnsupportedError::from_format_and_kind(
format_hint(),
UnsupportedErrorKind::Color(color.into()),
),
));
};
self.color = supported_color;
Ok(())
}
}

impl<R: Read + Seek> ImageDecoder for DdsDecoder<R> {
fn dimensions(&self) -> (u32, u32) {
(self.size.width, self.size.height)
}

fn color_type(&self) -> ColorType {
self.color.image
}

fn original_color_type(&self) -> ExtendedColorType {
match self.inner.format() {
Format::R1_UNORM => ExtendedColorType::L1,
Format::B4G4R4A4_UNORM | Format::A4B4G4R4_UNORM => ExtendedColorType::Rgba4,
Format::A8_UNORM => ExtendedColorType::A8,
_ => SupportedColor::from_dds_widen(self.inner.native_color())
.image
.into(),
}
}

fn set_limits(&mut self, limits: image::Limits) -> ImageResult<()> {
limits.check_dimensions(self.size.width, self.size.height)?;

if let Some(max_alloc) = limits.max_alloc {
self.inner.options.memory_limit = max_alloc.try_into().unwrap_or(usize::MAX);
}

Ok(())
}

#[track_caller]
fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> {
let color = self.color.dds;
let size = self.size;

assert_eq!(
buf.len(),
color.buffer_size(size).unwrap(),
"Buffer len does not match for {:?} and {:?}",
size,
color
);

let image = ImageViewMut::new(buf, size, color).expect("Invalid buffer length");

if self.is_cubemap {
self.inner.read_cube_map(image).map_err(to_image_error)?;
} else {
self.inner.read_surface(image).map_err(to_image_error)?;
}

Ok(())
}

fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> {
(*self).read_image(buf)
}
}

impl<R: Read + Seek> ImageDecoderRect for DdsDecoder<R> {
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(
format_hint(),
"read_rect is not supported for DDS cubemaps",
)));
}

let Some(view) =
ImageViewMut::new_with(buf, row_pitch, Size::new(width, height), self.color.dds)
else {
return Err(ImageError::Decoding(DecodingError::new(
format_hint(),
"Invalid buffer length for reading rect",
)));
};

self.inner
.read_surface_rect(view, Offset::new(x, y))
.map_err(to_image_error)?;
self.inner
.rewind_to_previous_surface()
.map_err(to_image_error)?;

Ok(())
}
}

/// A color type supported by both the `image` and `dds` crates.
struct SupportedColor {
image: ColorType,
dds: ColorFormat,
}
impl SupportedColor {
fn new(image: ColorType, dds: ColorFormat) -> SupportedColor {
debug_assert_eq!(image.bytes_per_pixel(), dds.bytes_per_pixel());
debug_assert_eq!(image.channel_count(), dds.channels.count());

SupportedColor { image, dds }
}

/// Returns a supported color format that exactly matches the given `image`
/// color format.
fn from_image_exact(color: ColorType) -> Option<Self> {
fn to_color_format_exact(color: ColorType) -> Option<ColorFormat> {
match color {
ColorType::L8 => Some(ColorFormat::GRAYSCALE_U8),
ColorType::Rgb8 => Some(ColorFormat::RGB_U8),
ColorType::Rgba8 => Some(ColorFormat::RGBA_U8),
ColorType::L16 => Some(ColorFormat::GRAYSCALE_U16),
ColorType::Rgb16 => Some(ColorFormat::RGB_U16),
ColorType::Rgba16 => Some(ColorFormat::RGBA_U16),
ColorType::Rgb32F => Some(ColorFormat::RGB_F32),
ColorType::Rgba32F => Some(ColorFormat::RGBA_F32),
_ => None,
}
}

to_color_format_exact(color).map(|dds| Self::new(color, dds))
}
/// Returns a supported color format that is the narrowest superset of the
/// given `image` color format.
fn from_dds_widen(color: ColorFormat) -> Self {
match color {
// exact
ColorFormat::RGB_U8 => Self::new(ColorType::Rgb8, color),
ColorFormat::RGB_U16 => Self::new(ColorType::Rgb16, color),
ColorFormat::RGB_F32 => Self::new(ColorType::Rgb32F, color),
ColorFormat::GRAYSCALE_U8 => Self::new(ColorType::L8, color),
ColorFormat::GRAYSCALE_U16 => Self::new(ColorType::L16, color),
ColorFormat::RGBA_U8 => Self::new(ColorType::Rgba8, color),
ColorFormat::RGBA_U16 => Self::new(ColorType::Rgba16, color),
ColorFormat::RGBA_F32 => Self::new(ColorType::Rgba32F, color),
// widen
ColorFormat::ALPHA_U8 => Self::new(ColorType::Rgba8, ColorFormat::RGBA_U8),
ColorFormat::ALPHA_U16 => Self::new(ColorType::Rgba16, ColorFormat::RGBA_U16),
ColorFormat::ALPHA_F32 => Self::new(ColorType::Rgba32F, ColorFormat::RGBA_F32),
ColorFormat::GRAYSCALE_F32 => Self::new(ColorType::Rgb32F, ColorFormat::RGB_F32),
}
}
}

fn to_image_error(e: dds::DecodingError) -> ImageError {
match e {
dds::DecodingError::Io(e) => ImageError::IoError(e),
dds::DecodingError::MemoryLimitExceeded => {
ImageError::Limits(LimitError::from_kind(LimitErrorKind::InsufficientMemory))
}
_ => ImageError::Decoding(DecodingError::new(format_hint(), e)),
}
}

fn format_hint() -> ImageFormatHint {
ImageFormatHint::Name("DDS".into())
}
11 changes: 11 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
#![forbid(unsafe_code)]

#[cfg(feature = "dds")]
pub mod dds;

#[cfg(feature = "ora")]
pub mod ora;

Expand Down Expand Up @@ -46,6 +49,14 @@ static REGISTER: std::sync::Once = std::sync::Once::new();
/// effect.
pub fn register() {
REGISTER.call_once(|| {
#[cfg(feature = "dds")]
if register_decoding_hook(
"dds".into(),
Box::new(|r| Ok(Box::new(dds::DdsDecoder::new(r)?))),
) {
register_format_detection_hook("dds".into(), b"DDS ", None);
}

// OpenRaster images are ZIP files and have no simple signature to distinguish them
// from ZIP files containing other content
#[cfg(feature = "ora")]
Expand Down
Binary file added tests/images/dds/A8_UNORM.dds
Binary file not shown.
Binary file added tests/images/dds/A8_UNORM.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/images/dds/ASTC 6x6.dds
Binary file not shown.
Binary file added tests/images/dds/ASTC 6x6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/images/dds/B4G4R4A4_UNORM.dds
Binary file not shown.
Binary file added tests/images/dds/B4G4R4A4_UNORM.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/images/dds/B5G6R5_UNORM.dds
Binary file not shown.
Binary file added tests/images/dds/B5G6R5_UNORM.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/images/dds/BC1_UNORM.dds
Binary file not shown.
Binary file added tests/images/dds/BC1_UNORM.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/images/dds/BC1_UNORM_SRGB.dds
Binary file not shown.
Binary file added tests/images/dds/BC1_UNORM_SRGB.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/images/dds/BC4_UNORM.dds
Binary file not shown.
Binary file added tests/images/dds/BC4_UNORM.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/images/dds/BC6H_SF16.dds
Binary file not shown.
Binary file added tests/images/dds/BC6H_SF16.tiff
Binary file not shown.
Binary file added tests/images/dds/BC6H_UF16.dds
Binary file not shown.
Binary file added tests/images/dds/BC6H_UF16.tiff
Binary file not shown.
Binary file added tests/images/dds/BC7_UNORM.dds
Binary file not shown.
Binary file added tests/images/dds/BC7_UNORM.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/images/dds/R1_UNORM.dds
Binary file not shown.
Binary file added tests/images/dds/R1_UNORM.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/images/dds/R9G9B9E5_SHAREDEXP.dds
Binary file not shown.
Binary file added tests/images/dds/R9G9B9E5_SHAREDEXP.tiff
Binary file not shown.
Binary file added tests/images/dds/cubemap.dds
Binary file not shown.
Binary file added tests/images/dds/cubemap.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading