mirror of
https://github.com/lifegpc/msg-tool.git
synced 2026-07-08 01:31:53 +08:00
Add psb rl compressed image support
This commit is contained in:
@@ -2,3 +2,4 @@
|
|||||||
pub mod dref;
|
pub mod dref;
|
||||||
pub mod pimg;
|
pub mod pimg;
|
||||||
pub mod psb;
|
pub mod psb;
|
||||||
|
pub mod rle;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
//! Basic Handle for all emote PSB files.
|
//! Basic Handle for all emote PSB files.
|
||||||
|
use super::rle::*;
|
||||||
use crate::ext::io::*;
|
use crate::ext::io::*;
|
||||||
use crate::ext::psb::*;
|
use crate::ext::psb::*;
|
||||||
use crate::scripts::base::*;
|
use crate::scripts::base::*;
|
||||||
@@ -128,7 +129,11 @@ impl Psb {
|
|||||||
path: String,
|
path: String,
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
) -> Result<Resource> {
|
) -> 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) {
|
if self.config.psb_process_tlg && is_valid_tlg(&data) {
|
||||||
let tlg = load_tlg(MemReaderRef::new(&data))?;
|
let tlg = load_tlg(MemReaderRef::new(&data))?;
|
||||||
res.tlg = Some(TlgInfo::from_tlg(&tlg, self.encoding));
|
res.tlg = Some(TlgInfo::from_tlg(&tlg, self.encoding));
|
||||||
@@ -159,6 +164,39 @@ impl Psb {
|
|||||||
}
|
}
|
||||||
Ok(res)
|
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)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
@@ -210,11 +248,19 @@ impl TlgInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
struct RLPixelInfo {
|
||||||
|
width: i64,
|
||||||
|
height: i64,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
struct Resource {
|
struct Resource {
|
||||||
path: String,
|
path: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
tlg: Option<TlgInfo>,
|
tlg: Option<TlgInfo>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
rle: Option<RLPixelInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Script for Psb {
|
impl Script for Psb {
|
||||||
@@ -245,10 +291,35 @@ impl Script for Psb {
|
|||||||
};
|
};
|
||||||
for (i, data) in self.psb.resources().iter().enumerate() {
|
for (i, data) in self.psb.resources().iter().enumerate() {
|
||||||
let i = i as u64;
|
let i = i as u64;
|
||||||
let res_name = self
|
let res_path = self.psb.root().find_resource_key(i, vec![]);
|
||||||
.psb
|
if let Some(path) = &res_path {
|
||||||
.root()
|
if path.len() >= 2 && *path.last().unwrap() == "pixel" {
|
||||||
.find_resource_key(i, vec![])
|
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("/"))
|
.map(|s| s.join("/"))
|
||||||
.unwrap_or(format!("res_{}", i));
|
.unwrap_or(format!("res_{}", i));
|
||||||
let res_name = sanitize_path(&res_name);
|
let res_name = sanitize_path(&res_name);
|
||||||
@@ -325,6 +396,42 @@ fn read_resource(
|
|||||||
let mut writer = MemWriter::new();
|
let mut writer = MemWriter::new();
|
||||||
save_tlg(&tlg, &mut writer)?;
|
save_tlg(&tlg, &mut writer)?;
|
||||||
Ok(writer.into_inner())
|
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 {
|
} else {
|
||||||
let path = folder_path.join(&res.path);
|
let path = folder_path.join(&res.path);
|
||||||
Ok(std::fs::read(&path)?)
|
Ok(std::fs::read(&path)?)
|
||||||
|
|||||||
118
src/scripts/emote/rle.rs
Normal file
118
src/scripts/emote/rle.rs
Normal 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user