1244 lines
44 KiB
Rust
1244 lines
44 KiB
Rust
//! Bounded native TGA and DDS decoding plus reference-compatible TGA output.
|
|
|
|
use crate::Error;
|
|
use libremetaverse_imaging::{DEFAULT_MAX_ENCODED_BYTES, ManagedImage, ManagedImageImageChannels};
|
|
use libremetaverse_types::compat::ReadWrite;
|
|
use std::io::Read as _;
|
|
|
|
type TgaPalette = (usize, Vec<[u8; 4]>);
|
|
|
|
/// Truevision TGA entry points backed by native TGA/DDS parsing.
|
|
pub struct Targa;
|
|
|
|
impl Targa {
|
|
/// Decodes a TGA or DDS file into planar managed-image storage.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns a typed error for inaccessible, oversized, malformed, or
|
|
/// unsupported input and for invalid decoded dimensions.
|
|
pub fn decode_to_managed_image_with_string(file_name: String) -> Result<ManagedImage, Error> {
|
|
let mut file = std::fs::File::open(file_name).map_err(|_| Error::InvalidOperation)?;
|
|
let bytes = read_bounded(&mut file)?;
|
|
decode(&bytes)
|
|
}
|
|
|
|
/// Decodes a TGA or DDS stream into planar managed-image storage.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns a typed error for failed reads, oversized input, malformed or
|
|
/// unsupported data, and invalid decoded dimensions.
|
|
pub fn decode_to_managed_image_with_stream(
|
|
mut stream: Box<dyn ReadWrite + Send>,
|
|
) -> Result<ManagedImage, Error> {
|
|
let bytes = read_bounded(&mut *stream)?;
|
|
decode(&bytes)
|
|
}
|
|
|
|
/// Encodes the exact compact TGA layout emitted by the C# implementation.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns a typed error for inconsistent planes, unsupported channel
|
|
/// combinations, dimensions outside the TGA range, or allocation limits.
|
|
#[allow(clippy::needless_pass_by_value)] // Fixed mapped C# value signature.
|
|
pub fn encode(image: ManagedImage) -> Result<Vec<u8>, Error> {
|
|
image.validate()?;
|
|
let width = u16::try_from(image.width).map_err(|_| Error::Argument)?;
|
|
let height = u16::try_from(image.height).map_err(|_| Error::Argument)?;
|
|
let color = image.channels.contains(ManagedImageImageChannels::COLOR);
|
|
let gray = image.channels.contains(ManagedImageImageChannels::GRAY);
|
|
let alpha = image.channels.contains(ManagedImageImageChannels::ALPHA);
|
|
let supported = color && !gray || !color && (gray || alpha);
|
|
if !supported {
|
|
return Err(Error::InvalidOperation);
|
|
}
|
|
let components = (if color { 3 } else { usize::from(gray) }) + usize::from(alpha);
|
|
let pixels = usize::from(width)
|
|
.checked_mul(usize::from(height))
|
|
.ok_or(Error::Argument)?;
|
|
let length = pixels
|
|
.checked_mul(components)
|
|
.and_then(|value| value.checked_add(32))
|
|
.ok_or(Error::Argument)?;
|
|
if length > DEFAULT_MAX_ENCODED_BYTES {
|
|
return Err(Error::Argument);
|
|
}
|
|
let written_components = if alpha && !color { 4 } else { components };
|
|
let written_end = pixels
|
|
.checked_mul(written_components)
|
|
.and_then(|value| value.checked_add(18))
|
|
.ok_or(Error::Argument)?;
|
|
if written_end > length {
|
|
return Err(Error::InvalidOperation);
|
|
}
|
|
let mut output = vec![0; length];
|
|
output[2] = 2;
|
|
output[12..14].copy_from_slice(&width.to_le_bytes());
|
|
output[14..16].copy_from_slice(&height.to_le_bytes());
|
|
output[16] = u8::try_from(components * 8).map_err(|_| Error::Argument)?;
|
|
output[17] = if alpha { 0x20 } else { 0 };
|
|
let mut target = 18;
|
|
for index in 0..pixels {
|
|
if color {
|
|
output[target] = image.blue[index];
|
|
output[target + 1] = image.green[index];
|
|
output[target + 2] = image.red[index];
|
|
target += 3;
|
|
if alpha {
|
|
output[target] = image.alpha[index];
|
|
target += 1;
|
|
}
|
|
} else if alpha {
|
|
output[target] = image.alpha[index];
|
|
output[target + 1] = image.alpha[index];
|
|
output[target + 2] = image.alpha[index];
|
|
output[target + 3] = u8::MAX;
|
|
target += 4;
|
|
} else {
|
|
output[target] = image.red[index];
|
|
target += 1;
|
|
}
|
|
}
|
|
Ok(output)
|
|
}
|
|
}
|
|
|
|
fn read_bounded<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Vec<u8>, Error> {
|
|
let mut bytes = Vec::new();
|
|
reader
|
|
.take((DEFAULT_MAX_ENCODED_BYTES + 1) as u64)
|
|
.read_to_end(&mut bytes)
|
|
.map_err(|_| Error::InvalidOperation)?;
|
|
if bytes.len() > DEFAULT_MAX_ENCODED_BYTES {
|
|
Err(Error::Argument)
|
|
} else {
|
|
Ok(bytes)
|
|
}
|
|
}
|
|
|
|
fn decode(bytes: &[u8]) -> Result<ManagedImage, Error> {
|
|
if bytes.len() > DEFAULT_MAX_ENCODED_BYTES {
|
|
return Err(Error::Argument);
|
|
}
|
|
if bytes.starts_with(b"DDS ") {
|
|
decode_dds(bytes)
|
|
} else {
|
|
decode_tga(bytes)
|
|
}
|
|
}
|
|
|
|
fn parse(position: usize, context: &'static str) -> Error {
|
|
Error::Parse { position, context }
|
|
}
|
|
|
|
fn read_u16(bytes: &[u8], position: usize, context: &'static str) -> Result<u16, Error> {
|
|
let value = bytes
|
|
.get(position..position + 2)
|
|
.ok_or_else(|| parse(position, context))?;
|
|
Ok(u16::from_le_bytes([value[0], value[1]]))
|
|
}
|
|
|
|
fn read_u32(bytes: &[u8], position: usize, context: &'static str) -> Result<u32, Error> {
|
|
let value = bytes
|
|
.get(position..position + 4)
|
|
.ok_or_else(|| parse(position, context))?;
|
|
Ok(u32::from_le_bytes([value[0], value[1], value[2], value[3]]))
|
|
}
|
|
|
|
fn decode_tga(bytes: &[u8]) -> Result<ManagedImage, Error> {
|
|
let header = bytes
|
|
.get(..18)
|
|
.ok_or_else(|| parse(0, "truncated TGA header"))?;
|
|
let id_length = usize::from(header[0]);
|
|
let image_type = header[2];
|
|
let color_mapped = matches!(image_type, 1 | 9);
|
|
let has_color_map = header[1] == 1;
|
|
if header[1] > 1 || color_mapped && !has_color_map {
|
|
return Err(parse(1, "invalid TGA color-map declaration"));
|
|
}
|
|
let rle = matches!(image_type, 9..=11);
|
|
let grayscale = matches!(image_type, 3 | 11);
|
|
if !matches!(image_type, 1 | 2 | 3 | 9 | 10 | 11) {
|
|
return Err(parse(2, "unsupported TGA image type"));
|
|
}
|
|
let width = read_u16(bytes, 12, "truncated TGA width")?;
|
|
let height = read_u16(bytes, 14, "truncated TGA height")?;
|
|
let depth = header[16];
|
|
if (grayscale && !matches!(depth, 8 | 16))
|
|
|| (color_mapped && !matches!(depth, 8 | 16))
|
|
|| (!grayscale && !color_mapped && !matches!(depth, 15 | 16 | 24 | 32))
|
|
{
|
|
return Err(parse(16, "unsupported TGA pixel depth"));
|
|
}
|
|
let palette_depth = header[7];
|
|
if has_color_map && !matches!(palette_depth, 15 | 16 | 24 | 32) {
|
|
return Err(parse(7, "unsupported TGA color-map depth"));
|
|
}
|
|
let channels = tga_channels(grayscale, color_mapped, depth, palette_depth, header[17]);
|
|
let mut image = ManagedImage::new(i32::from(width), i32::from(height), channels)?;
|
|
let pixels = usize::from(width) * usize::from(height);
|
|
let pixel_bytes = usize::from(depth.div_ceil(8));
|
|
let mut position = 18usize.checked_add(id_length).ok_or(Error::Argument)?;
|
|
if position > bytes.len() {
|
|
return Err(parse(18, "truncated TGA image ID"));
|
|
}
|
|
let palette = read_tga_palette(bytes, header, has_color_map, color_mapped, &mut position)?;
|
|
let mut decoded = 0;
|
|
while decoded < pixels {
|
|
let (count, repeated) = if rle {
|
|
let packet = *bytes
|
|
.get(position)
|
|
.ok_or_else(|| parse(position, "truncated TGA RLE packet"))?;
|
|
position += 1;
|
|
(usize::from(packet & 0x7f) + 1, packet & 0x80 != 0)
|
|
} else {
|
|
(1, false)
|
|
};
|
|
if count > pixels - decoded {
|
|
return Err(parse(position, "TGA RLE packet exceeds pixel count"));
|
|
}
|
|
if repeated {
|
|
let pixel = bytes
|
|
.get(position..position + pixel_bytes)
|
|
.ok_or_else(|| parse(position, "truncated TGA pixel"))?;
|
|
position += pixel_bytes;
|
|
for _ in 0..count {
|
|
write_tga_pixel(
|
|
&mut image,
|
|
decoded,
|
|
pixel,
|
|
depth,
|
|
header[17],
|
|
grayscale,
|
|
palette.as_ref(),
|
|
)?;
|
|
decoded += 1;
|
|
}
|
|
} else {
|
|
for _ in 0..count {
|
|
let pixel = bytes
|
|
.get(position..position + pixel_bytes)
|
|
.ok_or_else(|| parse(position, "truncated TGA pixel"))?;
|
|
position += pixel_bytes;
|
|
write_tga_pixel(
|
|
&mut image,
|
|
decoded,
|
|
pixel,
|
|
depth,
|
|
header[17],
|
|
grayscale,
|
|
palette.as_ref(),
|
|
)?;
|
|
decoded += 1;
|
|
}
|
|
}
|
|
}
|
|
Ok(image)
|
|
}
|
|
|
|
fn tga_channels(
|
|
grayscale: bool,
|
|
color_mapped: bool,
|
|
depth: u8,
|
|
palette_depth: u8,
|
|
descriptor: u8,
|
|
) -> ManagedImageImageChannels {
|
|
if grayscale {
|
|
if depth == 16 {
|
|
ManagedImageImageChannels::GRAY | ManagedImageImageChannels::ALPHA
|
|
} else {
|
|
ManagedImageImageChannels::GRAY
|
|
}
|
|
} else if depth == 32
|
|
|| color_mapped && palette_depth == 32
|
|
|| descriptor & 0x0f != 0 && (depth == 16 || color_mapped && palette_depth == 16)
|
|
{
|
|
ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA
|
|
} else {
|
|
ManagedImageImageChannels::COLOR
|
|
}
|
|
}
|
|
|
|
fn read_tga_palette(
|
|
bytes: &[u8],
|
|
header: &[u8],
|
|
has_color_map: bool,
|
|
apply_palette: bool,
|
|
position: &mut usize,
|
|
) -> Result<Option<TgaPalette>, Error> {
|
|
if !has_color_map {
|
|
return Ok(None);
|
|
}
|
|
let first = usize::from(read_u16(bytes, 3, "truncated TGA color-map origin")?);
|
|
let length = usize::from(read_u16(bytes, 5, "truncated TGA color-map length")?);
|
|
if apply_palette && length == 0 {
|
|
return Err(parse(5, "empty TGA color map"));
|
|
}
|
|
let palette_depth = header[7];
|
|
let entry_bytes = usize::from(palette_depth.div_ceil(8));
|
|
let byte_length = length.checked_mul(entry_bytes).ok_or(Error::Argument)?;
|
|
let palette_end = position.checked_add(byte_length).ok_or(Error::Argument)?;
|
|
let data = bytes
|
|
.get(*position..palette_end)
|
|
.ok_or_else(|| parse(*position, "truncated TGA color map"))?;
|
|
*position = palette_end;
|
|
if !apply_palette {
|
|
return Ok(None);
|
|
}
|
|
let mut entries = Vec::with_capacity(length);
|
|
for entry in data.chunks_exact(entry_bytes) {
|
|
entries.push(decode_tga_color(entry, palette_depth)?);
|
|
}
|
|
Ok(Some((first, entries)))
|
|
}
|
|
|
|
fn write_tga_pixel(
|
|
image: &mut ManagedImage,
|
|
file_index: usize,
|
|
pixel: &[u8],
|
|
depth: u8,
|
|
descriptor: u8,
|
|
grayscale: bool,
|
|
palette: Option<&TgaPalette>,
|
|
) -> Result<(), Error> {
|
|
let width = usize::try_from(image.width).map_err(|_| Error::Argument)?;
|
|
let height = usize::try_from(image.height).map_err(|_| Error::Argument)?;
|
|
let file_x = file_index % width;
|
|
let file_y = file_index / width;
|
|
let x = if descriptor & 0x10 == 0 {
|
|
file_x
|
|
} else {
|
|
width - 1 - file_x
|
|
};
|
|
let y = if descriptor & 0x20 != 0 {
|
|
file_y
|
|
} else {
|
|
height - 1 - file_y
|
|
};
|
|
let target = y * width + x;
|
|
if let Some((first, entries)) = palette {
|
|
let index = if depth == 8 {
|
|
usize::from(pixel[0])
|
|
} else {
|
|
usize::from(u16::from_le_bytes([pixel[0], pixel[1]]))
|
|
};
|
|
let color = entries
|
|
.get(
|
|
index
|
|
.checked_sub(*first)
|
|
.ok_or_else(|| parse(18, "TGA color-map index below origin"))?,
|
|
)
|
|
.ok_or_else(|| parse(18, "TGA color-map index out of range"))?;
|
|
image.red[target] = color[0];
|
|
image.green[target] = color[1];
|
|
image.blue[target] = color[2];
|
|
if !image.alpha.is_empty() {
|
|
image.alpha[target] = color[3];
|
|
}
|
|
return Ok(());
|
|
}
|
|
if grayscale {
|
|
image.red[target] = pixel[0];
|
|
if depth == 16 {
|
|
image.alpha[target] = pixel[1];
|
|
}
|
|
return Ok(());
|
|
}
|
|
match depth {
|
|
15 | 16 => {
|
|
let value = u16::from_le_bytes([pixel[0], pixel[1]]);
|
|
image.red[target] = expand_5(u8::try_from((value >> 10) & 31).unwrap());
|
|
image.green[target] = expand_5(u8::try_from((value >> 5) & 31).unwrap());
|
|
image.blue[target] = expand_5(u8::try_from(value & 31).unwrap());
|
|
if !image.alpha.is_empty() {
|
|
image.alpha[target] = if value & 0x8000 == 0 { 0 } else { u8::MAX };
|
|
}
|
|
}
|
|
24 | 32 => {
|
|
image.blue[target] = pixel[0];
|
|
image.green[target] = pixel[1];
|
|
image.red[target] = pixel[2];
|
|
if depth == 32 {
|
|
image.alpha[target] = pixel[3];
|
|
}
|
|
}
|
|
_ => return Err(Error::Argument),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn decode_tga_color(pixel: &[u8], depth: u8) -> Result<[u8; 4], Error> {
|
|
match depth {
|
|
15 | 16 => {
|
|
let value = u16::from_le_bytes([pixel[0], pixel[1]]);
|
|
Ok([
|
|
expand_5(u8::try_from((value >> 10) & 31).unwrap()),
|
|
expand_5(u8::try_from((value >> 5) & 31).unwrap()),
|
|
expand_5(u8::try_from(value & 31).unwrap()),
|
|
if depth == 16 && value & 0x8000 == 0 {
|
|
0
|
|
} else {
|
|
u8::MAX
|
|
},
|
|
])
|
|
}
|
|
24 => Ok([pixel[2], pixel[1], pixel[0], 255]),
|
|
32 => Ok([pixel[2], pixel[1], pixel[0], pixel[3]]),
|
|
_ => Err(Error::Argument),
|
|
}
|
|
}
|
|
|
|
fn expand_5(value: u8) -> u8 {
|
|
u8::try_from((u16::from(value) * 255 + 15) / 31).unwrap()
|
|
}
|
|
|
|
fn expand_6(value: u8) -> u8 {
|
|
u8::try_from((u16::from(value) * 255 + 31) / 63).unwrap()
|
|
}
|
|
|
|
fn decode_dds(bytes: &[u8]) -> Result<ManagedImage, Error> {
|
|
if bytes.len() < 128 || read_u32(bytes, 4, "truncated DDS header")? != 124 {
|
|
return Err(parse(4, "invalid DDS header"));
|
|
}
|
|
if read_u32(bytes, 76, "truncated DDS pixel format")? != 32 {
|
|
return Err(parse(76, "invalid DDS pixel format"));
|
|
}
|
|
let height = read_u32(bytes, 12, "truncated DDS height")?;
|
|
let width = read_u32(bytes, 16, "truncated DDS width")?;
|
|
let width_i32 = i32::try_from(width).map_err(|_| Error::Argument)?;
|
|
let height_i32 = i32::try_from(height).map_err(|_| Error::Argument)?;
|
|
let fourcc = bytes
|
|
.get(84..88)
|
|
.ok_or_else(|| parse(84, "truncated DDS FourCC"))?;
|
|
if fourcc == b"DX10" {
|
|
let format = read_u32(bytes, 128, "truncated DDS DX10 header")?;
|
|
return decode_dds_dx10(bytes, width_i32, height_i32, format);
|
|
}
|
|
let block = match fourcc {
|
|
[0, 0, 0, 0] => {
|
|
let bits = read_u32(bytes, 88, "truncated DDS bit count")?;
|
|
let masks = [
|
|
read_u32(bytes, 92, "truncated DDS red mask")?,
|
|
read_u32(bytes, 96, "truncated DDS green mask")?,
|
|
read_u32(bytes, 100, "truncated DDS blue mask")?,
|
|
read_u32(bytes, 104, "truncated DDS alpha mask")?,
|
|
];
|
|
return decode_dds_uncompressed(bytes, width_i32, height_i32, 128, bits, masks);
|
|
}
|
|
b"DXT1" => DdsBlock::Bc1,
|
|
b"DXT3" => DdsBlock::Bc2,
|
|
b"DXT5" => DdsBlock::Bc3,
|
|
b"ATI1" | b"BC4U" => DdsBlock::Bc4 { signed: false },
|
|
b"BC4S" => DdsBlock::Bc4 { signed: true },
|
|
b"ATI2" | b"BC5U" => DdsBlock::Bc5 { signed: false },
|
|
b"BC5S" => DdsBlock::Bc5 { signed: true },
|
|
_ => return Err(parse(84, "unsupported DDS compression")),
|
|
};
|
|
decode_dds_blocks(bytes, width_i32, height_i32, 128, block)
|
|
}
|
|
|
|
fn decode_dds_dx10(
|
|
bytes: &[u8],
|
|
width: i32,
|
|
height: i32,
|
|
format: u32,
|
|
) -> Result<ManagedImage, Error> {
|
|
match format {
|
|
27..=32 => decode_dds_uncompressed(
|
|
bytes,
|
|
width,
|
|
height,
|
|
148,
|
|
32,
|
|
[0x0000_00ff, 0x0000_ff00, 0x00ff_0000, 0xff00_0000],
|
|
),
|
|
87 | 90 | 91 | 93 => decode_dds_uncompressed(
|
|
bytes,
|
|
width,
|
|
height,
|
|
148,
|
|
32,
|
|
[0x00ff_0000, 0x0000_ff00, 0x0000_00ff, 0xff00_0000],
|
|
),
|
|
70..=72 => decode_dds_blocks(bytes, width, height, 148, DdsBlock::Bc1),
|
|
73..=75 => decode_dds_blocks(bytes, width, height, 148, DdsBlock::Bc2),
|
|
76..=78 => decode_dds_blocks(bytes, width, height, 148, DdsBlock::Bc3),
|
|
79 | 80 => decode_dds_blocks(bytes, width, height, 148, DdsBlock::Bc4 { signed: false }),
|
|
81 => decode_dds_blocks(bytes, width, height, 148, DdsBlock::Bc4 { signed: true }),
|
|
82 | 83 => decode_dds_blocks(bytes, width, height, 148, DdsBlock::Bc5 { signed: false }),
|
|
84 => decode_dds_blocks(bytes, width, height, 148, DdsBlock::Bc5 { signed: true }),
|
|
94 | 95 => decode_dds_blocks(bytes, width, height, 148, DdsBlock::Bc6 { signed: false }),
|
|
96 => decode_dds_blocks(bytes, width, height, 148, DdsBlock::Bc6 { signed: true }),
|
|
97..=99 => decode_dds_blocks(bytes, width, height, 148, DdsBlock::Bc7),
|
|
_ => Err(parse(128, "unsupported DDS DXGI format")),
|
|
}
|
|
}
|
|
|
|
fn decode_dds_uncompressed(
|
|
bytes: &[u8],
|
|
width: i32,
|
|
height: i32,
|
|
data_offset: usize,
|
|
bits: u32,
|
|
mut masks: [u32; 4],
|
|
) -> Result<ManagedImage, Error> {
|
|
let bytes_per_pixel = usize::try_from(bits.div_ceil(8)).map_err(|_| Error::Argument)?;
|
|
if !matches!(bits, 8 | 16 | 24 | 32) {
|
|
return Err(parse(88, "unsupported DDS pixel depth"));
|
|
}
|
|
if bits == 16 && masks[1] != 0x07e0 {
|
|
return Err(parse(96, "unsupported DDS 16-bit layout"));
|
|
}
|
|
if bits >= 24 && masks[..3].contains(&0) {
|
|
masks[..3].copy_from_slice(&[0x00ff_0000, 0x0000_ff00, 0x0000_00ff]);
|
|
}
|
|
if bits == 32 && masks[3] == 0 {
|
|
masks[3] = 0xff00_0000;
|
|
}
|
|
let channels = if bits == 8 {
|
|
ManagedImageImageChannels::GRAY
|
|
} else if bits == 32 {
|
|
ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA
|
|
} else {
|
|
ManagedImageImageChannels::COLOR
|
|
};
|
|
let mut image = ManagedImage::new(width, height, channels)?;
|
|
let width_usize = usize::try_from(width).map_err(|_| Error::Argument)?;
|
|
let height_usize = usize::try_from(height).map_err(|_| Error::Argument)?;
|
|
let packed_row = width_usize
|
|
.checked_mul(bytes_per_pixel)
|
|
.ok_or(Error::Argument)?;
|
|
let required = data_offset
|
|
.checked_add(
|
|
packed_row
|
|
.checked_mul(height_usize)
|
|
.ok_or(Error::Argument)?,
|
|
)
|
|
.ok_or(Error::Argument)?;
|
|
if required > bytes.len() {
|
|
return Err(parse(bytes.len(), "truncated DDS pixel data"));
|
|
}
|
|
for y in 0..height_usize {
|
|
for x in 0..width_usize {
|
|
let source = data_offset + y * packed_row + x * bytes_per_pixel;
|
|
let raw = bytes[source..source + bytes_per_pixel]
|
|
.iter()
|
|
.enumerate()
|
|
.fold(0u32, |value, (shift, byte)| {
|
|
value | (u32::from(*byte) << (shift * 8))
|
|
});
|
|
let target = y * width_usize + x;
|
|
if bits == 8 {
|
|
image.red[target] = u8::try_from(raw).unwrap();
|
|
} else {
|
|
image.red[target] = extract_mask(raw, masks[0], bits)?;
|
|
image.green[target] = extract_mask(raw, masks[1], bits)?;
|
|
image.blue[target] = extract_mask(raw, masks[2], bits)?;
|
|
if bits == 32 {
|
|
image.alpha[target] = extract_mask(raw, masks[3], bits)?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(image)
|
|
}
|
|
|
|
fn extract_mask(value: u32, mask: u32, bits: u32) -> Result<u8, Error> {
|
|
if mask == 0 {
|
|
if bits == 8 {
|
|
return Ok(u8::try_from(value & 0xff).unwrap());
|
|
}
|
|
return Err(parse(92, "missing DDS channel mask"));
|
|
}
|
|
let shift = mask.trailing_zeros();
|
|
let maximum = mask >> shift;
|
|
let component = (value & mask) >> shift;
|
|
let numerator = u64::from(component) * 255 + u64::from(maximum) / 2;
|
|
Ok(u8::try_from(numerator / u64::from(maximum)).unwrap())
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum DdsBlock {
|
|
Bc1,
|
|
Bc2,
|
|
Bc3,
|
|
Bc4 { signed: bool },
|
|
Bc5 { signed: bool },
|
|
Bc6 { signed: bool },
|
|
Bc7,
|
|
}
|
|
|
|
impl DdsBlock {
|
|
const fn encoded_bytes(self) -> usize {
|
|
match self {
|
|
Self::Bc1 | Self::Bc4 { .. } => 8,
|
|
Self::Bc2 | Self::Bc3 | Self::Bc5 { .. } | Self::Bc6 { .. } | Self::Bc7 => 16,
|
|
}
|
|
}
|
|
|
|
const fn channels(self) -> ManagedImageImageChannels {
|
|
match self {
|
|
Self::Bc4 { .. } => ManagedImageImageChannels::GRAY,
|
|
Self::Bc5 { .. } => ManagedImageImageChannels::COLOR,
|
|
Self::Bc1 | Self::Bc2 | Self::Bc3 | Self::Bc6 { .. } | Self::Bc7 => {
|
|
ManagedImageImageChannels(
|
|
ManagedImageImageChannels::COLOR.0 | ManagedImageImageChannels::ALPHA.0,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn decode_dds_blocks(
|
|
bytes: &[u8],
|
|
width: i32,
|
|
height: i32,
|
|
data_offset: usize,
|
|
format: DdsBlock,
|
|
) -> Result<ManagedImage, Error> {
|
|
let block_bytes = format.encoded_bytes();
|
|
let mut image = ManagedImage::new(width, height, format.channels())?;
|
|
let width = usize::try_from(width).map_err(|_| Error::Argument)?;
|
|
let height = usize::try_from(height).map_err(|_| Error::Argument)?;
|
|
let blocks_x = width.div_ceil(4);
|
|
let blocks_y = height.div_ceil(4);
|
|
let length = blocks_x
|
|
.checked_mul(blocks_y)
|
|
.and_then(|count| count.checked_mul(block_bytes))
|
|
.ok_or(Error::Argument)?;
|
|
let data_end = data_offset.checked_add(length).ok_or(Error::Argument)?;
|
|
let data = bytes
|
|
.get(data_offset..data_end)
|
|
.ok_or_else(|| parse(data_offset, "truncated DDS block data"))?;
|
|
for block_y in 0..blocks_y {
|
|
for block_x in 0..blocks_x {
|
|
let start = (block_y * blocks_x + block_x) * block_bytes;
|
|
decode_dds_block(
|
|
&data[start..start + block_bytes],
|
|
format,
|
|
block_x,
|
|
block_y,
|
|
width,
|
|
height,
|
|
&mut image,
|
|
)?;
|
|
}
|
|
}
|
|
Ok(image)
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn decode_dds_block(
|
|
block: &[u8],
|
|
format: DdsBlock,
|
|
block_x: usize,
|
|
block_y: usize,
|
|
width: usize,
|
|
height: usize,
|
|
image: &mut ManagedImage,
|
|
) -> Result<(), Error> {
|
|
match format {
|
|
DdsBlock::Bc1 => decode_dxt_block(block, 1, block_x, block_y, width, height, image),
|
|
DdsBlock::Bc2 => decode_dxt_block(block, 3, block_x, block_y, width, height, image),
|
|
DdsBlock::Bc3 => decode_dxt_block(block, 5, block_x, block_y, width, height, image),
|
|
DdsBlock::Bc4 { signed } => {
|
|
decode_bc4_block(block, signed, block_x, block_y, width, height, image);
|
|
Ok(())
|
|
}
|
|
DdsBlock::Bc5 { signed } => {
|
|
decode_bc5_block(block, signed, block_x, block_y, width, height, image);
|
|
Ok(())
|
|
}
|
|
DdsBlock::Bc6 { signed } => {
|
|
decode_bc6_block(block, signed, block_x, block_y, width, height, image)
|
|
}
|
|
DdsBlock::Bc7 => decode_bc7_block(block, block_x, block_y, width, height, image),
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn decode_dxt_block(
|
|
block: &[u8],
|
|
alpha_kind: u8,
|
|
block_x: usize,
|
|
block_y: usize,
|
|
width: usize,
|
|
height: usize,
|
|
image: &mut ManagedImage,
|
|
) -> Result<(), Error> {
|
|
let color_offset = if alpha_kind == 1 { 0 } else { 8 };
|
|
let color0 = u16::from_le_bytes([block[color_offset], block[color_offset + 1]]);
|
|
let color1 = u16::from_le_bytes([block[color_offset + 2], block[color_offset + 3]]);
|
|
let mut color_table = [rgb565(color0), rgb565(color1), [0; 3], [0; 3]];
|
|
if color0 > color1 || alpha_kind != 1 {
|
|
color_table[2] = blend_rgb565(color0, color1, 2, 1, 3);
|
|
color_table[3] = blend_rgb565(color0, color1, 1, 2, 3);
|
|
} else {
|
|
color_table[2] = blend_rgb565(color0, color1, 1, 1, 2);
|
|
}
|
|
let selectors = u32::from_le_bytes([
|
|
block[color_offset + 4],
|
|
block[color_offset + 5],
|
|
block[color_offset + 6],
|
|
block[color_offset + 7],
|
|
]);
|
|
let alphas = dxt_alphas(block, alpha_kind)?;
|
|
for local_y in 0..4 {
|
|
for local_x in 0..4 {
|
|
let x = block_x * 4 + local_x;
|
|
let y = block_y * 4 + local_y;
|
|
if x >= width || y >= height {
|
|
continue;
|
|
}
|
|
let local = local_y * 4 + local_x;
|
|
let selector = usize::try_from((selectors >> (local * 2)) & 3).unwrap();
|
|
let target = y * width + x;
|
|
image.red[target] = color_table[selector][0];
|
|
image.green[target] = color_table[selector][1];
|
|
image.blue[target] = color_table[selector][2];
|
|
image.alpha[target] = if alpha_kind == 1 && color0 <= color1 && selector == 3 {
|
|
0
|
|
} else {
|
|
alphas[local]
|
|
};
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn decode_bc4_block(
|
|
block: &[u8],
|
|
signed: bool,
|
|
block_x: usize,
|
|
block_y: usize,
|
|
width: usize,
|
|
height: usize,
|
|
image: &mut ManagedImage,
|
|
) {
|
|
let values = bc4_values(block, signed, true);
|
|
let selectors = bc_selectors(block);
|
|
for local_y in 0..4 {
|
|
for local_x in 0..4 {
|
|
let x = block_x * 4 + local_x;
|
|
let y = block_y * 4 + local_y;
|
|
if x < width && y < height {
|
|
let local = local_y * 4 + local_x;
|
|
let selector = usize::try_from((selectors >> (local * 3)) & 7).unwrap();
|
|
image.red[y * width + x] = values[selector];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn decode_bc5_block(
|
|
block: &[u8],
|
|
signed: bool,
|
|
block_x: usize,
|
|
block_y: usize,
|
|
width: usize,
|
|
height: usize,
|
|
image: &mut ManagedImage,
|
|
) {
|
|
let red = bc4_values(block, signed, false);
|
|
let green = bc4_values(&block[8..], signed, false);
|
|
let red_selectors = bc_selectors(block);
|
|
let green_selectors = bc_selectors(&block[8..]);
|
|
for local_y in 0..4 {
|
|
for local_x in 0..4 {
|
|
let x = block_x * 4 + local_x;
|
|
let y = block_y * 4 + local_y;
|
|
if x < width && y < height {
|
|
let local = local_y * 4 + local_x;
|
|
let target = y * width + x;
|
|
let red_selector = usize::try_from((red_selectors >> (local * 3)) & 7).unwrap();
|
|
let green_selector = usize::try_from((green_selectors >> (local * 3)) & 7).unwrap();
|
|
image.red[target] = red[red_selector];
|
|
image.green[target] = green[green_selector];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn bc_selectors(block: &[u8]) -> u64 {
|
|
block[2..8]
|
|
.iter()
|
|
.enumerate()
|
|
.fold(0, |value, (index, byte)| {
|
|
value | (u64::from(*byte) << (index * 8))
|
|
})
|
|
}
|
|
|
|
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
|
fn bc4_values(block: &[u8], signed: bool, rounded_unsigned: bool) -> [u8; 8] {
|
|
if signed {
|
|
let first = f32::from(i8::from_ne_bytes([block[0]]).max(-127));
|
|
let second = f32::from(i8::from_ne_bytes([block[1]]).max(-127));
|
|
let mut values = [0.0f32; 8];
|
|
values[0] = first;
|
|
values[1] = second;
|
|
if first > second {
|
|
for index in 1u8..=6 {
|
|
values[usize::from(index + 1)] =
|
|
(f32::from(7 - index) * first + f32::from(index) * second) / 7.0;
|
|
}
|
|
} else {
|
|
for index in 1u8..=4 {
|
|
values[usize::from(index + 1)] =
|
|
(f32::from(5 - index) * first + f32::from(index) * second) / 5.0;
|
|
}
|
|
values[6] = -127.0;
|
|
values[7] = 127.0;
|
|
}
|
|
return values.map(|value| {
|
|
let normalized = ((value + 127.0) * (255.0 / 254.0)) + 0.5;
|
|
normalized as u8
|
|
});
|
|
}
|
|
|
|
let first = usize::from(block[0]);
|
|
let second = usize::from(block[1]);
|
|
let mut values = [0; 8];
|
|
values[0] = block[0];
|
|
values[1] = block[1];
|
|
if first > second {
|
|
for index in 1..=6 {
|
|
let numerator = (7 - index) * first + index * second;
|
|
values[index + 1] = u8::try_from(if rounded_unsigned {
|
|
(numerator + 3) / 7
|
|
} else {
|
|
numerator / 7
|
|
})
|
|
.unwrap();
|
|
}
|
|
} else {
|
|
for index in 1..=4 {
|
|
let numerator = (5 - index) * first + index * second;
|
|
values[index + 1] = u8::try_from(if rounded_unsigned {
|
|
(numerator + 2) / 5
|
|
} else {
|
|
numerator / 5
|
|
})
|
|
.unwrap();
|
|
}
|
|
values[6] = 0;
|
|
values[7] = u8::MAX;
|
|
}
|
|
values
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments, clippy::unnecessary_wraps)]
|
|
fn decode_bc6_block(
|
|
block: &[u8],
|
|
signed: bool,
|
|
block_x: usize,
|
|
block_y: usize,
|
|
width: usize,
|
|
height: usize,
|
|
image: &mut ManagedImage,
|
|
) -> Result<(), Error> {
|
|
#[cfg(not(feature = "dds-bc67"))]
|
|
{
|
|
let _ = (block, signed, block_x, block_y, width, height, image);
|
|
Err(parse(128, "DDS BC6H support is disabled"))
|
|
}
|
|
#[cfg(feature = "dds-bc67")]
|
|
{
|
|
let mut decoded = [0.0f32; 4 * 4 * 3];
|
|
bcdec_rs::bc6h_float(block, &mut decoded, 4 * 3, signed);
|
|
for local_y in 0..4 {
|
|
for local_x in 0..4 {
|
|
let x = block_x * 4 + local_x;
|
|
let y = block_y * 4 + local_y;
|
|
if x < width && y < height {
|
|
let source = (local_y * 4 + local_x) * 3;
|
|
let target = y * width + x;
|
|
image.red[target] = float_channel(decoded[source]);
|
|
image.green[target] = float_channel(decoded[source + 1]);
|
|
image.blue[target] = float_channel(decoded[source + 2]);
|
|
image.alpha[target] = u8::MAX;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "dds-bc67")]
|
|
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
|
fn float_channel(value: f32) -> u8 {
|
|
(value.clamp(0.0, 1.0) * 255.0 + 0.5) as u8
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments, clippy::unnecessary_wraps)]
|
|
fn decode_bc7_block(
|
|
block: &[u8],
|
|
block_x: usize,
|
|
block_y: usize,
|
|
width: usize,
|
|
height: usize,
|
|
image: &mut ManagedImage,
|
|
) -> Result<(), Error> {
|
|
#[cfg(not(feature = "dds-bc67"))]
|
|
{
|
|
let _ = (block, block_x, block_y, width, height, image);
|
|
Err(parse(128, "DDS BC7 support is disabled"))
|
|
}
|
|
#[cfg(feature = "dds-bc67")]
|
|
{
|
|
let mut decoded = [0u8; 4 * 4 * 4];
|
|
bcdec_rs::bc7(block, &mut decoded, 4 * 4);
|
|
for local_y in 0..4 {
|
|
for local_x in 0..4 {
|
|
let x = block_x * 4 + local_x;
|
|
let y = block_y * 4 + local_y;
|
|
if x < width && y < height {
|
|
let source = (local_y * 4 + local_x) * 4;
|
|
let target = y * width + x;
|
|
image.red[target] = decoded[source];
|
|
image.green[target] = decoded[source + 1];
|
|
image.blue[target] = decoded[source + 2];
|
|
image.alpha[target] = decoded[source + 3];
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn blend_rgb565(
|
|
first: u16,
|
|
second: u16,
|
|
first_weight: u16,
|
|
second_weight: u16,
|
|
divisor: u16,
|
|
) -> [u8; 3] {
|
|
let components = [
|
|
((first >> 11) & 31, (second >> 11) & 31, 31),
|
|
((first >> 5) & 63, (second >> 5) & 63, 63),
|
|
(first & 31, second & 31, 31),
|
|
];
|
|
components.map(|(first, second, maximum)| {
|
|
let numerator = u32::from(first_weight * first + second_weight * second) * 255;
|
|
let denominator = u32::from(divisor * maximum);
|
|
u8::try_from((numerator + denominator / 2) / denominator).unwrap()
|
|
})
|
|
}
|
|
|
|
fn rgb565(value: u16) -> [u8; 3] {
|
|
[
|
|
expand_5(u8::try_from((value >> 11) & 31).unwrap()),
|
|
expand_6(u8::try_from((value >> 5) & 63).unwrap()),
|
|
expand_5(u8::try_from(value & 31).unwrap()),
|
|
]
|
|
}
|
|
|
|
fn dxt_alphas(block: &[u8], kind: u8) -> Result<[u8; 16], Error> {
|
|
let mut output = [u8::MAX; 16];
|
|
if kind == 3 {
|
|
let bits = u64::from_le_bytes(block[..8].try_into().map_err(|_| Error::Argument)?);
|
|
for (index, alpha) in output.iter_mut().enumerate() {
|
|
let nibble = u8::try_from((bits >> (index * 4)) & 15).unwrap();
|
|
*alpha = nibble * 17;
|
|
}
|
|
} else if kind == 5 {
|
|
let alpha0 = block[0];
|
|
let alpha1 = block[1];
|
|
let mut table = [0; 8];
|
|
table[0] = alpha0;
|
|
table[1] = alpha1;
|
|
if alpha0 > alpha1 {
|
|
for index in 1..=6 {
|
|
table[index + 1] = u8::try_from(
|
|
((7 - index) * usize::from(alpha0) + index * usize::from(alpha1)) / 7,
|
|
)
|
|
.unwrap();
|
|
}
|
|
} else {
|
|
for index in 1..=4 {
|
|
table[index + 1] = u8::try_from(
|
|
((5 - index) * usize::from(alpha0) + index * usize::from(alpha1)) / 5,
|
|
)
|
|
.unwrap();
|
|
}
|
|
table[6] = 0;
|
|
table[7] = u8::MAX;
|
|
}
|
|
let mut selectors = 0u64;
|
|
for index in 0..6 {
|
|
selectors |= u64::from(block[index + 2]) << (index * 8);
|
|
}
|
|
for (index, alpha) in output.iter_mut().enumerate() {
|
|
*alpha = table[usize::try_from((selectors >> (index * 3)) & 7).unwrap()];
|
|
}
|
|
}
|
|
Ok(output)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::io::Cursor;
|
|
|
|
fn tga_header(image_type: u8, width: u16, height: u16, depth: u8, descriptor: u8) -> Vec<u8> {
|
|
let mut header = vec![0; 18];
|
|
header[2] = image_type;
|
|
header[12..14].copy_from_slice(&width.to_le_bytes());
|
|
header[14..16].copy_from_slice(&height.to_le_bytes());
|
|
header[16] = depth;
|
|
header[17] = descriptor;
|
|
header
|
|
}
|
|
|
|
fn dds_header(width: u32, height: u32, fourcc: [u8; 4], bits: u32) -> Vec<u8> {
|
|
let mut header = vec![0; 128];
|
|
header[..4].copy_from_slice(b"DDS ");
|
|
header[4..8].copy_from_slice(&124u32.to_le_bytes());
|
|
header[12..16].copy_from_slice(&height.to_le_bytes());
|
|
header[16..20].copy_from_slice(&width.to_le_bytes());
|
|
header[20..24].copy_from_slice(&(width * bits.div_ceil(8)).to_le_bytes());
|
|
header[76..80].copy_from_slice(&32u32.to_le_bytes());
|
|
header[80..84].copy_from_slice(&0x40u32.to_le_bytes());
|
|
header[84..88].copy_from_slice(&fourcc);
|
|
header[88..92].copy_from_slice(&bits.to_le_bytes());
|
|
header
|
|
}
|
|
|
|
#[test]
|
|
fn reference_tga_encoder_bytes_are_exact_and_deterministic() {
|
|
let mut image = ManagedImage::new(1, 1, ManagedImageImageChannels::COLOR).unwrap();
|
|
image.red[0] = 1;
|
|
image.green[0] = 2;
|
|
image.blue[0] = 3;
|
|
let first = Targa::encode(image).unwrap();
|
|
assert_eq!(first.len(), 35);
|
|
assert_eq!(&first[..18], &tga_header(2, 1, 1, 24, 0));
|
|
assert_eq!(&first[18..21], &[3, 2, 1]);
|
|
assert!(first[21..].iter().all(|byte| *byte == 0));
|
|
|
|
let mut image = ManagedImage::new(
|
|
1,
|
|
1,
|
|
ManagedImageImageChannels::COLOR | ManagedImageImageChannels::ALPHA,
|
|
)
|
|
.unwrap();
|
|
image.red[0] = 10;
|
|
image.green[0] = 20;
|
|
image.blue[0] = 30;
|
|
image.alpha[0] = 40;
|
|
let encoded = Targa::encode(image).unwrap();
|
|
assert_eq!(&encoded[..18], &tga_header(2, 1, 1, 32, 0x20));
|
|
assert_eq!(&encoded[18..22], &[30, 20, 10, 40]);
|
|
|
|
let mut alpha = ManagedImage::new(1, 1, ManagedImageImageChannels::ALPHA).unwrap();
|
|
alpha.alpha[0] = 42;
|
|
let encoded = Targa::encode(alpha).unwrap();
|
|
assert_eq!(encoded.len(), 33);
|
|
assert_eq!(&encoded[..18], &tga_header(2, 1, 1, 8, 0x20));
|
|
assert_eq!(&encoded[18..22], &[42, 42, 42, 255]);
|
|
}
|
|
|
|
#[test]
|
|
fn tga_decoder_preserves_origin_depth_and_rle_packets() {
|
|
let mut bottom_left = tga_header(2, 2, 2, 24, 0);
|
|
bottom_left.extend_from_slice(&[
|
|
30, 20, 10, 60, 50, 40, // bottom row
|
|
90, 80, 70, 120, 110, 100, // top row
|
|
]);
|
|
let image = decode_tga(&bottom_left).unwrap();
|
|
assert_eq!(image.red, [70, 100, 10, 40]);
|
|
assert_eq!(image.green, [80, 110, 20, 50]);
|
|
assert_eq!(image.blue, [90, 120, 30, 60]);
|
|
|
|
let mut rle = tga_header(10, 4, 1, 24, 0x20);
|
|
rle.extend_from_slice(&[0x83, 3, 2, 1]);
|
|
let image = decode_tga(&rle).unwrap();
|
|
assert_eq!(image.red, [1; 4]);
|
|
assert_eq!(image.green, [2; 4]);
|
|
assert_eq!(image.blue, [3; 4]);
|
|
|
|
let mut indexed = tga_header(1, 2, 1, 8, 0x20);
|
|
indexed[1] = 1;
|
|
indexed[5..7].copy_from_slice(&2u16.to_le_bytes());
|
|
indexed[7] = 24;
|
|
indexed.extend_from_slice(&[0, 0, 255, 0, 255, 0, 0, 1]);
|
|
let image = decode_tga(&indexed).unwrap();
|
|
assert_eq!(image.red, [255, 0]);
|
|
assert_eq!(image.green, [0, 255]);
|
|
assert_eq!(image.blue, [0, 0]);
|
|
|
|
let mut top_right = tga_header(2, 2, 1, 24, 0x30);
|
|
top_right.extend_from_slice(&[0, 0, 255, 0, 255, 0]);
|
|
let image = decode_tga(&top_right).unwrap();
|
|
assert_eq!(image.red, [0, 255]);
|
|
assert_eq!(image.green, [255, 0]);
|
|
|
|
let mut gray_rle = tga_header(11, 3, 1, 8, 0x20);
|
|
gray_rle.extend_from_slice(&[0x82, 9]);
|
|
assert_eq!(decode_tga(&gray_rle).unwrap().red, [9; 3]);
|
|
|
|
let mut gray_alpha = tga_header(3, 1, 1, 16, 0x28);
|
|
gray_alpha.extend_from_slice(&[9, 200]);
|
|
let image = decode_tga(&gray_alpha).unwrap();
|
|
assert_eq!(image.red, [9]);
|
|
assert_eq!(image.alpha, [200]);
|
|
|
|
let mut rgba = tga_header(2, 1, 1, 32, 0x20);
|
|
rgba.extend_from_slice(&[3, 2, 1, 4]);
|
|
let image = decode_tga(&rgba).unwrap();
|
|
assert_eq!((image.red[0], image.green[0], image.blue[0]), (1, 2, 3));
|
|
assert_eq!(image.alpha, [4]);
|
|
|
|
let mut rgb555 = tga_header(2, 1, 1, 16, 0x20);
|
|
rgb555.extend_from_slice(&0x7c00u16.to_le_bytes());
|
|
assert_eq!(decode_tga(&rgb555).unwrap().red, [255]);
|
|
|
|
let mut argb1555 = tga_header(2, 1, 1, 16, 0x21);
|
|
argb1555.extend_from_slice(&0xfc00u16.to_le_bytes());
|
|
let image = decode_tga(&argb1555).unwrap();
|
|
assert_eq!(image.red, [255]);
|
|
assert_eq!(image.alpha, [255]);
|
|
|
|
let mut ancillary_palette = tga_header(2, 1, 1, 24, 0x20);
|
|
ancillary_palette[1] = 1;
|
|
ancillary_palette[5..7].copy_from_slice(&1u16.to_le_bytes());
|
|
ancillary_palette[7] = 24;
|
|
ancillary_palette.extend_from_slice(&[99, 98, 97, 3, 2, 1]);
|
|
assert_eq!(decode_tga(&ancillary_palette).unwrap().red, [1]);
|
|
}
|
|
|
|
#[test]
|
|
fn dds_uncompressed_rgb565_and_dxt1_decode_exact_pixels() {
|
|
let mut rgb565 = dds_header(2, 1, [0; 4], 16);
|
|
rgb565[92..96].copy_from_slice(&0xf800u32.to_le_bytes());
|
|
rgb565[96..100].copy_from_slice(&0x07e0u32.to_le_bytes());
|
|
rgb565[100..104].copy_from_slice(&0x001fu32.to_le_bytes());
|
|
rgb565.extend_from_slice(&[0x00, 0xf8, 0xe0, 0x07]);
|
|
let image = decode_dds(&rgb565).unwrap();
|
|
assert_eq!(image.red, [255, 0]);
|
|
assert_eq!(image.green, [0, 255]);
|
|
assert_eq!(image.blue, [0, 0]);
|
|
|
|
let mut dxt1 = dds_header(4, 4, *b"DXT1", 0);
|
|
dxt1.extend_from_slice(&[0x00, 0xf8, 0xe0, 0x07, 0, 0, 0, 0]);
|
|
let image = decode_dds(&dxt1).unwrap();
|
|
assert_eq!(image.red, [255; 16]);
|
|
assert_eq!(image.green, [0; 16]);
|
|
assert_eq!(image.blue, [0; 16]);
|
|
assert_eq!(image.alpha, [255; 16]);
|
|
|
|
let mut bc4 = dds_header(4, 4, *b"ATI1", 0);
|
|
bc4.extend_from_slice(&[200, 100, 0, 0, 0, 0, 0, 0]);
|
|
assert_eq!(decode_dds(&bc4).unwrap().red, [200; 16]);
|
|
|
|
let mut bc5 = dds_header(4, 4, *b"ATI2", 0);
|
|
bc5.extend_from_slice(&[200, 100, 0, 0, 0, 0, 0, 0]);
|
|
bc5.extend_from_slice(&[40, 20, 0, 0, 0, 0, 0, 0]);
|
|
let image = decode_dds(&bc5).unwrap();
|
|
assert_eq!(image.red, [200; 16]);
|
|
assert_eq!(image.green, [40; 16]);
|
|
assert_eq!(image.blue, [0; 16]);
|
|
|
|
let mut dx10_dds = dds_header(1, 1, *b"DX10", 0);
|
|
dx10_dds.extend_from_slice(&28u32.to_le_bytes());
|
|
dx10_dds.extend_from_slice(&[0; 16]);
|
|
dx10_dds.extend_from_slice(&[1, 2, 3, 4]);
|
|
let image = decode_dds(&dx10_dds).unwrap();
|
|
assert_eq!(image.red, [1]);
|
|
assert_eq!(image.green, [2]);
|
|
assert_eq!(image.blue, [3]);
|
|
assert_eq!(image.alpha, [4]);
|
|
|
|
#[cfg(feature = "dds-bc67")]
|
|
for (format, block, expected) in [
|
|
(
|
|
95u32,
|
|
[
|
|
0xaf, 0xf4, 0xd2, 0xbd, 0x07, 0x07, 0x1c, 0xf0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
],
|
|
[128, 128, 255, 255],
|
|
),
|
|
(
|
|
98u32,
|
|
[
|
|
0xa0, 0x40, 0xe0, 0xff, 0xff, 0xff, 0x03, 0x02, 0x02, 0, 0, 0, 0, 0, 0, 0,
|
|
],
|
|
[129, 128, 255, 255],
|
|
),
|
|
] {
|
|
// Blocks and exact pixels are derived from Pfim 0.11.4's published fixtures.
|
|
let mut compressed = dds_header(4, 4, *b"DX10", 0);
|
|
compressed.extend_from_slice(&format.to_le_bytes());
|
|
compressed.extend_from_slice(&[0; 16]);
|
|
compressed.extend_from_slice(&block);
|
|
let image = decode_dds(&compressed).unwrap();
|
|
assert_eq!(image.red, [expected[0]; 16]);
|
|
assert_eq!(image.green, [expected[1]; 16]);
|
|
assert_eq!(image.blue, [expected[2]; 16]);
|
|
assert_eq!(image.alpha, [expected[3]; 16]);
|
|
}
|
|
|
|
#[cfg(not(feature = "dds-bc67"))]
|
|
{
|
|
let mut compressed = dds_header(4, 4, *b"DX10", 0);
|
|
compressed.extend_from_slice(&95u32.to_le_bytes());
|
|
compressed.extend_from_slice(&[0; 16 + 16]);
|
|
assert!(matches!(decode_dds(&compressed), Err(Error::Parse { .. })));
|
|
}
|
|
|
|
let mut packed = dds_header(1, 2, [0; 4], 24);
|
|
packed[20..24].copy_from_slice(&4u32.to_le_bytes());
|
|
packed[92..96].copy_from_slice(&0x00ff_0000_u32.to_le_bytes());
|
|
packed[96..100].copy_from_slice(&0x0000_ff00_u32.to_le_bytes());
|
|
packed[100..104].copy_from_slice(&0x0000_00ff_u32.to_le_bytes());
|
|
packed.extend_from_slice(&[3, 2, 1, 6, 5, 4]);
|
|
let image = decode_dds(&packed).unwrap();
|
|
assert_eq!(image.red, [1, 4]);
|
|
assert_eq!(image.green, [2, 5]);
|
|
assert_eq!(image.blue, [3, 6]);
|
|
|
|
assert_eq!(blend_rgb565(0xf800, 0, 2, 1, 3), [170, 0, 0]);
|
|
}
|
|
|
|
#[test]
|
|
fn stream_dispatch_and_malformed_sizes_return_typed_errors() {
|
|
let mut tga = tga_header(3, 1, 1, 8, 0x20);
|
|
tga.push(77);
|
|
let image = Targa::decode_to_managed_image_with_stream(Box::new(Cursor::new(tga))).unwrap();
|
|
assert_eq!(image.red, [77]);
|
|
|
|
let huge = tga_header(2, u16::MAX, u16::MAX, 24, 0);
|
|
assert_eq!(decode_tga(&huge), Err(Error::Argument));
|
|
assert!(matches!(
|
|
decode_tga(&tga_header(10, 1, 1, 24, 0)),
|
|
Err(Error::Parse { .. })
|
|
));
|
|
let mut oversized_packet = tga_header(10, 1, 1, 24, 0);
|
|
oversized_packet.extend_from_slice(&[0x81, 0, 0, 0]);
|
|
assert!(matches!(
|
|
decode_tga(&oversized_packet),
|
|
Err(Error::Parse { .. })
|
|
));
|
|
assert!(matches!(decode_dds(b"DDS "), Err(Error::Parse { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn dxt3_and_dxt5_alpha_tables_are_exact() {
|
|
let dxt3 = [0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe];
|
|
let alpha = dxt_alphas(&dxt3, 3).unwrap();
|
|
assert_eq!(&alpha[..4], &[0, 17, 34, 51]);
|
|
assert_eq!(alpha[15], 255);
|
|
|
|
let mut dxt5 = [0; 8];
|
|
dxt5[0] = 255;
|
|
dxt5[1] = 0;
|
|
dxt5[2] = 0b0000_0010;
|
|
let alpha = dxt_alphas(&dxt5, 5).unwrap();
|
|
assert_eq!(alpha[0], 218);
|
|
assert_eq!(alpha[1], 255);
|
|
}
|
|
}
|