From 5d1573fc82a604ca37c876a4e09932e0a020c702 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sun, 9 Aug 2026 02:51:39 +0000 Subject: [PATCH] Implement native TGA and DDS compatibility (#39) --- Cargo.lock | 7 + README.md | 12 + api/SHIM-COVERAGE.md | 2 +- crates/libremetaverse/Cargo.toml | 5 + crates/libremetaverse/src/generated.rs | 31 +- crates/libremetaverse/src/lib.rs | 1 + crates/libremetaverse/src/targa.rs | 1243 ++++++++++++++++++++++++ tests/api-compile/Cargo.lock | 7 + tests/red-suite-baseline.json | 2 +- tools/generate_api_shims.py | 6 +- 10 files changed, 1285 insertions(+), 31 deletions(-) create mode 100644 crates/libremetaverse/src/targa.rs diff --git a/Cargo.lock b/Cargo.lock index 3b72472..fbc1161 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bcdec_rs" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09c37bc0e9f0924b7dae9988265ef3c76c88538f41a3b06caf4bed07cee5226" + [[package]] name = "block-buffer" version = "0.10.4" @@ -183,6 +189,7 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" name = "libremetaverse" version = "0.0.1" dependencies = [ + "bcdec_rs", "libremetaverse-imaging", "libremetaverse-structured-data", "libremetaverse-types", diff --git a/README.md b/README.md index 4a19c3f..0814078 100644 --- a/README.md +++ b/README.md @@ -129,5 +129,17 @@ constructors and resizers cap decoded images at 4096 by 4096 worth of pixels, codec adapters cap buffered encoded input at 64 MiB, and canonical interleaving accepts explicit row strides without exposing codec-library types through the core abstraction. +The native TGA/Pfim-compatible path decodes uncompressed and RLE true-color, +grayscale, and indexed TGA data with all descriptor orientations, plus bounded +legacy and DX10 DDS packed luminance/RGB(A) layouts and BC1 through BC7 blocks. +TGA output preserves the reference header, BGR(A) order, descriptor choice, +alpha-only expansion, and deterministic trailing padding. File and stream +inputs are capped before decoding, and all dimensions, packed rows, palettes, +packets, masks, and block indices use checked arithmetic with positioned parse +errors. +Legacy and DX10 BC1 through BC5 decoding is built in. The default `dds-bc67` +feature adds BC6H/BC7 decoding through the safe, pure-Rust `bcdec_rs` crate; +disabling default features removes that optional dependency and makes those +two formats return a typed unsupported-format error. The controlled audit aggregates every expected failure by standardized C# member ID and rejects unrelated fixture, assertion, compile, or symbol errors. diff --git a/api/SHIM-COVERAGE.md b/api/SHIM-COVERAGE.md index 9296d08..3bbbcbc 100644 --- a/api/SHIM-COVERAGE.md +++ b/api/SHIM-COVERAGE.md @@ -4,7 +4,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand. | Assembly | Types | Members | Status | |---|---:|---:|---| -| `LibreMetaverse` | 2,711 | 27,281 | callable failure-only shim | +| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 1 type / 3 members; remaining surface is callable failure-only shims | | `LibreMetaverse.Imaging.Abstractions` | 3 | 20 | native implementation: 3 types / 20 members; no generated shims remain | | `LibreMetaverse.Imaging.Skia` | 1 | 3 | callable failure-only shim | | `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim | diff --git a/crates/libremetaverse/Cargo.toml b/crates/libremetaverse/Cargo.toml index c8cff26..e164d73 100644 --- a/crates/libremetaverse/Cargo.toml +++ b/crates/libremetaverse/Cargo.toml @@ -7,7 +7,12 @@ license.workspace = true repository.workspace = true description = "Rust rewrite shell for the LibreMetaverse client library" +[features] +default = ["dds-bc67"] +dds-bc67 = ["dep:bcdec_rs"] + [dependencies] +bcdec_rs = { version = "0.2.0", optional = true } libremetaverse-imaging = { path = "../libremetaverse-imaging" } libremetaverse-structured-data = { path = "../libremetaverse-structured-data" } libremetaverse-types = { path = "../libremetaverse-types" } diff --git a/crates/libremetaverse/src/generated.rs b/crates/libremetaverse/src/generated.rs index 53fb8eb..a4f5430 100644 --- a/crates/libremetaverse/src/generated.rs +++ b/crates/libremetaverse/src/generated.rs @@ -34081,33 +34081,10 @@ pub mod imaging { } /// C# type: `T:LibreMetaverse.Imaging.Targa`. - pub struct Targa; - impl Targa { - /// C# member: `M:LibreMetaverse.Imaging.Targa.DecodeToManagedImage(System.IO.Stream)`. - pub fn decode_to_managed_image_with_stream( - stream: Box, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Imaging.Targa.DecodeToManagedImage(System.IO.Stream)", - ) - } - /// C# member: `M:LibreMetaverse.Imaging.Targa.DecodeToManagedImage(System.String)`. - pub fn decode_to_managed_image_with_string( - file_name: String, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Imaging.Targa.DecodeToManagedImage(System.String)", - ) - } - /// C# member: `M:LibreMetaverse.Imaging.Targa.Encode(LibreMetaverse.Imaging.ManagedImage)`. - pub fn encode( - image: libremetaverse_imaging::ManagedImage, - ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Imaging.Targa.Encode(LibreMetaverse.Imaging.ManagedImage)", - ) - } - } + /// C# member: `M:LibreMetaverse.Imaging.Targa.DecodeToManagedImage(System.IO.Stream)`. + /// C# member: `M:LibreMetaverse.Imaging.Targa.DecodeToManagedImage(System.String)`. + /// C# member: `M:LibreMetaverse.Imaging.Targa.Encode(LibreMetaverse.Imaging.ManagedImage)`. + pub use crate::targa::Targa; } pub mod import_export { diff --git a/crates/libremetaverse/src/lib.rs b/crates/libremetaverse/src/lib.rs index d9532e5..e1f65df 100644 --- a/crates/libremetaverse/src/lib.rs +++ b/crates/libremetaverse/src/lib.rs @@ -3,6 +3,7 @@ extern crate self as libremetaverse; mod generated; +mod targa; #[cfg(test)] mod appearance_baker_semantics; diff --git a/crates/libremetaverse/src/targa.rs b/crates/libremetaverse/src/targa.rs new file mode 100644 index 0000000..b097d70 --- /dev/null +++ b/crates/libremetaverse/src/targa.rs @@ -0,0 +1,1243 @@ +//! 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 { + 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)?; + 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, 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); + } +} diff --git a/tests/api-compile/Cargo.lock b/tests/api-compile/Cargo.lock index 94ff9dd..c7c8928 100644 --- a/tests/api-compile/Cargo.lock +++ b/tests/api-compile/Cargo.lock @@ -8,6 +8,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bcdec_rs" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09c37bc0e9f0924b7dae9988265ef3c76c88538f41a3b06caf4bed07cee5226" + [[package]] name = "block-buffer" version = "0.10.4" @@ -183,6 +189,7 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" name = "libremetaverse" version = "0.0.1" dependencies = [ + "bcdec_rs", "libremetaverse-imaging", "libremetaverse-structured-data", "libremetaverse-types", diff --git a/tests/red-suite-baseline.json b/tests/red-suite-baseline.json index 56c34d7..da49481 100644 --- a/tests/red-suite-baseline.json +++ b/tests/red-suite-baseline.json @@ -101,5 +101,5 @@ "LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUUID::test", "LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUndef::test" ], - "support_passes": 104 + "support_passes": 109 } diff --git a/tools/generate_api_shims.py b/tools/generate_api_shims.py index 2bdf4da..5e4d720 100644 --- a/tools/generate_api_shims.py +++ b/tools/generate_api_shims.py @@ -38,6 +38,7 @@ TARGETS = { # implementations. The generated module keeps catalog markers and re-exports # the hand-written type so coverage remains deterministic. NATIVE_TYPES = { + "T:LibreMetaverse.Imaging.Targa": "crate::targa::Targa", "T:LibreMetaverse.Imaging.ITextureCodec": "crate::managed_image::ITextureCodec", "T:LibreMetaverse.Imaging.ManagedImage": "crate::managed_image::ManagedImage", "T:LibreMetaverse.Imaging.ManagedImage.ImageChannels": "crate::managed_image::ManagedImageImageChannels", @@ -1019,14 +1020,15 @@ def coverage_report(catalog: dict, coverage: dict[str, tuple[int, int, bool]]) - if item["doc_id"] in native_type_ids or member["doc_id"] in NATIVE_MEMBER_BODIES } if native_type_ids or native_member_ids: + type_noun = "type" if len(native_type_ids) == 1 else "types" if len(native_member_ids) == members: status = ( - f"native implementation: {len(native_type_ids):,} types / " + f"native implementation: {len(native_type_ids):,} {type_noun} / " f"{len(native_member_ids):,} members; no generated shims remain" ) else: status = ( - f"native implementation: {len(native_type_ids):,} types / " + f"native implementation: {len(native_type_ids):,} {type_noun} / " f"{len(native_member_ids):,} members; remaining surface is callable failure-only shims" ) else: