From 8afc11b9432f281906f9fa3a978ceb7c3c9ccc22 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Wed, 17 Sep 2025 21:32:21 +0800 Subject: [PATCH] Add compress support for softpal pgd image file --- src/args.rs | 4 + src/main.rs | 2 + src/scripts/softpal/img/pgd/base.rs | 211 +++++++++++++++++++++++++++- src/scripts/softpal/img/pgd/ge.rs | 19 ++- src/types.rs | 3 + 5 files changed, 231 insertions(+), 8 deletions(-) diff --git a/src/args.rs b/src/args.rs index 23f1d24..887ca84 100644 --- a/src/args.rs +++ b/src/args.rs @@ -468,6 +468,10 @@ pub struct Arg { #[arg(long, global = true, action = ArgAction::SetTrue, visible_alias = "psb-no-tlg")] /// Do not process TLG images in PSB files. pub psb_no_process_tlg: bool, + #[cfg(feature = "softpal-img")] + #[arg(long, global = true, visible_alias = "pgd-fc")] + /// Whether to use fake compression for Softpal Pgd images + pub pgd_fake_compress: bool, #[command(subcommand)] /// Command pub command: Command, diff --git a/src/main.rs b/src/main.rs index 3607da1..ef24afe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2060,6 +2060,8 @@ fn main() { jxl_workers: arg.jxl_workers, #[cfg(feature = "emote-img")] psb_process_tlg: !arg.psb_no_process_tlg, + #[cfg(feature = "softpal-img")] + pgd_fake_compress: arg.pgd_fake_compress, }); match &arg.command { args::Command::Export { input, output } => { diff --git a/src/scripts/softpal/img/pgd/base.rs b/src/scripts/softpal/img/pgd/base.rs index b75b748..8de26e1 100644 --- a/src/scripts/softpal/img/pgd/base.rs +++ b/src/scripts/softpal/img/pgd/base.rs @@ -297,11 +297,16 @@ impl PgdReader { pub struct PgdWriter { data: ImageData, method: u32, + fake_compress: bool, } impl PgdWriter { - pub fn new(data: ImageData) -> Self { - Self { data, method: 3 } + pub fn new(data: ImageData, fake_compress: bool) -> Self { + Self { + data, + method: 3, + fake_compress, + } } pub fn with_method(mut self, method: u32) -> Self { @@ -315,7 +320,11 @@ impl PgdWriter { _ => panic!("Unsupported GE mode: {}", self.method), }; let unpacked_len = data.len() as u32; - let compressed = ge_fake_compress(&data)?; + let compressed = if self.fake_compress { + ge_fake_compress(&data)? + } else { + ge_compress(&data)? + }; let packed_len = compressed.len() as u32; writer.write_u32(unpacked_len)?; writer.write_u32(packed_len)?; @@ -412,3 +421,199 @@ fn ge_fake_compress(data: &[u8]) -> Result> { Ok(output) } + +// 新增:基于散列的快速 LZSS 压缩,兼容 unpack_ge_pre +fn ge_compress(data: &[u8]) -> Result> { + const MIN_MATCH: usize = 4; + const MAX_LEN: usize = 0x7FF + 4; // 2047 + 4 = 2051 + const MAX_DIST: usize = 0xFFF; // 12-bit distance + const HASH_BITS: usize = 16; + const HASH_SIZE: usize = 1 << HASH_BITS; + + #[inline(always)] + fn hash3(bytes: &[u8]) -> usize { + // 3字节哈希,乘黄金常数,取高 HASH_BITS 位 + let v = ((bytes[0] as u32) << 16) ^ ((bytes[1] as u32) << 8) ^ (bytes[2] as u32); + (v.wrapping_mul(0x9E3779B1) >> (32 - HASH_BITS)) as usize + } + + let n = data.len(); + if n == 0 { + return Ok(Vec::new()); + } + + let mut out = Vec::with_capacity(n / 2 + 16); + + // 控制块状态 + let mut ctrl_pos = out.len(); + out.push(0u8); // 占位 + let mut ctrl: u8 = 0; + let mut ctrl_cnt: u8 = 0; + + // 哈希表:保存最近出现位置 + let mut head = vec![usize::MAX; HASH_SIZE]; + + // 延迟字面量缓冲 + let mut lit_start = 0usize; + let mut lit_len = 0usize; + + // 辅助:开始新的控制块 + #[inline(always)] + fn start_block(buf: &mut Vec, ctrl_pos: &mut usize, ctrl: &mut u8, ctrl_cnt: &mut u8) { + *ctrl = 0; + *ctrl_cnt = 0; + *ctrl_pos = buf.len(); + buf.push(0u8); + } + + // 辅助:写入控制字节 + #[inline(always)] + fn flush_ctrl(buf: &mut Vec, ctrl_pos: usize, ctrl: u8) { + if let Some(slot) = buf.get_mut(ctrl_pos) { + *slot = ctrl; + } + } + + // 辅助:输出字面量(可拆分为多个 <=255 的条目) + let flush_literals = |out: &mut Vec, + ctrl: &mut u8, + ctrl_cnt: &mut u8, + ctrl_pos: &mut usize, + lit_start: &mut usize, + lit_len: &mut usize| { + while *lit_len > 0 { + if *ctrl_cnt == 8 { + flush_ctrl(out, *ctrl_pos, *ctrl); + start_block(out, ctrl_pos, ctrl, ctrl_cnt); + } + let chunk = std::cmp::min(255, *lit_len); + out.push(chunk as u8); + out.extend_from_slice(&data[*lit_start..*lit_start + chunk]); + *lit_start += chunk; + *lit_len -= chunk; + *ctrl_cnt += 1; // 字面量控制位为0,无需设置位 + } + }; + + let mut pos = 0usize; + + while pos < n { + // 尝试匹配 + let mut best_len = 0usize; + let mut best_dist = 0usize; + + if pos + MIN_MATCH <= n { + let h = hash3(&data[pos..pos + 3]); + let cand = head[h]; + if cand != usize::MAX && cand < pos { + let dist = pos - cand; + if dist > 0 && dist <= MAX_DIST { + // 计算匹配长度 + let max_len = std::cmp::min(MAX_LEN, n - pos); + // 快速比较 + let mut l = 0usize; + while l < max_len && data[cand + l] == data[pos + l] { + l += 1; + } + if l >= MIN_MATCH { + best_len = l; + best_dist = dist; + } + } + } + } + + if best_len >= MIN_MATCH { + // 先刷新字面量 + flush_literals( + &mut out, + &mut ctrl, + &mut ctrl_cnt, + &mut ctrl_pos, + &mut lit_start, + &mut lit_len, + ); + + // 控制块满则换块 + if ctrl_cnt == 8 { + flush_ctrl(&mut out, ctrl_pos, ctrl); + start_block(&mut out, &mut ctrl_pos, &mut ctrl, &mut ctrl_cnt); + } + + let l = best_len.min(MAX_LEN); + let dist = best_dist; + + // 写入回溯条目:u16 (LE),必要时再跟长度低8位 + let len_minus4 = l - 4; + let mut word: u16 = ((dist as u16) << 4) as u16; + if len_minus4 <= 7 { + // 短长度:bit3=1,低3位为长度 + word |= (len_minus4 as u16) | 0x8; + out.push((word & 0xFF) as u8); + out.push((word >> 8) as u8); + } else { + // 扩展长度:bit3=0,低3位为长度高3位,随后写低8位 + word |= ((len_minus4 >> 8) as u16) & 0x7; + out.push((word & 0xFF) as u8); + out.push((word >> 8) as u8); + out.push((len_minus4 & 0xFF) as u8); + } + + // 设置控制位为1 + let bit = 1u8.wrapping_shl(ctrl_cnt as u32); + ctrl |= bit; + ctrl_cnt += 1; + + // 更新哈希表(覆盖匹配范围,避免过度开销也可简化为只更新起始位) + let end = pos + l; + let upd_end = end + .saturating_sub(MIN_MATCH - 1) + .min(n.saturating_sub(MIN_MATCH)); + let mut p = pos; + while p <= upd_end { + let h = hash3(&data[p..p + 3]); + head[h] = p; + p += 1; + } + + pos += l; + lit_start = pos; + lit_len = 0; + } else { + // 作为字面量 + if pos + 2 < n { + let h = hash3(&data[pos..pos + 3]); + head[h] = pos; + } + pos += 1; + lit_len += 1; + + // 字面量满255则立刻输出一条 + if lit_len == 255 { + flush_literals( + &mut out, + &mut ctrl, + &mut ctrl_cnt, + &mut ctrl_pos, + &mut lit_start, + &mut lit_len, + ); + } + } + } + + // 结束时刷新剩余字面量与控制字节 + if lit_len > 0 { + flush_literals( + &mut out, + &mut ctrl, + &mut ctrl_cnt, + &mut ctrl_pos, + &mut lit_start, + &mut lit_len, + ); + } + flush_ctrl(&mut out, ctrl_pos, ctrl); + + Ok(out) +} diff --git a/src/scripts/softpal/img/pgd/ge.rs b/src/scripts/softpal/img/pgd/ge.rs index 5f86ddb..563b57b 100644 --- a/src/scripts/softpal/img/pgd/ge.rs +++ b/src/scripts/softpal/img/pgd/ge.rs @@ -59,7 +59,7 @@ impl ScriptBuilder for PgdGeBuilder { &'a self, data: ImageData, mut writer: Box, - _options: &ExtraConfig, + options: &ExtraConfig, ) -> Result<()> { let header = PgdGeHeader { offset_x: 0, @@ -72,7 +72,9 @@ impl ScriptBuilder for PgdGeBuilder { }; writer.write_all(b"GE \0")?; header.pack(&mut writer, false, Encoding::Utf8)?; - PgdWriter::new(data).with_method(3).pack_ge(&mut writer)?; + PgdWriter::new(data, options.pgd_fake_compress) + .with_method(3) + .pack_ge(&mut writer)?; Ok(()) } } @@ -81,10 +83,11 @@ impl ScriptBuilder for PgdGeBuilder { pub struct PgdGe { header: PgdGeHeader, data: ImageData, + fake_compress: bool, } impl PgdGe { - pub fn new(mut input: T, _config: &ExtraConfig) -> Result { + pub fn new(mut input: T, config: &ExtraConfig) -> Result { let mut magic = [0u8; 4]; input.read_exact(&mut magic)?; if &magic != b"GE \0" { @@ -93,7 +96,11 @@ impl PgdGe { let header = PgdGeHeader::unpack(&mut input, false, Encoding::Utf8)?; let reader = PgdReader::with_ge_header(input, &header)?; let data = reader.unpack_ge()?; - Ok(Self { header, data }) + Ok(Self { + header, + data, + fake_compress: config.pgd_fake_compress, + }) } } @@ -137,7 +144,9 @@ impl Script for PgdGe { header.mode = 3; file.write_all(b"GE \0")?; header.pack(&mut file, false, Encoding::Utf8)?; - PgdWriter::new(data).with_method(3).pack_ge(&mut file)?; + PgdWriter::new(data, self.fake_compress) + .with_method(3) + .pack_ge(&mut file)?; Ok(()) } } diff --git a/src/types.rs b/src/types.rs index 25d2e7c..f5ffe12 100644 --- a/src/types.rs +++ b/src/types.rs @@ -449,6 +449,9 @@ pub struct ExtraConfig { #[default(true)] /// Process tlg images. pub psb_process_tlg: bool, + #[cfg(feature = "softpal-img")] + /// Whether to use fake compression for Softpal Pgd images + pub pgd_fake_compress: bool, } #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)]