diff --git a/src/builder.rs b/src/builder.rs new file mode 100644 index 0000000..bf20ebb --- /dev/null +++ b/src/builder.rs @@ -0,0 +1,54 @@ +use crate::{Error, ImageFormat, vtf::VTF}; +use image::{DynamicImage, GenericImageView}; + +#[derive(Clone, Debug)] +pub struct VTFBuilder { + frames: Vec, + image_format: ImageFormat, + first_frame: u16, +} +impl VTFBuilder { + pub fn new(image_format: ImageFormat) -> Self { + VTFBuilder { frames: Vec::new(), image_format, first_frame: 0 } + } + + pub fn add_frame( + mut self, + image: DynamicImage, + ) -> Result { + if !image.width().is_power_of_two() + || !image.height().is_power_of_two() + || image.width() > u16::MAX as u32 + || image.height() > u16::MAX as u32 + { + return Err(Error::InvalidImageSize); + } + + if let Some(first) = self.frames.first() { + if image.dimensions() != first.dimensions() { + return Err(Error::MismatchedFrameDimensions); + } + } + + self.frames.push(image); + + Ok(self) + } + + pub fn set_first_frame(mut self, first_frame: u16) -> Self { + self.first_frame = first_frame; + self + } + + pub fn build(self) -> Result, Error> { + if self.frames.is_empty() { + return Err(Error::NoFrames); + } + + if self.first_frame >= self.frames.len() as u16 { + return Err(Error::InvalidFirstFrame); + } + + VTF::encode(&self.frames, self.image_format, self.first_frame) + } +} diff --git a/src/lib.rs b/src/lib.rs index 5012287..e0f38a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,9 +3,10 @@ pub mod image; pub mod resources; mod utils; pub mod vtf; +pub mod builder; pub use crate::image::ImageFormat; -use crate::vtf::VTF; +use crate::{builder::VTFBuilder, vtf::VTF}; use ::image::DynamicImage; use num_enum::TryFromPrimitiveError; use thiserror::Error; @@ -28,6 +29,14 @@ pub enum Error { InvalidImageSize, #[error("Encoding {0} images is not supported")] UnsupportedEncodeImageFormat(ImageFormat), + #[error("Mismatched frame dimensions")] + MismatchedFrameDimensions, + #[error("Maximum number of frames exceeded")] + TooManyFrames, + #[error("No frames provided")] + NoFrames, + #[error("First frame index is out of bounds")] + InvalidFirstFrame, } impl From> for Error { @@ -43,3 +52,7 @@ pub fn from_bytes(bytes: &[u8]) -> Result { pub fn create(image: DynamicImage, image_format: ImageFormat) -> Result, Error> { VTF::create(image, image_format) } + +pub fn create_animated(image_format: ImageFormat) -> VTFBuilder { + VTFBuilder::new(image_format) +} diff --git a/src/vtf.rs b/src/vtf.rs index c6dd240..948b079 100644 --- a/src/vtf.rs +++ b/src/vtf.rs @@ -1,3 +1,4 @@ +use crate::builder::VTFBuilder; use crate::header::VTFHeader; use crate::image::{ImageFormat, VTFImage}; use crate::resources::{ResourceList, ResourceType}; @@ -15,6 +16,10 @@ pub struct VTF<'a> { } impl<'a> VTF<'a> { + pub fn create_animated(image_format: ImageFormat) -> VTFBuilder { + VTFBuilder::new(image_format) + } + pub fn read(bytes: &'a [u8]) -> Result, Error> { let mut cursor = Cursor::new(bytes); @@ -67,15 +72,13 @@ impl<'a> VTF<'a> { }) } - pub fn create(image: DynamicImage, image_format: ImageFormat) -> Result, Error> { - if !image.width().is_power_of_two() - || !image.height().is_power_of_two() - || image.width() > u16::MAX as u32 - || image.height() > u16::MAX as u32 - { - return Err(Error::InvalidImageSize); + pub(crate) fn encode(frames: &[DynamicImage], image_format: ImageFormat, first_frame: u16) -> Result, Error> { + if frames.len() > u16::MAX as usize { + return Err(Error::TooManyFrames); } + let image = &frames[0]; + let header = VTFHeader { signature: VTFHeader::SIGNATURE, version: [7, 1], // simpler version without resources for now @@ -83,8 +86,8 @@ impl<'a> VTF<'a> { width: image.width() as u16, height: image.height() as u16, flags: 8972, - frames: 1, - first_frame: 0, + frames: frames.len() as u16, + first_frame, reflectivity: [0.0, 0.0, 0.0], bumpmap_scale: 1.0, highres_image_format: image_format, @@ -113,45 +116,74 @@ impl<'a> VTF<'a> { let header_size = header.size(); assert!(data.len() <= header_size, "invalid header size"); - data.resize(header_size, 0); - let width = header.width as usize; let height = header.height as usize; - match image_format { - ImageFormat::Dxt5 => { - let image_data = image.to_rgba8(); - data.resize(header_size + Format::Bc3.compressed_size(width, height), 0); - Format::Bc3.compress( - image_data.as_raw(), - width, - height, - Params::default(), - &mut data[header_size..], - ); - } - ImageFormat::Dxt1Onebitalpha => { - let image_data = image.to_rgba8(); - data.resize(header_size + Format::Bc1.compressed_size(width, height), 0); - Format::Bc1.compress( - image_data.as_raw(), - width, - height, - Params::default(), - &mut data[header_size..], - ); - } - ImageFormat::Rgba8888 => { - let image_data = image.to_rgba8(); - data.extend_from_slice(&image_data); - } - ImageFormat::Rgb888 => { - let image_data = image.to_rgb8(); - data.extend_from_slice(&image_data); - } + let frame_size = match image_format { + ImageFormat::Dxt5 => Format::Bc3.compressed_size(width, height), + ImageFormat::Dxt1Onebitalpha => Format::Bc1.compressed_size(width, height), + ImageFormat::Rgba8888 => width * height * 4, + ImageFormat::Rgb888 => width * height * 3, _ => return Err(Error::UnsupportedEncodeImageFormat(image_format)), + }; + + data.resize( + header_size + frame_size * frames.len(), + 0, + ); + + let mut output = &mut data[header_size..]; + + for image in frames { + match image_format { + ImageFormat::Dxt5 => { + let image_data = image.to_rgba8(); + Format::Bc3.compress( + image_data.as_raw(), + width, + height, + Params::default(), + output, + ); + output = &mut output[frame_size..]; + } + ImageFormat::Dxt1Onebitalpha => { + let image_data = image.to_rgba8(); + Format::Bc1.compress( + image_data.as_raw(), + width, + height, + Params::default(), + output, + ); + output = &mut output[frame_size..]; + } + ImageFormat::Rgba8888 => { + let image_data = image.to_rgba8(); + output[..frame_size].copy_from_slice(&image_data); + output = &mut output[frame_size..]; + } + ImageFormat::Rgb888 => { + let image_data = image.to_rgb8(); + output[..frame_size].copy_from_slice(&image_data); + output = &mut output[frame_size..]; + } + _ => return Err(Error::UnsupportedEncodeImageFormat(image_format)), + } } Ok(data) } + + pub fn create(image: DynamicImage, image_format: ImageFormat) -> Result, Error> { + if !image.width().is_power_of_two() + || !image.height().is_power_of_two() + || image.width() > u16::MAX as u32 + || image.height() > u16::MAX as u32 + { + return Err(Error::InvalidImageSize); + } + + Self::encode(&[image], image_format, 0) + } } diff --git a/tests/animated.rs b/tests/animated.rs new file mode 100644 index 0000000..8673720 --- /dev/null +++ b/tests/animated.rs @@ -0,0 +1,39 @@ +mod common; +use common::hash; + +use std::{fs::File, io::BufReader}; +use vtf::builder::VTFBuilder; + +#[test] +fn test_animated() { + let first_frame = image::load( + BufReader::new(File::open("tests/data/rust_rgb_8.png").unwrap()), + image::ImageFormat::Png, + ) + .unwrap(); + + let second_frame = image::load( + BufReader::new(File::open("tests/data/rust_rgb_8_alpha.png").unwrap()), + image::ImageFormat::Png, + ) + .unwrap(); + + let first_frame_hash = hash(&first_frame); + let second_frame_hash = hash(&second_frame); + + let vtf = VTFBuilder::new(vtf::ImageFormat::Rgba8888) + .add_frame(first_frame) + .unwrap() + .add_frame(second_frame) + .unwrap() + .set_first_frame(0) + .build() + .unwrap(); + + let vtf = vtf::from_bytes(&vtf).unwrap(); + let decoded_first = vtf.highres_image.decode(0).unwrap(); + let decoded_second = vtf.highres_image.decode(1).unwrap(); + + assert_eq!(first_frame_hash, hash(&decoded_first)); + assert_eq!(second_frame_hash, hash(&decoded_second)); +} diff --git a/tests/common.rs b/tests/common.rs new file mode 100644 index 0000000..5c9a67a --- /dev/null +++ b/tests/common.rs @@ -0,0 +1,9 @@ +use image::DynamicImage; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; + +pub fn hash(image: &DynamicImage) -> u64 { + let mut hasher = DefaultHasher::new(); + image.to_rgba8().hash(&mut hasher); + hasher.finish() +} diff --git a/tests/png.rs b/tests/png.rs index 0b0aecb..4e04bac 100644 --- a/tests/png.rs +++ b/tests/png.rs @@ -1,10 +1,26 @@ -use image::{open, DynamicImage, GenericImageView}; -use std::collections::hash_map::DefaultHasher; +use image::{open, GenericImageView}; use std::fs::File; -use std::hash::{Hash, Hasher}; use std::io::Read; use std::vec::Vec; +mod common; +use common::*; + +fn test_image(input: &str, expected: &str) { + let mut file = File::open(input).unwrap(); + let mut buf = Vec::new(); + file.read_to_end(&mut buf).unwrap(); + + let vtf = vtf::from_bytes(&buf).unwrap(); + let image = vtf.highres_image.decode(0).unwrap(); + + let expected = open(expected).unwrap(); + + assert_eq!(expected.dimensions(), image.dimensions()); + + assert_eq!(hash(&expected), hash(&image)); +} + #[test] fn test_to_png_dxt5() { test_image("tests/data/rust_dxt5.vtf", "tests/data/rust_dxt5.png"); @@ -22,24 +38,3 @@ fn test_to_png_rgb8_alpha() { "tests/data/rust_rgb_8_alpha.png", ); } - -fn test_image(input: &str, expected: &str) { - let mut file = File::open(input).unwrap(); - let mut buf = Vec::new(); - file.read_to_end(&mut buf).unwrap(); - - let vtf = vtf::from_bytes(&buf).unwrap(); - let image = vtf.highres_image.decode(0).unwrap(); - - let expected = open(expected).unwrap(); - - assert_eq!(expected.dimensions(), image.dimensions()); - - assert_eq!(hash(expected), hash(image)); -} - -fn hash(image: DynamicImage) -> u64 { - let mut hasher = DefaultHasher::new(); - image.into_rgba8().hash(&mut hasher); - hasher.finish() -}