From 242d501af51c56f1cc75fb14930c52fcbee1ac1e Mon Sep 17 00:00:00 2001 From: lifegpc Date: Thu, 12 Jun 2025 21:52:01 +0800 Subject: [PATCH] Add BGI Image decode support Fix DSC decompress --- Cargo.toml | 2 +- src/args.rs | 4 ++ src/ext/io.rs | 4 +- src/ext/vec.rs | 22 +++--- src/main.rs | 32 +++++++++ src/scripts/base.rs | 54 +++++++++++++++ src/scripts/bgi/image/img.rs | 112 +++++++++++++++++++++++++++++++ src/scripts/bgi/image/mod.rs | 1 + src/scripts/bgi/mod.rs | 2 + src/scripts/mod.rs | 2 + src/types.rs | 46 ++++++++++++- src/utils/img.rs | 126 +++++++++++++++++++++++++++++++++++ src/utils/mod.rs | 2 + 13 files changed, 395 insertions(+), 14 deletions(-) create mode 100644 src/scripts/bgi/image/img.rs create mode 100644 src/scripts/bgi/image/mod.rs create mode 100644 src/utils/img.rs diff --git a/Cargo.toml b/Cargo.toml index 410beca..a035967 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ unicode-segmentation = "1.12" default = ["bgi", "bgi-arc", "bgi-img", "circus", "escude", "escude-arc", "yaneurao", "yaneurao-itufuru"] bgi = [] bgi-arc = ["bgi", "utils-bit-stream"] -bgi-img = ["image"] +bgi-img = ["bgi", "image"] circus = [] escude = ["int-enum"] escude-arc = ["escude", "rand", "utils-bit-stream"] diff --git a/src/args.rs b/src/args.rs index 3fb7c73..c580ae0 100644 --- a/src/args.rs +++ b/src/args.rs @@ -16,6 +16,10 @@ pub struct Arg { #[arg(short = 'T', long, value_enum, global = true)] /// Output script type pub output_type: Option, + #[cfg(feature = "image")] + #[arg(short = 'i', long, value_enum, global = true)] + /// Output image type + pub image_type: Option, #[arg(short = 'e', long, value_enum, global = true, group = "encodingg")] /// Script encoding pub encoding: Option, diff --git a/src/ext/io.rs b/src/ext/io.rs index db08763..b2a08ca 100644 --- a/src/ext/io.rs +++ b/src/ext/io.rs @@ -1040,14 +1040,14 @@ impl<'a> CPeek for MemReaderRef<'a> { fn cpeek(&self, buf: &mut [u8]) -> Result { let len = self.data.len(); let bytes_to_read = std::cmp::min(buf.len(), len - self.pos); - buf.copy_from_slice(&self.data[self.pos..self.pos + bytes_to_read]); + buf[..bytes_to_read].copy_from_slice(&self.data[self.pos..self.pos + bytes_to_read]); Ok(bytes_to_read) } fn cpeek_at(&self, offset: usize, buf: &mut [u8]) -> Result { let len = self.data.len(); let bytes_to_read = std::cmp::min(buf.len(), len - offset); - buf.copy_from_slice(&self.data[offset..offset + bytes_to_read]); + buf[..bytes_to_read].copy_from_slice(&self.data[offset..offset + bytes_to_read]); Ok(bytes_to_read) } } diff --git a/src/ext/vec.rs b/src/ext/vec.rs index db33a9d..33f20d9 100644 --- a/src/ext/vec.rs +++ b/src/ext/vec.rs @@ -4,17 +4,21 @@ pub trait VecExt { } impl VecExt for Vec { - fn copy_overlapped(&mut self, src: usize, dst: usize, len: usize) { - let src = src.min(self.len()); - let dst = dst.min(self.len()); - if src < dst { - let max_count = len.min(dst - src); - for i in 0..max_count { - self[dst + i] = self[src + i]; + fn copy_overlapped(&mut self, src: usize, dst: usize, mut len: usize) { + let mut src = src.min(self.len()); + let mut dst = dst.min(self.len()); + if dst > src { + while len > 0 { + let preceding = (dst - src).min(len); + for i in 0..preceding { + self[dst + i] = self[src + i]; + } + len -= preceding; + src += preceding; + dst += preceding; } } else { - let max_count = len.min(src - dst); - for i in (0..max_count).rev() { + for i in 0..len { self[dst + i] = self[src + i]; } } diff --git a/src/main.rs b/src/main.rs index 5bdcb53..868cff6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -516,6 +516,36 @@ pub fn export_script( } return Ok(types::ScriptResult::Ok); } + if script.is_image() { + let img_data = script.export_image()?; + let out_type = arg.image_type.unwrap_or(types::ImageOutputType::Png); + let f = if filename == "-" { + String::from("-") + } else { + match output.as_ref() { + Some(output) => { + if is_dir { + let f = std::path::PathBuf::from(filename); + let mut pb = std::path::PathBuf::from(output); + if let Some(fname) = f.file_name() { + pb.push(fname); + } + pb.set_extension(out_type.as_ref()); + pb.to_string_lossy().into_owned() + } else { + output.clone() + } + } + None => { + let mut pb = std::path::PathBuf::from(filename); + pb.set_extension(out_type.as_ref()); + pb.to_string_lossy().into_owned() + } + } + }; + utils::img::encode_img(img_data, out_type, &f)?; + return Ok(types::ScriptResult::Ok); + } let mut of = match &arg.output_type { Some(t) => t.clone(), None => script.default_output_script_type(), @@ -1122,6 +1152,8 @@ fn main() { bgi_import_duplicate: arg.bgi_import_duplicate, #[cfg(feature = "bgi")] bgi_disable_append: arg.bgi_disable_append, + #[cfg(feature = "image")] + image_type: arg.image_type.clone(), }; match &arg.command { args::Command::Export { input, output } => { diff --git a/src/scripts/base.rs b/src/scripts/base.rs index d1b5033..dd2466f 100644 --- a/src/scripts/base.rs +++ b/src/scripts/base.rs @@ -108,6 +108,34 @@ pub trait ScriptBuilder: std::fmt::Debug { let f = std::io::BufWriter::new(f); self.create_file(filename, Box::new(f), encoding, file_encoding) } + + #[cfg(feature = "image")] + fn is_image(&self) -> bool { + false + } + + #[cfg(feature = "image")] + fn can_create_image_file(&self) -> bool { + false + } + + #[cfg(feature = "image")] + fn create_image_file<'a>( + &'a self, + _data: ImageData, + _writer: Box, + ) -> Result<()> { + Err(anyhow::anyhow!( + "This script type does not support creating an image file." + )) + } + + #[cfg(feature = "image")] + fn create_image_file_filename(&self, data: ImageData, filename: &str) -> Result<()> { + let f = std::fs::File::create(filename)?; + let f = std::io::BufWriter::new(f); + self.create_image_file(data, Box::new(f)) + } } pub trait ArchiveContent: Read { @@ -222,6 +250,32 @@ pub trait Script: std::fmt::Debug { ) -> Result>> + 'a>> { Ok(Box::new(std::iter::empty())) } + + #[cfg(feature = "image")] + fn is_image(&self) -> bool { + false + } + + #[cfg(feature = "image")] + fn export_image(&self) -> Result { + Err(anyhow::anyhow!( + "This script type does not support to export image." + )) + } + + #[cfg(feature = "image")] + fn import_image<'a>(&'a self, _data: ImageData, _file: Box) -> Result<()> { + Err(anyhow::anyhow!( + "This script type does not support to import image." + )) + } + + #[cfg(feature = "image")] + fn import_image_filename(&self, data: ImageData, filename: &str) -> Result<()> { + let f = std::fs::File::create(filename)?; + let f = std::io::BufWriter::new(f); + self.import_image(data, Box::new(f)) + } } pub trait Archive { diff --git a/src/scripts/bgi/image/img.rs b/src/scripts/bgi/image/img.rs new file mode 100644 index 0000000..fafaa0c --- /dev/null +++ b/src/scripts/bgi/image/img.rs @@ -0,0 +1,112 @@ +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use anyhow::Result; + +#[derive(Debug)] +pub struct BgiImageBuilder {} + +impl BgiImageBuilder { + pub const fn new() -> Self { + BgiImageBuilder {} + } +} + +impl ScriptBuilder for BgiImageBuilder { + fn default_encoding(&self) -> Encoding { + Encoding::Cp932 + } + + fn build_script( + &self, + data: Vec, + _filename: &str, + _encoding: Encoding, + _archive_encoding: Encoding, + config: &ExtraConfig, + ) -> Result> { + Ok(Box::new(BgiImage::new(data, config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &[] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::BGIImg + } + + fn is_image(&self) -> bool { + true + } +} + +#[derive(Debug)] +pub struct BgiImage { + data: MemReader, + width: u32, + height: u32, + color_type: ImageColorType, + is_scrambled: bool, +} + +impl BgiImage { + pub fn new(buf: Vec, _config: &ExtraConfig) -> Result { + let mut reader = MemReader::new(buf); + let width = reader.read_u16()? as u32; + let height = reader.read_u16()? as u32; + let bpp = reader.read_u16()?; + let color_type = match bpp { + 8 => ImageColorType::Grayscale, + 24 => ImageColorType::Bgr, + 32 => ImageColorType::Bgra, + _ => return Err(anyhow::anyhow!("Unsupported BPP: {}", bpp)), + }; + let flag = reader.read_u16()?; + let padding = reader.read_u64()?; + if padding != 0 { + return Err(anyhow::anyhow!("Invalid padding: {}", padding)); + } + let is_scrambled = flag != 0; + + Ok(BgiImage { + data: reader, + width, + height, + color_type, + is_scrambled, + }) + } +} + +impl Script for BgiImage { + 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 { + let stride = self.width as usize * ((self.color_type.bbp(8) as usize + 7) / 8); + let buf_size = stride * self.height as usize; + if self.is_scrambled { + return Err(anyhow::anyhow!("Scrambled images are not supported")); + } + let mut data = Vec::with_capacity(buf_size); + data.resize(buf_size, 0); + self.data.cpeek_extract_at(0x10, &mut data)?; + Ok(ImageData { + width: self.width, + height: self.height, + color_type: self.color_type, + depth: 8, + data, + }) + } +} diff --git a/src/scripts/bgi/image/mod.rs b/src/scripts/bgi/image/mod.rs new file mode 100644 index 0000000..ea10239 --- /dev/null +++ b/src/scripts/bgi/image/mod.rs @@ -0,0 +1 @@ +pub mod img; diff --git a/src/scripts/bgi/mod.rs b/src/scripts/bgi/mod.rs index 09001c5..cf888dd 100644 --- a/src/scripts/bgi/mod.rs +++ b/src/scripts/bgi/mod.rs @@ -2,5 +2,7 @@ pub mod archive; pub mod bp; pub mod bsi; +#[cfg(feature = "bgi-img")] +pub mod image; mod parser; pub mod script; diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index ec15405..a29128d 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -24,6 +24,8 @@ lazy_static::lazy_static! { Box::new(bgi::archive::v1::BgiArchiveBuilder::new()), #[cfg(feature = "bgi-arc")] Box::new(bgi::archive::v2::BgiArchiveBuilder::new()), + #[cfg(feature = "bgi-img")] + Box::new(bgi::image::img::BgiImageBuilder::new()), #[cfg(feature = "escude-arc")] Box::new(escude::archive::EscudeBinArchiveBuilder::new()), #[cfg(feature = "escude")] diff --git a/src/types.rs b/src/types.rs index 99c3efb..5c7d655 100644 --- a/src/types.rs +++ b/src/types.rs @@ -197,6 +197,8 @@ pub struct ExtraConfig { pub bgi_import_duplicate: bool, #[cfg(feature = "bgi")] pub bgi_disable_append: bool, + #[cfg(feature = "image")] + pub image_type: Option, } #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)] @@ -225,6 +227,10 @@ pub enum ScriptType { #[value(alias = "ethornell-arc-v2", alias = "bgi-arc", alias = "ethornell-arc")] /// Buriko General Interpreter/Ethornell archive v2 BGIArcV2, + #[cfg(feature = "bgi-img")] + #[value(alias("ethornell-img"))] + /// Buriko General Interpreter/Ethornell image (Image files in sysgrp.arc) + BGIImg, #[cfg(feature = "escude-arc")] /// Escude bin archive EscudeArc, @@ -301,13 +307,49 @@ pub struct ReplacementTable { } #[cfg(feature = "image")] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum ImageColorType { Grayscale, - Rgb24, - Rgba32, + Rgb, + Rgba, + Bgr, + Bgra, } #[cfg(feature = "image")] +impl ImageColorType { + pub fn bbp(&self, depth: u8) -> u16 { + match self { + ImageColorType::Grayscale => depth as u16, + ImageColorType::Rgb => depth as u16 * 3, + ImageColorType::Rgba => depth as u16 * 4, + ImageColorType::Bgr => depth as u16 * 3, + ImageColorType::Bgra => depth as u16 * 4, + } + } +} + +#[cfg(feature = "image")] +#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)] pub enum ImageOutputType { Png, } + +#[cfg(feature = "image")] +impl AsRef for ImageOutputType { + fn as_ref(&self) -> &str { + match self { + ImageOutputType::Png => "png", + } + } +} + +#[cfg(feature = "image")] +#[derive(Clone, Debug)] +pub struct ImageData { + pub width: u32, + pub height: u32, + pub color_type: ImageColorType, + pub depth: u8, + pub data: Vec, +} diff --git a/src/utils/img.rs b/src/utils/img.rs new file mode 100644 index 0000000..a95661e --- /dev/null +++ b/src/utils/img.rs @@ -0,0 +1,126 @@ +use crate::types::*; +use anyhow::Result; + +pub fn reverse_alpha_values(data: &mut ImageData) -> Result<()> { + if data.color_type != ImageColorType::Rgba && data.color_type != ImageColorType::Bgra { + return Err(anyhow::anyhow!("Image is not RGBA or BGRA")); + } + if data.depth != 8 { + return Err(anyhow::anyhow!( + "Alpha value reversal only supports 8-bit depth" + )); + } + for i in (0..data.data.len()).step_by(4) { + data.data[i + 3] = 255 - data.data[i + 3]; + } + Ok(()) +} + +pub fn convert_bgr_to_rgb(data: &mut ImageData) -> Result<()> { + if data.color_type != ImageColorType::Bgr { + return Err(anyhow::anyhow!("Image is not BGR")); + } + if data.depth != 8 { + return Err(anyhow::anyhow!( + "BGR to RGB conversion only supports 8-bit depth" + )); + } + for i in (0..data.data.len()).step_by(3) { + let b = data.data[i]; + data.data[i] = data.data[i + 2]; + data.data[i + 2] = b; + } + data.color_type = ImageColorType::Rgb; + Ok(()) +} + +pub fn convert_bgra_to_rgba(data: &mut ImageData) -> Result<()> { + if data.color_type != ImageColorType::Bgra { + return Err(anyhow::anyhow!("Image is not BGRA")); + } + if data.depth != 8 { + return Err(anyhow::anyhow!( + "BGRA to RGBA conversion only supports 8-bit depth" + )); + } + for i in (0..data.data.len()).step_by(4) { + let b = data.data[i]; + data.data[i] = data.data[i + 2]; + data.data[i + 2] = b; + } + data.color_type = ImageColorType::Rgba; + Ok(()) +} + +pub fn encode_img(mut data: ImageData, typ: ImageOutputType, filename: &str) -> Result<()> { + match typ { + ImageOutputType::Png => { + let mut file = crate::utils::files::write_file(filename)?; + let color_type = match data.color_type { + ImageColorType::Grayscale => png::ColorType::Grayscale, + ImageColorType::Rgb => png::ColorType::Rgb, + ImageColorType::Rgba => png::ColorType::Rgba, + ImageColorType::Bgr => { + convert_bgr_to_rgb(&mut data)?; + png::ColorType::Rgb + } + ImageColorType::Bgra => { + convert_bgra_to_rgba(&mut data)?; + png::ColorType::Rgba + } + }; + let bit_depth = match &data.depth { + 1 => png::BitDepth::One, + 2 => png::BitDepth::Two, + 4 => png::BitDepth::Four, + 8 => png::BitDepth::Eight, + 16 => png::BitDepth::Sixteen, + _ => return Err(anyhow::anyhow!("Unsupported bit depth: {}", data.depth)), + }; + let mut encoder = png::Encoder::new(&mut file, data.width, data.height); + encoder.set_color(color_type); + encoder.set_depth(bit_depth); + let mut writer = encoder.write_header()?; + writer.write_image_data(&data.data)?; + writer.finish()?; + Ok(()) + } + } +} + +pub fn decode_img(typ: ImageOutputType, filename: &str) -> Result { + match typ { + ImageOutputType::Png => { + let file = crate::utils::files::read_file(filename)?; + let decoder = png::Decoder::new(&file[..]); + let mut reader = decoder.read_info()?; + let bit_depth = match reader.info().bit_depth { + png::BitDepth::One => 1, + png::BitDepth::Two => 2, + png::BitDepth::Four => 4, + png::BitDepth::Eight => 8, + png::BitDepth::Sixteen => 16, + }; + let color_type = match reader.info().color_type { + png::ColorType::Grayscale => ImageColorType::Grayscale, + png::ColorType::Rgb => ImageColorType::Rgb, + png::ColorType::Rgba => ImageColorType::Rgba, + _ => { + return Err(anyhow::anyhow!( + "Unsupported color type: {:?}", + reader.info().color_type + )); + } + }; + let mut data = vec![0; reader.info().raw_bytes()]; + reader.next_frame(&mut data)?; + Ok(ImageData { + width: reader.info().width, + height: reader.info().height, + depth: bit_depth, + color_type, + data, + }) + } + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 8bc3833..d28da96 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -5,5 +5,7 @@ pub mod encoding; #[cfg(windows)] mod encoding_win; pub mod files; +#[cfg(feature = "image")] +pub mod img; pub mod name_replacement; pub mod struct_pack;