//! 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 an in-memory TGA or DDS payload without copying it through the /// compatibility stream boundary. /// /// # Errors /// /// Returns a typed error for oversized, malformed, or unsupported input. pub fn decode_to_managed_image_with_bytes(data: &[u8]) -> Result { decode(data) } /// 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 { 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, ) -> Result { 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, 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(reader: &mut R) -> Result, 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 { 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 { 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 { 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 { 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)?; if !rle && !grayscale && palette.is_none() && matches!(depth, 24 | 32) && header[17] & 0x30 == 0x20 { decode_plain_truecolor(bytes, position, pixels, pixel_bytes, depth, &mut image)?; return Ok(image); } 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 decode_plain_truecolor( bytes: &[u8], position: usize, pixels: usize, pixel_bytes: usize, depth: u8, image: &mut ManagedImage, ) -> Result<(), Error> { let byte_length = pixels.checked_mul(pixel_bytes).ok_or(Error::Argument)?; let end = position.checked_add(byte_length).ok_or(Error::Argument)?; let data = bytes .get(position..end) .ok_or_else(|| parse(position, "truncated TGA pixel data"))?; for (target, pixel) in data.chunks_exact(pixel_bytes).enumerate() { image.blue[target] = pixel[0]; image.green[target] = pixel[1]; image.red[target] = pixel[2]; if depth == 32 { image.alpha[target] = pixel[3]; } } Ok(()) } 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, 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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); } }