Files
MetaCrate/crates/libremetaverse/src/targa.rs

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);
}
}