Add psb rl compressed image support

This commit is contained in:
2025-10-13 23:09:17 +08:00
parent c99fd6450a
commit e791d967c6
3 changed files with 231 additions and 5 deletions

View File

@@ -2,3 +2,4 @@
pub mod dref;
pub mod pimg;
pub mod psb;
pub mod rle;

View File

@@ -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<Resource> {
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<Resource> {
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<TlgInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
rle: Option<RLPixelInfo>,
}
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)?)

118
src/scripts/emote/rle.rs Normal file
View File

@@ -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<T: Read + Seek>(
mut input: T,
align: usize,
actual_size: Option<usize>,
) -> Result<Vec<u8>> {
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<T: Read + Seek>(input: &mut T, align: usize) -> Result<(usize, u8, Vec<u8>)> {
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<T: Read + Seek>(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<T: Read + Seek>(mut input: T, align: usize) -> Result<Vec<u8>> {
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)
}