From 3508960835272ac5d90605efa59ee079e885f47d Mon Sep 17 00:00:00 2001 From: lifegpc Date: Tue, 16 Sep 2025 19:00:10 +0800 Subject: [PATCH] Add softpal pgd ge image support --- Cargo.toml | 3 +- README.md | 4 + src/scripts/mod.rs | 2 + src/scripts/softpal/img/mod.rs | 1 + src/scripts/softpal/img/pgd/base.rs | 363 ++++++++++++++++++++++++++++ src/scripts/softpal/img/pgd/ge.rs | 143 +++++++++++ src/scripts/softpal/img/pgd/mod.rs | 2 + src/scripts/softpal/mod.rs | 2 + src/types.rs | 4 + 9 files changed, 523 insertions(+), 1 deletion(-) create mode 100644 src/scripts/softpal/img/mod.rs create mode 100644 src/scripts/softpal/img/pgd/base.rs create mode 100644 src/scripts/softpal/img/pgd/ge.rs create mode 100644 src/scripts/softpal/img/pgd/mod.rs diff --git a/Cargo.toml b/Cargo.toml index dfc5f70..6957c94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ zstd = { version = "0.13", optional = true } default = ["all-fmt", "image-jpg", "image-jxl", "image-webp", "audio-flac", "jieba"] all-fmt = ["all-script", "all-img", "all-arc", "all-audio"] all-script = ["artemis", "artemis-panmimisoft", "bgi", "cat-system", "circus", "entis-gls", "escude", "ex-hibit", "favorite", "hexen-haus", "kirikiri", "silky", "softpal", "will-plus", "yaneurao", "yaneurao-itufuru"] -all-img = ["bgi-img", "cat-system-img", "circus-img", "emote-img", "kirikiri-img"] +all-img = ["bgi-img", "cat-system-img", "circus-img", "emote-img", "kirikiri-img", "softpal-img"] all-arc = ["artemis-arc", "bgi-arc", "cat-system-arc", "circus-arc", "escude-arc"] all-audio = ["bgi-audio", "circus-audio"] artemis = ["stylua", "utils-escape"] @@ -80,6 +80,7 @@ kirikiri = ["emote-psb", "fancy-regex", "flate2", "json", "utils-escape"] kirikiri-img = ["kirikiri", "image", "libtlg-rs"] silky = [] softpal = ["int-enum"] +softpal-img = ["softpal", "image"] will-plus = ["utils-str"] yaneurao = [] yaneurao-itufuru = ["yaneurao"] diff --git a/README.md b/README.md index 05e35f2..49213be 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,10 @@ msg-tool create -t | Script Type | Feature Name | Name | Export | Import | Custom Export | Custom Import | Create | Remarks | |---|---|---|---|---|---|---|---|---| | `softpal` | `softpal` | Softpal Script File (.src) | ✔️ | ✔️ | ✔️ | ❌ | ❌ | | + +| Image Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Create | Remarks | +|---|---|---|---|---|---|---|---|---| +| `softpal-pgd-ge`/`pgd-ge`/`pgd` | `softpal-img` | Softpal PGD Ge Image File (.pgd) | ✔️ | ✔️ | ❌ | ❌ | ✔️ | | ### WillPlus / AdvHD | Script Type | Feature Name | Name | Export | Import | Custom Export | Custom Import | Create | Remarks | |---|---|---|---|---|---|---|---|---| diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 0a68d35..44892ea 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -130,6 +130,8 @@ lazy_static::lazy_static! { Box::new(silky::map::MapBuilder::new()), #[cfg(feature = "emote-img")] Box::new(emote::psb::PsbBuilder::new()), + #[cfg(feature = "softpal-img")] + Box::new(softpal::img::pgd::ge::PgdGeBuilder::new()), ]; /// A list of all script extensions. pub static ref ALL_EXTS: Vec = diff --git a/src/scripts/softpal/img/mod.rs b/src/scripts/softpal/img/mod.rs new file mode 100644 index 0000000..068d6e4 --- /dev/null +++ b/src/scripts/softpal/img/mod.rs @@ -0,0 +1 @@ +pub mod pgd; diff --git a/src/scripts/softpal/img/pgd/base.rs b/src/scripts/softpal/img/pgd/base.rs new file mode 100644 index 0000000..a38652c --- /dev/null +++ b/src/scripts/softpal/img/pgd/base.rs @@ -0,0 +1,363 @@ +use crate::ext::io::*; +use crate::ext::vec::*; +use crate::types::*; +use crate::utils::img::*; +use crate::utils::struct_pack::*; +use anyhow::Result; +use msg_tool_macro::*; +use std::io::{Read, Seek, SeekFrom, Write}; + +#[derive(Clone, Debug, StructPack, StructUnpack)] +pub struct PgdGeHeader { + pub offset_x: u32, + pub offset_y: u32, + pub width: u32, + pub height: u32, + // untested + pub canvas_width: u32, + // untested + pub canvas_height: u32, + pub mode: u32, +} + +pub struct PgdReader { + pub input: T, + output: Vec, + width: u32, + height: u32, + _bpp: u8, + method: u32, + format: Option, +} + +impl PgdReader { + pub fn new(mut input: T, pos: u64) -> Result { + input.seek(SeekFrom::Start(pos))?; + let unpacked_size = input.read_u32()?; + input.read_u32()?; // packed size + let output = vec![0u8; unpacked_size as usize]; + Ok(PgdReader { + input, + output, + width: 0, + height: 0, + _bpp: 0, + method: 0, + format: None, + }) + } + + pub fn with_ge_header(input: T, header: &PgdGeHeader) -> Result { + let mut s = Self::new(input, 0x20)?; + s.width = header.width; + s.height = header.height; + s.method = header.mode; + Ok(s) + } + + pub fn unpack_ge(mut self) -> Result { + self.unpack_ge_pre()?; + let data = match self.method { + 1 => self.post_process1()?, + 2 => self.post_process2()?, + 3 => self.post_process3()?, + _ => return Err(anyhow::anyhow!("Unsupported GE mode: {}", self.method)), + }; + let color_type = self + .format + .ok_or_else(|| anyhow::anyhow!("Unknown image format"))?; + Ok(ImageData { + width: self.width, + height: self.height, + color_type, + depth: 8, + data, + }) + } + + fn unpack_ge_pre(&mut self) -> Result<()> { + let mut dst = 0; + let mut ctl = 2; + let len = self.output.len(); + while dst < len { + ctl >>= 1; + if ctl == 1 { + ctl = self.input.read_u8()? as i32 | 0x100; + } + let mut count; + if ctl & 1 != 0 { + let mut offset = self.input.read_u16()? as usize; + count = offset & 7; + if offset & 8 == 0 { + count = count << 8 | (self.input.read_u8()? as usize); + } + count += 4; + offset >>= 4; + self.output.copy_overlapped(dst - offset, dst, count); + } else { + count = self.input.read_u8()? as usize; + self.input.read_exact(&mut self.output[dst..dst + count])?; + } + dst += count; + } + Ok(()) + } + + fn post_process1(&mut self) -> Result> { + self.format = Some(ImageColorType::Bgra); + let input = &self.output; + let mut output = Vec::with_capacity(input.len()); + let plane_size = input.len() / 4; + let a_src = 0; + let r_src = plane_size; + let g_src = plane_size * 2; + let b_src = plane_size * 3; + for i in 0..plane_size { + output.push(input[b_src + i]); + output.push(input[g_src + i]); + output.push(input[r_src + i]); + output.push(input[a_src + i]); + } + Ok(output) + } + + #[inline(always)] + fn clamp(v: i32) -> u8 { + if v > 255 { + 255 + } else if v < 0 { + 0 + } else { + v as u8 + } + } + + fn post_process2(&mut self) -> Result> { + self.format = Some(ImageColorType::Bgr); + let input = &self.output; + let stride = self.width as usize * 3; + let segment_size = self.width as usize * self.height as usize / 4; + let mut src0 = 0; + let mut src1 = segment_size; + let mut src2 = segment_size * 2; + let mut output = vec![0u8; stride * self.height as usize]; + let mut dst = 0; + let points = [0, 1, self.width, self.width + 1]; + for _y in (1..=(self.height as usize / 2)).rev() { + for _x in (1..=(self.width as usize / 2)).rev() { + let i0 = input[src0] as i8; + let i1 = input[src1] as i8; + let b = 226 * i0 as i32; + let g = -43 * i0 as i32 - 89 * i1 as i32; + let r = 179 * i1 as i32; + src0 += 1; + src1 += 1; + for i in 0..4 { + let mut offset = points[i] as usize; + let base_value = (input[src2 + offset] as i32) << 7; + offset = dst + 3 * offset; + output[offset] = Self::clamp(base_value + b); + output[offset + 1] = Self::clamp(base_value + g); + output[offset + 2] = Self::clamp(base_value + r); + } + src2 += 2; + dst += 6; + } + src2 += self.width as usize; + dst += stride; + } + Ok(output) + } + + fn post_process3(&mut self) -> Result> { + let input = &self.output; + let reader = MemReaderRef::new(input); + let bbp = reader.cpeek_u16_at(0x2)?; + self.format = Some(if bbp == 24 { + ImageColorType::Bgr + } else if bbp == 32 { + ImageColorType::Bgra + } else { + return Err(anyhow::anyhow!("Unsupported bpp: {}", bbp)); + }); + self.width = reader.cpeek_u16_at(0x4)? as u32; + self.height = reader.cpeek_u16_at(0x6)? as u32; + self.post_process_pal(input, 8, bbp as usize / 8) + } + + fn post_process_pal(&self, input: &[u8], mut src: usize, pixel_size: usize) -> Result> { + let stride = self.width as usize * pixel_size; + let mut output = vec![0u8; stride * self.height as usize]; + let mut ctl = src; + src += self.height as usize; + let mut dst = 0; + for _row in 0..self.height as usize { + let c = input[ctl]; + ctl += 1; + if c & 1 != 0 { + let mut prev = dst; + for _ in 0..pixel_size { + output[dst] = input[src]; + dst += 1; + src += 1; + } + let mut count = stride - pixel_size; + while count > 0 { + count -= 1; + output[dst] = output[prev].wrapping_sub(input[src]); + dst += 1; + prev += 1; + src += 1; + } + } else if c & 2 != 0 { + let mut prev = dst - stride; + let mut count = stride; + while count > 0 { + count -= 1; + output[dst] = output[prev].wrapping_sub(input[src]); + dst += 1; + prev += 1; + src += 1; + } + } else { + for _ in 0..pixel_size { + output[dst] = input[src]; + dst += 1; + src += 1; + } + let mut prev = dst - stride; + let mut count = stride - pixel_size; + while count > 0 { + count -= 1; + output[dst] = (((output[prev] as u16) + .wrapping_add(output[dst - pixel_size] as u16) + / 2) as u8) + .wrapping_sub(input[src]); + dst += 1; + prev += 1; + src += 1; + } + } + } + Ok(output) + } +} + +pub struct PgdWriter { + data: ImageData, + method: u32, +} + +impl PgdWriter { + pub fn new(data: ImageData) -> Self { + Self { data, method: 3 } + } + + pub fn with_method(mut self, method: u32) -> Self { + self.method = method; + self + } + + pub fn pack_ge(mut self, mut writer: W) -> Result<()> { + let data = match self.method { + 3 => self.process3()?, + _ => panic!("Unsupported GE mode: {}", self.method), + }; + let unpacked_len = data.len() as u32; + let compressed = ge_fake_compress(&data)?; + let packed_len = compressed.len() as u32; + writer.write_u32(unpacked_len)?; + writer.write_u32(packed_len)?; + writer.write_all(&compressed)?; + Ok(()) + } + + fn process3(&mut self) -> Result> { + let bpp = self.data.color_type.bpp(8) as usize; + let width = self.data.width as u16; + let height = self.data.height as u16; + let mut data = MemWriter::new(); + data.write_u16(0)?; // unk + data.write_u16(bpp as u16)?; + data.write_u16(width)?; + data.write_u16(height)?; + data.write_all(&self.process_pal()?)?; + Ok(data.into_inner()) + } + + fn process_pal(&mut self) -> Result> { + let bpp = match self.data.color_type { + ImageColorType::Bgr => 3, + ImageColorType::Bgra => 4, + ImageColorType::Rgb => { + convert_rgb_to_bgr(&mut self.data)?; + 3 + } + ImageColorType::Rgba => { + convert_rgba_to_bgra(&mut self.data)?; + 4 + } + _ => { + return Err(anyhow::anyhow!( + "Unsupported color type for palettized PGD: {:?}", + self.data.color_type + )); + } + }; + // Fixed mode + let ctl = vec![1u8; self.data.height as usize]; + let stride = self.data.width as usize * bpp; + let mut output = vec![0u8; stride * self.data.height as usize]; + let mut dst = 0; + for _ in 0..self.data.height as usize { + let mut prev = dst; + for _ in 0..bpp { + output[dst] = self.data.data[dst]; + dst += 1; + } + let mut count = stride - bpp; + while count > 0 { + count -= 1; + output[dst] = self.data.data[prev].wrapping_sub(self.data.data[dst]); + dst += 1; + prev += 1; + } + } + let mut result = Vec::with_capacity(ctl.len() + output.len()); + result.extend_from_slice(&ctl); + result.extend_from_slice(&output); + Ok(result) + } +} + +fn ge_fake_compress(data: &[u8]) -> Result> { + let mut output = Vec::new(); + let mut pos = 0; + let data_len = data.len(); + + while pos < data_len { + // 每8个数据块需要一个控制字节 + // 控制字节为0表示接下来8个操作都是直接数据复制 + output.push(0u8); + + // 处理最多8个数据块 + for _ in 0..8 { + if pos >= data_len { + break; + } + + // 计算当前块的大小(最大255字节) + let chunk_size = std::cmp::min(255, data_len - pos); + + // 写入块大小 + output.push(chunk_size as u8); + + // 写入数据 + output.extend_from_slice(&data[pos..pos + chunk_size]); + + pos += chunk_size; + } + } + + Ok(output) +} diff --git a/src/scripts/softpal/img/pgd/ge.rs b/src/scripts/softpal/img/pgd/ge.rs new file mode 100644 index 0000000..5f86ddb --- /dev/null +++ b/src/scripts/softpal/img/pgd/ge.rs @@ -0,0 +1,143 @@ +use super::base::*; +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::struct_pack::*; +use anyhow::Result; +use std::io::{Read, Seek}; + +#[derive(Debug)] +pub struct PgdGeBuilder {} + +impl PgdGeBuilder { + pub fn new() -> Self { + Self {} + } +} + +impl ScriptBuilder for PgdGeBuilder { + fn default_encoding(&self) -> Encoding { + Encoding::Cp932 + } + + fn build_script( + &self, + buf: Vec, + _filename: &str, + _encoding: Encoding, + _archive_encoding: Encoding, + config: &ExtraConfig, + _archive: Option<&Box>, + ) -> Result> { + Ok(Box::new(PgdGe::new(MemReader::new(buf), config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["pgd"] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::SoftpalPgdGe + } + + fn is_image(&self) -> bool { + true + } + + fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option { + if buf_len >= 4 && buf.starts_with(b"GE \0") { + return Some(20); + } + None + } + + fn can_create_image_file(&self) -> bool { + true + } + + fn create_image_file<'a>( + &'a self, + data: ImageData, + mut writer: Box, + _options: &ExtraConfig, + ) -> Result<()> { + let header = PgdGeHeader { + offset_x: 0, + offset_y: 0, + width: data.width, + height: data.height, + canvas_width: data.width, + canvas_height: data.height, + mode: 3, + }; + writer.write_all(b"GE \0")?; + header.pack(&mut writer, false, Encoding::Utf8)?; + PgdWriter::new(data).with_method(3).pack_ge(&mut writer)?; + Ok(()) + } +} + +#[derive(Debug)] +pub struct PgdGe { + header: PgdGeHeader, + data: ImageData, +} + +impl PgdGe { + pub fn new(mut input: T, _config: &ExtraConfig) -> Result { + let mut magic = [0u8; 4]; + input.read_exact(&mut magic)?; + if &magic != b"GE \0" { + return Err(anyhow::anyhow!("Not a valid PGD GE image")); + } + 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 }) + } +} + +impl Script for PgdGe { + fn default_output_script_type(&self) -> OutputScriptType { + OutputScriptType::Json + } + + fn default_format_type(&self) -> FormatOptions { + FormatOptions::None + } + + fn is_image(&self) -> bool { + true + } + + fn export_image(&self) -> Result { + Ok(self.data.clone()) + } + + fn import_image<'a>( + &'a self, + data: ImageData, + mut file: Box, + ) -> Result<()> { + let mut header = self.header.clone(); + if data.height != self.data.height { + return Err(anyhow::anyhow!( + "Image height does not match: expected {}, got {}", + self.data.height, + data.height + )); + } + if data.width != self.data.width { + return Err(anyhow::anyhow!( + "Image width does not match: expected {}, got {}", + self.data.width, + data.width + )); + } + 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)?; + Ok(()) + } +} diff --git a/src/scripts/softpal/img/pgd/mod.rs b/src/scripts/softpal/img/pgd/mod.rs new file mode 100644 index 0000000..eac82fc --- /dev/null +++ b/src/scripts/softpal/img/pgd/mod.rs @@ -0,0 +1,2 @@ +mod base; +pub mod ge; diff --git a/src/scripts/softpal/mod.rs b/src/scripts/softpal/mod.rs index 050f080..286e809 100644 --- a/src/scripts/softpal/mod.rs +++ b/src/scripts/softpal/mod.rs @@ -1,2 +1,4 @@ //! Softpal scripts +#[cfg(feature = "softpal-img")] +pub mod img; pub mod scr; diff --git a/src/types.rs b/src/types.rs index 393458d..ef946c0 100644 --- a/src/types.rs +++ b/src/types.rs @@ -606,6 +606,10 @@ pub enum ScriptType { #[cfg(feature = "softpal")] /// Softpal src script Softpal, + #[cfg(feature = "softpal-img")] + #[value(alias = "pgd-ge", alias = "pgd")] + /// Softpal Pgd Ge image + SoftpalPgdGe, #[cfg(feature = "will-plus")] #[value(alias("adv-hd-ws2"))] /// WillPlus ws2 script