aboutsummaryrefslogtreecommitdiff
path: root/crates/texm
diff options
context:
space:
mode:
Diffstat (limited to 'crates/texm')
-rw-r--r--crates/texm/src/error.rs24
-rw-r--r--crates/texm/src/lib.rs163
-rw-r--r--crates/texm/src/tests.rs45
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]);
+}