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
54 changes: 54 additions & 0 deletions src/builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use crate::{Error, ImageFormat, vtf::VTF};
use image::{DynamicImage, GenericImageView};

#[derive(Clone, Debug)]
pub struct VTFBuilder {
frames: Vec<DynamicImage>,
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<Self, 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);
}

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<Vec<u8>, 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)
}
}
15 changes: 14 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<TryFromPrimitiveError<image::ImageFormat>> for Error {
Expand All @@ -43,3 +52,7 @@ pub fn from_bytes(bytes: &[u8]) -> Result<VTF, Error> {
pub fn create(image: DynamicImage, image_format: ImageFormat) -> Result<Vec<u8>, Error> {
VTF::create(image, image_format)
}

pub fn create_animated(image_format: ImageFormat) -> VTFBuilder {
VTFBuilder::new(image_format)
}
116 changes: 74 additions & 42 deletions src/vtf.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::builder::VTFBuilder;
use crate::header::VTFHeader;
use crate::image::{ImageFormat, VTFImage};
use crate::resources::{ResourceList, ResourceType};
Expand All @@ -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<VTF<'a>, Error> {
let mut cursor = Cursor::new(bytes);

Expand Down Expand Up @@ -67,24 +72,22 @@ impl<'a> VTF<'a> {
})
}

pub fn create(image: DynamicImage, image_format: ImageFormat) -> Result<Vec<u8>, 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<Vec<u8>, 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
header_size: 64,
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,
Expand Down Expand Up @@ -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<Vec<u8>, 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)
}
}
39 changes: 39 additions & 0 deletions tests/animated.rs
Original file line number Diff line number Diff line change
@@ -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));
}
9 changes: 9 additions & 0 deletions tests/common.rs
Original file line number Diff line number Diff line change
@@ -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()
}
43 changes: 19 additions & 24 deletions tests/png.rs
Original file line number Diff line number Diff line change
@@ -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");
Expand All @@ -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()
}