diff --git a/benches/encode.rs b/benches/encode.rs index 7695b69..44939b5 100644 --- a/benches/encode.rs +++ b/benches/encode.rs @@ -1,7 +1,7 @@ #![allow(unused)] use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use dds::{header::*, *}; +use dds::*; use rand::Rng; struct Image { @@ -100,8 +100,8 @@ where let image = black_box(image); - let header = Header::new_image(image.size.width, image.size.height, format); - let mut encoder = Encoder::new(black_box(&mut output), format, &header).unwrap(); + let mut encoder = + Encoder::new_image(black_box(&mut output), image.size, format, false).unwrap(); encoder.encoding = black_box(options).clone(); let result = encoder.write_surface(black_box(image.view())); black_box(result).unwrap(); @@ -314,11 +314,9 @@ pub fn generate_mipmaps(c: &mut Criterion) { let image = black_box(image); - let header = - Header::new_image(image.width(), image.height(), format).with_mipmaps(); let mut encoder = - Encoder::new(black_box(&mut output), format, &header).unwrap(); - encoder.mipmaps.generate = true; // enable mipmap generation for this test + Encoder::new_image(black_box(&mut output), image.size(), format, true) + .unwrap(); encoder.mipmaps.resize_filter = filter; let result = encoder.write_surface(image); black_box(result).unwrap(); diff --git a/src/encoder.rs b/src/encoder.rs index 7b28943..2a7ae68 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -25,7 +25,8 @@ pub struct Encoder { pub encoding: EncodeOptions, /// Options regarding automatic mipmap generation. /// - /// Set `self.mipmaps.generate = true` to enable automatic mipmap generation. + /// Set `self.mipmaps.generate = false` to disable automatic mipmap + /// generation. /// /// Default: `MipmapOptions::default()` pub mipmaps: MipmapOptions, @@ -67,6 +68,31 @@ impl Encoder { }) } + /// Creates a new encoder for a single image with the given size and format. + /// + /// If `mipmaps` is `true`, a full mipmap chain will be declared in the + /// header. + /// + /// The header is created using [`Header::new_image`] and immediately + /// written to the writer. For more control over the header, use + /// [`Encoder::new`]. + pub fn new_image( + writer: W, + size: Size, + format: Format, + mipmaps: bool, + ) -> Result + where + W: Write, + { + let mut header = Header::new_image(size.width, size.height, format); + if mipmaps { + header = header.with_mipmaps(); + } + + Self::new(writer, format, &header) + } + /// The format of the pixel data. pub fn format(&self) -> Format { self.format @@ -296,7 +322,7 @@ pub struct MipmapOptions { /// will **NOT** result in an error. Instead, the encoder will silently /// ignore the option and assume mipmap generation is disabled. /// - /// Default: `false` + /// Default: `true` pub generate: bool, /// Whether the alpha channel (if any) is straight alpha transparency. /// @@ -323,7 +349,7 @@ pub struct MipmapOptions { impl Default for MipmapOptions { fn default() -> Self { Self { - generate: false, + generate: true, resize_straight_alpha: true, resize_filter: ResizeFilter::Box, } diff --git a/src/lib.rs b/src/lib.rs index 867d981..132dee0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,12 +65,12 @@ //! //! ### Encoding //! -//! Since the data of a DDS file is determined by the header, the first step to -//! encoding a DDS file is to create a header. See the documentation of -//! the [`dds::header`](crate::header) module for more details. +//! [`Encoder`] defines the high-level interface for encoding DDS images. +//! +//! A single texture can be encoded as follows: //! //! ```no_run -//! use dds::{*, header::*}; +//! use dds::*; //! use std::fs::File; //! //! fn save_rgba_image( @@ -78,14 +78,14 @@ //! image_data: &[u8], //! width: u32, //! height: u32, +//! mipmaps: bool, //! ) -> Result<(), EncodingError> { -//! let format = Format::BC1_UNORM; -//! let header = Header::new_image(width, height, format); -//! -//! let mut encoder = Encoder::new(file, format, &header)?; +//! let size = Size::new(width, height); +//! let mut encoder = Encoder::new_image(file, size, Format::BC7_UNORM, mipmaps)?; +//! // lower quality for faster encoding //! encoder.encoding.quality = CompressionQuality::Fast; //! -//! let view = ImageView::new(image_data, Size::new(width, height), ColorFormat::RGBA_U8) +//! let view = ImageView::new(image_data, size, ColorFormat::RGBA_U8) //! .expect("invalid image data"); //! encoder.write_surface(view)?; //! encoder.finish()?; @@ -93,54 +93,36 @@ //! } //! ``` //! -//! Note the use of [`Encoder::finish()`]. This method will verify that the -//! file has been created correctly and contains all necessary data. Always -//! use [`Encoder::finish()`] instead of dropping the encoder. -//! -//! To create DDS files with mipmaps, we simply create a header with mipmaps and -//! enable automatic mipmap generation in the encoder: +//! Mipmaps are automatically generated if requested (by default). How mipmaps +//! are generated can be configured using [`Encoder::mipmaps`]. //! -//! ```no_run -//! use dds::{*, header::*}; -//! use std::fs::File; +//! Most formats also support encoding options to change the encoded image data +//! in some way. These can be set using the [`Encoder::encoding`] field. E.g. +//! most formats support [dithering](EncodeOptions::dithering) and compressed +//! formats support different [quality levels](EncodeOptions::quality) among +//! others. //! -//! fn save_rgba_image_with_mipmaps( -//! file: &mut File, -//! image_data: &[u8], -//! width: u32, -//! height: u32, -//! ) -> Result<(), EncodingError> { -//! let format = Format::BC1_UNORM; -//! // Create a header with mipmaps -//! let header = Header::new_image(width, height, format).with_mipmaps(); -//! -//! let mut encoder = Encoder::new(file, format, &header)?; -//! encoder.encoding.quality = CompressionQuality::Fast; -//! encoder.mipmaps.generate = true; // Enable automatic mipmap generation -//! -//! let view = ImageView::new(image_data, Size::new(width, height), ColorFormat::RGBA_U8) -//! .expect("invalid image data"); -//! encoder.write_surface(view)?; -//! encoder.finish()?; -//! Ok(()) -//! } -//! ``` +//! Also note the use of [`Encoder::finish()`]. This method verifies that the +//! DDS file contains all necessary data and is valid. ALWAYS use +//! [`Encoder::finish()`] instead of dropping the encoder. //! -//! Note: If the header does not specify mipmaps, no mipmaps will be generated -//! even if automatic mipmap generation is enabled. +//! #### Texture arrays, cube maps, and volumes //! -//! For other types of data: +//! Other types of data work slightly differently. You need to create a +//! [`Header`](crate::header::Header) for them manually and pass it to +//! [`Encoder::new`]. After the encoder has been created, the process is +//! similar: //! //! - Texture arrays can be encoded using [`Encoder::write_surface`] for each //! texture in the array. //! - Cube maps, like texture arrays, can be encoded using [`Encoder::write_surface`] //! for each face. The order of the faces must be +X, -X, +Y, -Y, +Z, -Z. -//! Writing whole cube maps at once is not supported. //! - Volumes can be encoded one depth slice at a time using //! [`Encoder::write_surface`]. //! -//! Automatic mipmap generation is **not** supported for volumes. If enabled, -//! the options will be silently ignored and no mipmaps will be generated. +//! Automatic mipmap generation is **not** supported for volumes. If automatic +//! mipmap generation is enabled, it will silently do nothing. Use +//! [`Encoder::finish()`] to prevent the creation of invalid DDS files. //! //! ### Progress reporting //! diff --git a/tests/encode.rs b/tests/encode.rs index 8376edd..cebf546 100644 --- a/tests/encode.rs +++ b/tests/encode.rs @@ -1,4 +1,4 @@ -use dds::{header::*, *}; +use dds::*; use rand::{Rng, RngCore}; use std::path::{Path, PathBuf}; @@ -59,12 +59,7 @@ fn encode_decode( ) -> (Vec, Image) { // encode let mut encoded = Vec::new(); - let mut encoder = Encoder::new( - &mut encoded, - format, - &Header::new_image(image.size.width, image.size.height, format), - ) - .unwrap(); + let mut encoder = Encoder::new_image(&mut encoded, image.size, format, false).unwrap(); encoder.encoding = options.clone(); encoder.write_surface(image.view()).unwrap(); encoder.finish().unwrap(); @@ -121,11 +116,7 @@ fn encode_base() { }; let mut output = Vec::new(); - let mut encoder = Encoder::new( - &mut output, - format, - &Header::new_image(size.width, size.height, format), - )?; + let mut encoder = Encoder::new_image(&mut output, size, format, false)?; encoder.encoding.quality = CompressionQuality::High; encoder.encoding.parallel = false; @@ -169,11 +160,7 @@ fn encode_dither() { dds_path: &Path, ) -> Result> { let mut output = Vec::new(); - let mut encoder = Encoder::new( - &mut output, - format, - &Header::new_image(image.size.width, image.size.height, format), - )?; + let mut encoder = Encoder::new_image(&mut output, image.size, format, false)?; encoder.encoding.quality = CompressionQuality::High; encoder.encoding.dithering = Dithering::ColorAndAlpha; encoder.write_surface(image.view())?; @@ -733,11 +720,7 @@ fn encode_mipmap() { let height = image.height(); let mut encoded = Vec::new(); - let mut encoder = Encoder::new( - &mut encoded, - format, - &Header::new_image(width, height, format).with_mipmaps(), - )?; + let mut encoder = Encoder::new_image(&mut encoded, image.size(), format, true)?; encoder.mipmaps = mipmaps; encoder.write_surface(image)?; encoder.finish()?; @@ -863,33 +846,17 @@ fn test_unaligned() { let aligned = ImageView::new(aligned, size, color).unwrap(); let unaligned = ImageView::new(unaligned, size, color).unwrap(); - for mipmaps in [ - MipmapOptions { - generate: false, - ..MipmapOptions::default() - }, - MipmapOptions { - generate: true, - ..MipmapOptions::default() - }, - ] { + for mipmaps in [false, true] { aligned_encoded.clear(); unaligned_encoded.clear(); - let mut header = Header::new_image(size.width, size.height, format); - if mipmaps.generate { - header = header.with_mipmaps(); - } - let mut aligned_encoder = - Encoder::new(&mut aligned_encoded, format, &header).unwrap(); - aligned_encoder.mipmaps = mipmaps; + Encoder::new_image(&mut aligned_encoded, size, format, mipmaps).unwrap(); aligned_encoder.write_surface(aligned).unwrap(); aligned_encoder.finish().unwrap(); let mut unaligned_encoder = - Encoder::new(&mut unaligned_encoded, format, &header).unwrap(); - unaligned_encoder.mipmaps = mipmaps; + Encoder::new_image(&mut unaligned_encoded, size, format, mipmaps).unwrap(); unaligned_encoder.write_surface(unaligned).unwrap(); unaligned_encoder.finish().unwrap(); @@ -950,23 +917,19 @@ fn test_row_pitch() { let image = image.cropped(Offset::ZERO, size); let cont_image = cont_image.cropped(Offset::ZERO, size); - let mut header = Header::new_image(size.width, size.height, format); - if encoding.size_multiple().is_none() { - // size multiple make mipmaps difficult, so only do them for - // formats that support images of any size. - header = header.with_mipmaps(); - } + // size multiple make mipmaps difficult, so only do them for + // formats that support images of any size. + let mipmaps = encoding.size_multiple().is_none(); let mut cont_encoded = Vec::new(); - let mut cont_encoder = Encoder::new(&mut cont_encoded, format, &header).unwrap(); - cont_encoder.mipmaps.generate = true; + let mut cont_encoder = + Encoder::new_image(&mut cont_encoded, size, format, mipmaps).unwrap(); cont_encoder.write_surface(cont_image).unwrap(); cont_encoder.finish().unwrap(); let mut non_cont_encoded = Vec::new(); let mut non_cont_encoder = - Encoder::new(&mut non_cont_encoded, format, &header).unwrap(); - non_cont_encoder.mipmaps.generate = true; + Encoder::new_image(&mut non_cont_encoded, size, format, mipmaps).unwrap(); non_cont_encoder.write_surface(image).unwrap(); non_cont_encoder.finish().unwrap(); @@ -985,10 +948,11 @@ mod errors { #[test] fn unsupported_format() { - let result = Encoder::new( + let result = Encoder::new_image( std::io::sink(), + Size::new(1, 1), Format::ASTC_10X10_UNORM, - &Header::new_image(1, 1, Format::ASTC_10X10_UNORM), + false, ); assert!(result.is_err()); let err = result.err().unwrap(); @@ -1006,12 +970,8 @@ mod errors { fn size_multiple() { // size is required to be multiple of a certain number // NV12 requires 2x2 - let mut encoder = Encoder::new( - std::io::sink(), - Format::NV12, - &Header::new_image(5, 5, Format::NV12), - ) - .unwrap(); + let mut encoder = + Encoder::new_image(std::io::sink(), Size::new(5, 5), Format::NV12, false).unwrap(); let image = util::Image::::new_empty(Channels::Rgb, Size::new(5, 5)); let result = encoder.write_surface(image.view()); @@ -1023,12 +983,8 @@ mod errors { #[test] fn wrong_surface_size() { - let mut encoder = Encoder::new( - std::io::sink(), - Format::R8_UNORM, - &Header::new_image(8, 8, Format::R8_UNORM), - ) - .unwrap(); + let mut encoder = + Encoder::new_image(std::io::sink(), Size::new(8, 8), Format::R8_UNORM, false).unwrap(); let image = util::Image::::new_empty(Channels::Rgb, Size::new(8, 10)); let result = encoder.write_surface(image.view()); @@ -1040,12 +996,8 @@ mod errors { #[test] fn too_many_surfaces() { - let mut encoder = Encoder::new( - std::io::sink(), - Format::R8_UNORM, - &Header::new_image(8, 8, Format::R8_UNORM), - ) - .unwrap(); + let mut encoder = + Encoder::new_image(std::io::sink(), Size::new(8, 8), Format::R8_UNORM, false).unwrap(); let image = util::Image::::new_empty(Channels::Rgb, Size::new(8, 8)); encoder.write_surface(image.view()).unwrap(); // write surface @@ -1061,13 +1013,10 @@ mod errors { #[test] fn missing_surfaces() { - let mut encoder = Encoder::new( - std::io::sink(), - Format::R8_UNORM, - &Header::new_image(8, 8, Format::R8_UNORM).with_mipmaps(), - ) - .unwrap(); + let mut encoder = + Encoder::new_image(std::io::sink(), Size::new(8, 8), Format::R8_UNORM, true).unwrap(); let image = util::Image::::new_empty(Channels::Rgb, Size::new(8, 8)); + encoder.mipmaps.generate = false; encoder.write_surface(image.view()).unwrap(); // write surface let result = encoder.finish(); diff --git a/tests/progress.rs b/tests/progress.rs index b2e74f3..cc3077c 100644 --- a/tests/progress.rs +++ b/tests/progress.rs @@ -3,7 +3,7 @@ use std::sync::{ Arc, }; -use dds::{header::*, *}; +use dds::*; use rand::Rng; use util::{Image, Snapshot}; @@ -75,14 +75,13 @@ fn track_progress() { }; let mut progress = Progress::new(&mut consume_progress); - let mut header = Header::new_image(image.width(), image.height(), format); - if mipmaps && format.encoding_support().unwrap().size_multiple().is_none() { - header = header.with_mipmaps(); - } - - let mut encoder = Encoder::new(std::io::sink(), format, &header)?; + let mut encoder = Encoder::new_image( + std::io::sink(), + image.size(), + format, + mipmaps && format.encoding_support().unwrap().size_multiple().is_none(), + )?; encoder.encoding = options.clone(); - encoder.mipmaps.generate = true; encoder.write_surface_with_progress(image, &mut progress)?; encoder.finish()?; @@ -194,14 +193,13 @@ fn forward_progress() { }; let mut progress = Progress::new(&mut consume_progress); - let mut header = Header::new_image(image.width(), image.height(), format); - if format.encoding_support().unwrap().size_multiple().is_none() { - header = header.with_mipmaps(); - } - - let mut encoder = Encoder::new(std::io::sink(), format, &header)?; + let mut encoder = Encoder::new_image( + std::io::sink(), + image.size(), + format, + format.encoding_support().unwrap().size_multiple().is_none(), + )?; encoder.encoding = options.clone(); - encoder.mipmaps.generate = true; encoder.write_surface_with_progress(image, &mut progress)?; encoder.finish()?;