mirror of
https://github.com/mon/ifstools.git
synced 2026-05-09 21:14:38 -05:00
100 lines
3.3 KiB
Rust
100 lines
3.3 KiB
Rust
//! DXT1 / DXT5 decoders for Konami's byte-swapped texture format.
|
|
//!
|
|
//! Konami stores standard DXT-compressed pixel data with each 16-bit word's
|
|
//! bytes swapped. Standard DDS/PIL/texpresso expect the canonical little-endian
|
|
//! layout, so we un-swap before handing the bytes off to texpresso.
|
|
|
|
use texpresso::Format;
|
|
|
|
#[derive(Debug)]
|
|
pub enum DxtError {
|
|
UnknownFormat(String),
|
|
OddByteCount(usize),
|
|
}
|
|
|
|
impl std::fmt::Display for DxtError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
DxtError::UnknownFormat(s) => write!(f, "unknown DXT format: {}", s),
|
|
DxtError::OddByteCount(n) => write!(f, "input length {} is not a multiple of 2", n),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for DxtError {}
|
|
|
|
fn parse_format(s: &str) -> Result<(Format, &'static str), DxtError> {
|
|
match s {
|
|
"dxt1" | "DXT1" | "bc1" => Ok((Format::Bc1, "DXT1")),
|
|
"dxt3" | "DXT3" | "bc2" => Ok((Format::Bc2, "DXT3")),
|
|
"dxt5" | "DXT5" | "bc3" => Ok((Format::Bc3, "DXT5")),
|
|
other => Err(DxtError::UnknownFormat(other.to_string())),
|
|
}
|
|
}
|
|
|
|
/// Decode Konami-format DXT data into raw RGBA8 pixels (`width * height * 4`
|
|
/// bytes). Pads short input with zero bytes to match the prior Python
|
|
/// behaviour, which warns and continues rather than failing on truncated
|
|
/// textures.
|
|
pub fn decode(
|
|
data: &[u8],
|
|
width: usize,
|
|
height: usize,
|
|
format: &str,
|
|
) -> Result<Vec<u8>, DxtError> {
|
|
let (fmt, fmt_name) = parse_format(format)?;
|
|
if data.len() % 2 != 0 {
|
|
return Err(DxtError::OddByteCount(data.len()));
|
|
}
|
|
|
|
let expected = fmt.compressed_size(width, height);
|
|
|
|
// Konami's per-WORD byte swap, with zero padding if the source is short.
|
|
// n is always even: data.len() is checked above, expected is a DXT block size.
|
|
let mut swapped = vec![0u8; expected];
|
|
let n = data.len().min(expected);
|
|
swapped[..n].copy_from_slice(&data[..n]);
|
|
for chunk in swapped[..n].chunks_exact_mut(2) {
|
|
chunk.swap(0, 1);
|
|
}
|
|
let _ = fmt_name; // currently only used for error formatting
|
|
|
|
let mut rgba = vec![0u8; width * height * 4];
|
|
fmt.decompress(&swapped, width, height, &mut rgba);
|
|
Ok(rgba)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn dxt5_smoke() {
|
|
// Single 4x4 DXT5 block: alpha 0xFF, RGB midgray.
|
|
// Standard DXT5 layout (little-endian within blocks):
|
|
// alpha0 alpha1 [6 bytes alpha indices] [color block 8 bytes]
|
|
// We Konami-swap it (per u16) before feeding to decode().
|
|
let canonical = [
|
|
0xFFu8, 0xFF, // both alpha endpoints = 255
|
|
0, 0, 0, 0, 0, 0, // alpha indices (all zero → endpoint 0 = 255 everywhere)
|
|
0xFF, 0x7F, 0xFF, 0x7F, // both colors = 0x7FFF (ish gray)
|
|
0, 0, 0, 0,
|
|
];
|
|
let mut konami = canonical;
|
|
for chunk in konami.chunks_exact_mut(2) {
|
|
chunk.swap(0, 1);
|
|
}
|
|
let rgba = decode(&konami, 4, 4, "dxt5").unwrap();
|
|
assert_eq!(rgba.len(), 4 * 4 * 4);
|
|
// Alpha should be 0xFF everywhere.
|
|
for px in rgba.chunks_exact(4) {
|
|
assert_eq!(px[3], 0xFF, "alpha not opaque: {:?}", px);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_format_errors() {
|
|
assert!(decode(&[0; 16], 4, 4, "garbage").is_err());
|
|
}
|
|
}
|