diff options
Diffstat (limited to 'crates/texm')
| -rw-r--r-- | crates/texm/src/error.rs | 24 | ||||
| -rw-r--r-- | crates/texm/src/lib.rs | 163 | ||||
| -rw-r--r-- | crates/texm/src/tests.rs | 45 |
3 files changed, 232 insertions, 0 deletions
diff --git a/crates/texm/src/error.rs b/crates/texm/src/error.rs index a5dda77..38e32ca 100644 --- a/crates/texm/src/error.rs +++ b/crates/texm/src/error.rs @@ -23,6 +23,15 @@ pub enum Error { expected_end: usize, actual_size: usize, }, + MipIndexOutOfRange { + requested: usize, + mip_count: usize, + }, + MipDataOutOfBounds { + offset: usize, + size: usize, + payload_size: usize, + }, InvalidPageMagic, InvalidPageSize { expected: usize, @@ -50,6 +59,21 @@ impl fmt::Display for Error { f, "Texm core data out of bounds: expected_end={expected_end}, actual_size={actual_size}" ), + Self::MipIndexOutOfRange { + requested, + mip_count, + } => write!( + f, + "Texm mip index out of range: requested={requested}, mip_count={mip_count}" + ), + Self::MipDataOutOfBounds { + offset, + size, + payload_size, + } => write!( + f, + "Texm mip data out of bounds: offset={offset}, size={size}, payload_size={payload_size}" + ), Self::InvalidPageMagic => write!(f, "Texm tail exists but Page magic is missing"), Self::InvalidPageSize { expected, actual } => { write!(f, "invalid Page chunk size: expected={expected}, actual={actual}") diff --git a/crates/texm/src/lib.rs b/crates/texm/src/lib.rs index c3616d5..5d8b594 100644 --- a/crates/texm/src/lib.rs +++ b/crates/texm/src/lib.rs @@ -90,6 +90,13 @@ impl Texture { } } +#[derive(Clone, Debug)] +pub struct DecodedMip { + pub width: u32, + pub height: u32, + pub rgba8: Vec<u8>, +} + pub fn parse_texm(payload: &[u8]) -> Result<Texture> { if payload.len() < 32 { return Err(Error::HeaderTooSmall { @@ -195,6 +202,81 @@ pub fn parse_texm(payload: &[u8]) -> Result<Texture> { }) } +pub fn decode_mip_rgba8(texture: &Texture, payload: &[u8], mip_index: usize) -> Result<DecodedMip> { + let Some(level) = texture.mip_levels.get(mip_index).copied() else { + return Err(Error::MipIndexOutOfRange { + requested: mip_index, + mip_count: texture.mip_levels.len(), + }); + }; + + let end = level + .offset + .checked_add(level.size) + .ok_or(Error::IntegerOverflow)?; + let Some(level_data) = payload.get(level.offset..end) else { + return Err(Error::MipDataOutOfBounds { + offset: level.offset, + size: level.size, + payload_size: payload.len(), + }); + }; + + let pixel_count = usize::try_from(level.width) + .ok() + .and_then(|w| { + usize::try_from(level.height) + .ok() + .map(|h| w.saturating_mul(h)) + }) + .ok_or(Error::IntegerOverflow)?; + let mut rgba = vec![0u8; pixel_count.saturating_mul(4)]; + + match texture.header.format { + PixelFormat::Indexed8 => { + let palette = texture.palette.as_ref().ok_or(Error::IntegerOverflow)?; + for (i, &index) in level_data.iter().enumerate() { + if i >= pixel_count { + break; + } + let poff = usize::from(index).saturating_mul(4); + if poff + 3 >= palette.len() { + continue; + } + let out = i.saturating_mul(4); + rgba[out] = palette[poff]; + rgba[out + 1] = palette[poff + 1]; + rgba[out + 2] = palette[poff + 2]; + rgba[out + 3] = palette[poff + 3]; + } + } + PixelFormat::Rgb565 => { + decode_words(level_data, pixel_count, &mut rgba, decode_rgb565); + } + PixelFormat::Rgb556 => { + decode_words(level_data, pixel_count, &mut rgba, decode_rgb556); + } + PixelFormat::Argb4444 => { + decode_words(level_data, pixel_count, &mut rgba, decode_argb4444); + } + PixelFormat::LuminanceAlpha88 => { + decode_words(level_data, pixel_count, &mut rgba, decode_luminance_alpha88); + } + PixelFormat::Rgb888 => { + decode_dwords(level_data, pixel_count, &mut rgba, decode_rgb888x); + } + PixelFormat::Argb8888 => { + decode_dwords(level_data, pixel_count, &mut rgba, decode_argb8888); + } + } + + Ok(DecodedMip { + width: level.width, + height: level.height, + rgba8: rgba, + }) +} + fn parse_page_tail(payload: &[u8], core_end: usize) -> Result<Vec<PageRect>> { if core_end == payload.len() { return Ok(Vec::new()); @@ -254,5 +336,86 @@ fn read_i16(data: &[u8], offset: usize) -> Result<i16> { Ok(i16::from_le_bytes(arr)) } +fn decode_words(data: &[u8], pixel_count: usize, rgba: &mut [u8], decode: fn(u16) -> [u8; 4]) { + for i in 0..pixel_count { + let off = i.saturating_mul(2); + let Some(bytes) = data.get(off..off + 2) else { + break; + }; + let word = u16::from_le_bytes([bytes[0], bytes[1]]); + let px = decode(word); + let out = i.saturating_mul(4); + rgba[out..out + 4].copy_from_slice(&px); + } +} + +fn decode_dwords(data: &[u8], pixel_count: usize, rgba: &mut [u8], decode: fn(u32) -> [u8; 4]) { + for i in 0..pixel_count { + let off = i.saturating_mul(4); + let Some(bytes) = data.get(off..off + 4) else { + break; + }; + let dword = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); + let px = decode(dword); + let out = i.saturating_mul(4); + rgba[out..out + 4].copy_from_slice(&px); + } +} + +fn expand5(v: u16) -> u8 { + ((u32::from(v) * 255 + 15) / 31) as u8 +} + +fn expand6(v: u16) -> u8 { + ((u32::from(v) * 255 + 31) / 63) as u8 +} + +fn expand4(v: u16) -> u8 { + (u32::from(v) * 17) as u8 +} + +fn decode_rgb565(word: u16) -> [u8; 4] { + let r = expand5((word >> 11) & 0x1F); + let g = expand6((word >> 5) & 0x3F); + let b = expand5(word & 0x1F); + [r, g, b, 255] +} + +fn decode_rgb556(word: u16) -> [u8; 4] { + let r = expand5((word >> 11) & 0x1F); + let g = expand5((word >> 6) & 0x1F); + let b = expand6(word & 0x3F); + [r, g, b, 255] +} + +fn decode_argb4444(word: u16) -> [u8; 4] { + let a = expand4((word >> 12) & 0x0F); + let r = expand4((word >> 8) & 0x0F); + let g = expand4((word >> 4) & 0x0F); + let b = expand4(word & 0x0F); + [r, g, b, a] +} + +fn decode_luminance_alpha88(word: u16) -> [u8; 4] { + let l = ((word >> 8) & 0xFF) as u8; + let a = (word & 0xFF) as u8; + [l, l, l, a] +} + +fn decode_rgb888x(dword: u32) -> [u8; 4] { + let r = (dword & 0xFF) as u8; + let g = ((dword >> 8) & 0xFF) as u8; + let b = ((dword >> 16) & 0xFF) as u8; + [r, g, b, 255] +} + +fn decode_argb8888(dword: u32) -> [u8; 4] { + let a = (dword & 0xFF) as u8; + let r = ((dword >> 8) & 0xFF) as u8; + let g = ((dword >> 16) & 0xFF) as u8; + let b = ((dword >> 24) & 0xFF) as u8; + [r, g, b, a] +} + #[cfg(test)] mod tests; diff --git a/crates/texm/src/tests.rs b/crates/texm/src/tests.rs index d021346..3d990bf 100644 --- a/crates/texm/src/tests.rs +++ b/crates/texm/src/tests.rs @@ -116,6 +116,26 @@ fn texm_parse_minimal_argb8888_no_page() { } #[test] +fn texm_decode_minimal_argb8888_no_page() { + let mut payload = Vec::new(); + payload.extend_from_slice(&TEXM_MAGIC.to_le_bytes()); + payload.extend_from_slice(&1u32.to_le_bytes()); // width + payload.extend_from_slice(&1u32.to_le_bytes()); // height + payload.extend_from_slice(&1u32.to_le_bytes()); // mip_count + payload.extend_from_slice(&0u32.to_le_bytes()); // flags4 + payload.extend_from_slice(&0u32.to_le_bytes()); // flags5 + payload.extend_from_slice(&0u32.to_le_bytes()); // unk6 + payload.extend_from_slice(&8888u32.to_le_bytes()); // format + payload.extend_from_slice(&[0x40, 0x11, 0x22, 0x33]); // A,R,G,B in little-endian order + + let parsed = parse_texm(&payload).expect("failed to parse minimal texm"); + let decoded = decode_mip_rgba8(&parsed, &payload, 0).expect("failed to decode mip"); + assert_eq!(decoded.width, 1); + assert_eq!(decoded.height, 1); + assert_eq!(decoded.rgba8, vec![0x11, 0x22, 0x33, 0x40]); +} + +#[test] fn texm_parse_indexed_with_page_chunk() { let mut payload = Vec::new(); payload.extend_from_slice(&TEXM_MAGIC.to_le_bytes()); @@ -148,3 +168,28 @@ fn texm_parse_indexed_with_page_chunk() { } ); } + +#[test] +fn texm_decode_indexed_with_palette() { + let mut payload = Vec::new(); + payload.extend_from_slice(&TEXM_MAGIC.to_le_bytes()); + payload.extend_from_slice(&2u32.to_le_bytes()); // width + payload.extend_from_slice(&1u32.to_le_bytes()); // height + payload.extend_from_slice(&1u32.to_le_bytes()); // mip_count + payload.extend_from_slice(&0u32.to_le_bytes()); // flags4 + payload.extend_from_slice(&0u32.to_le_bytes()); // flags5 + payload.extend_from_slice(&0u32.to_le_bytes()); // unk6 + payload.extend_from_slice(&0u32.to_le_bytes()); // format indexed8 + + let mut palette = [0u8; 1024]; + palette[4..8].copy_from_slice(&[10, 20, 30, 255]); // index 1 + palette[8..12].copy_from_slice(&[40, 50, 60, 200]); // index 2 + payload.extend_from_slice(&palette); + payload.extend_from_slice(&[1u8, 2u8]); // two pixels + + let parsed = parse_texm(&payload).expect("failed to parse indexed texm"); + let decoded = decode_mip_rgba8(&parsed, &payload, 0).expect("failed to decode indexed texm"); + assert_eq!(decoded.width, 2); + assert_eq!(decoded.height, 1); + assert_eq!(decoded.rgba8, vec![10, 20, 30, 255, 40, 50, 60, 200]); +} |
