diff --git a/src/scripts/emote/mod.rs b/src/scripts/emote/mod.rs index 1ba6e53..3263212 100644 --- a/src/scripts/emote/mod.rs +++ b/src/scripts/emote/mod.rs @@ -2,3 +2,4 @@ pub mod dref; pub mod pimg; pub mod psb; +pub mod rle; diff --git a/src/scripts/emote/psb.rs b/src/scripts/emote/psb.rs index b067e53..fcdf918 100644 --- a/src/scripts/emote/psb.rs +++ b/src/scripts/emote/psb.rs @@ -1,4 +1,5 @@ //! Basic Handle for all emote PSB files. +use super::rle::*; use crate::ext::io::*; use crate::ext::psb::*; use crate::scripts::base::*; @@ -128,7 +129,11 @@ impl Psb { path: String, data: &[u8], ) -> Result { - let mut res = Resource { path, tlg: None }; + let mut res = Resource { + path, + tlg: None, + rle: None, + }; if self.config.psb_process_tlg && is_valid_tlg(&data) { let tlg = load_tlg(MemReaderRef::new(&data))?; res.tlg = Some(TlgInfo::from_tlg(&tlg, self.encoding)); @@ -159,6 +164,39 @@ impl Psb { } Ok(res) } + + fn output_rle_resource( + &self, + folder_path: &std::path::PathBuf, + path: String, + data: &[u8], + width: i64, + height: i64, + ) -> Result { + let mut res = Resource { + path, + tlg: None, + rle: Some(RLPixelInfo { width, height }), + }; + let decompressed = rl_decompress(MemReaderRef::new(data), 4, None)?; + let outtype = self.config.image_type.unwrap_or(ImageOutputType::Png); + res.path = { + let mut pb = std::path::PathBuf::from(&res.path); + pb.set_extension(outtype.as_ref()); + pb.to_string_lossy().to_string() + }; + let path = folder_path.join(&res.path); + make_sure_dir_exists(&path)?; + let img = ImageData { + width: width as u32, + height: height as u32, + color_type: ImageColorType::Bgra, + depth: 8, + data: decompressed, + }; + encode_img(img, outtype, &path.to_string_lossy(), &self.config)?; + Ok(res) + } } #[derive(Debug, Deserialize, Serialize)] @@ -210,11 +248,19 @@ impl TlgInfo { } } +#[derive(Debug, Deserialize, Serialize)] +struct RLPixelInfo { + width: i64, + height: i64, +} + #[derive(Debug, Deserialize, Serialize)] struct Resource { path: String, #[serde(skip_serializing_if = "Option::is_none")] tlg: Option, + #[serde(skip_serializing_if = "Option::is_none")] + rle: Option, } impl Script for Psb { @@ -245,10 +291,35 @@ impl Script for Psb { }; for (i, data) in self.psb.resources().iter().enumerate() { let i = i as u64; - let res_name = self - .psb - .root() - .find_resource_key(i, vec![]) + let res_path = self.psb.root().find_resource_key(i, vec![]); + if let Some(path) = &res_path { + if path.len() >= 2 && *path.last().unwrap() == "pixel" { + let pb_data = self.psb.root(); + let mut pb_data = &pb_data[*path.first().unwrap()]; + for p in path.iter().take(path.len() - 1).skip(1) { + pb_data = &pb_data[*p]; + } + let width = pb_data["width"].as_i64(); + let height = pb_data["height"].as_i64(); + let compress = pb_data["compress"].as_str(); + if let (Some(w), Some(h), Some(c)) = (width, height, compress) { + if c == "RL" { + let res_name: Vec<_> = path + .iter() + .take(path.len() - 1) + .map(|s| s.to_string()) + .collect(); + let res_name = res_name.join("/"); + let res_name = sanitize_path(&res_name); + let res = + self.output_rle_resource(&folder_path, res_name, data, w, h)?; + resources.push(res); + continue; + } + } + } + } + let res_name = res_path .map(|s| s.join("/")) .unwrap_or(format!("res_{}", i)); let res_name = sanitize_path(&res_name); @@ -325,6 +396,42 @@ fn read_resource( let mut writer = MemWriter::new(); save_tlg(&tlg, &mut writer)?; Ok(writer.into_inner()) + } else if let Some(rle) = &res.rle { + let path = folder_path.join(&res.path); + let imgfmt = ImageOutputType::try_from(path.as_path())?; + let mut img = decode_img(imgfmt, &path.to_string_lossy())?; + if img.depth != 8 { + return Err(anyhow::anyhow!( + "Only 8-bit images are supported for RLE conversion" + )); + } + if img.color_type == ImageColorType::Rgba { + convert_rgba_to_bgra(&mut img)?; + } else if img.color_type == ImageColorType::Rgb { + convert_rgb_to_bgr(&mut img)?; + convert_bgr_to_bgra(&mut img)?; + } else if img.color_type == ImageColorType::Bgr { + convert_bgr_to_bgra(&mut img)?; + } + if img.color_type != ImageColorType::Bgra { + return Err(anyhow::anyhow!( + "Only BGRA images are supported for RLE conversion" + )); + } + if img.width as i64 != rle.width { + eprintln!( + "Warning: Image width {} does not match RLE width {}", + img.width, rle.width + ); + } + if img.height as i64 != rle.height { + eprintln!( + "Warning: Image height {} does not match RLE height {}", + img.height, rle.height + ); + } + let compressed = rl_compress(MemReaderRef::new(&img.data), 4)?; + Ok(compressed) } else { let path = folder_path.join(&res.path); Ok(std::fs::read(&path)?) diff --git a/src/scripts/emote/rle.rs b/src/scripts/emote/rle.rs new file mode 100644 index 0000000..d6b6dab --- /dev/null +++ b/src/scripts/emote/rle.rs @@ -0,0 +1,118 @@ +//! RL Encode used in mtn files +use crate::ext::io::*; +use anyhow::Result; +use std::io::{Read, Seek, SeekFrom}; + +const LZSS_LOOKAHED: usize = 1 << 7; + +/// Decompress RL data +/// * `align` - alignment. usually 4 +/// * `actual_size` - if known, set it to preallocate memory +pub fn rl_decompress( + mut input: T, + align: usize, + actual_size: Option, +) -> Result> { + let mut output = if let Some(size) = actual_size { + Vec::with_capacity(size) + } else { + Vec::new() + }; + let mut readed = input.stream_position()?; + let len = input.stream_length()?; + while readed < len { + let current = input.read_u8()? as usize; + readed += 1; + let count; + if (current & LZSS_LOOKAHED) != 0 { + count = (current ^ LZSS_LOOKAHED) + 3; + let buf = input.read_exact_vec(align)?; + readed += align as u64; + for _ in 0..count { + output.extend_from_slice(&buf); + } + } else { + count = (current + 1) * align; + let buf = input.read_exact_vec(count)?; + readed += count as u64; + output.extend_from_slice(&buf); + } + } + Ok(output) +} + +fn compress_bound(input: &mut T, align: usize) -> Result<(usize, u8, Vec)> { + let pos = input.stream_position()?; + let mut curpos = pos; + let len = input.stream_length()?; + let mut buffer = vec![0u8; align]; + let mut tmp = vec![0u8; align]; + input.read_exact(&mut buffer)?; + curpos += align as u64; + let mut count = 1usize; + for _ in 1..LZSS_LOOKAHED + 2 { + if curpos >= len { + break; + } + input.read_exact(&mut tmp)?; + curpos += align as u64; + if buffer == tmp { + count += 1; + } else { + break; + } + } + input.seek(SeekFrom::Start(pos))?; + if count >= 3 { + return Ok((count, (count - 3) as u8 | LZSS_LOOKAHED as u8, buffer)); + } + Ok((0, 0, buffer)) +} + +fn compress_bound_np(input: &mut T, align: usize) -> Result<(usize, u8)> { + let pos = input.stream_position()?; + let mut curpos = pos; + let len = input.stream_length()?; + input.seek_relative(align as i64)?; + curpos += align as u64; + let mut count = 1; + for _ in 1..LZSS_LOOKAHED { + if curpos >= len { + break; + } + let (ncount, _cmd, _buf) = compress_bound(input, align)?; + if ncount == 0 { + input.seek_relative(align as i64)?; + count += 1; + curpos += align as u64; + } else { + break; + } + } + input.seek(SeekFrom::Start(pos))?; + Ok((count, (count - 1) as u8)) +} + +/// Compress data using RL +/// * `align` - alignment. usually 4 +pub fn rl_compress(mut input: T, align: usize) -> Result> { + let mut output = Vec::new(); + let len = input.stream_length()?; + let mut readed = input.stream_position()?; + while readed < len { + let (count, cmd, buf) = compress_bound(&mut input, align)?; + if count > 0 { + output.push(cmd); + output.extend_from_slice(&buf); + readed += (count * align) as u64; + input.seek_relative((count * align) as i64)?; + } else { + let (ncount, ncmd) = compress_bound_np(&mut input, align)?; + output.push(ncmd); + let buf = input.read_exact_vec(ncount * align)?; + output.extend_from_slice(&buf); + readed += (ncount * align) as u64; + } + } + Ok(output) +}