diff --git a/src/args.rs b/src/args.rs index 90656e7..4578b62 100644 --- a/src/args.rs +++ b/src/args.rs @@ -97,6 +97,11 @@ pub struct Arg { #[arg(long, global = true)] /// Detect all files in BGI archive as SysGrp Images. By default, only files which name is `sysgrp.arc` will enabled this. pub bgi_is_sysgrp_arc: Option, + #[cfg(feature = "bgi-img")] + #[arg(long, global = true)] + /// Whether to create scrambled SysGrp images. When in import mode, the default value depends on the original image. + /// When in creation mode, it is not enabled by default. + pub bgi_img_scramble: Option, #[command(subcommand)] /// Command pub command: Command, diff --git a/src/main.rs b/src/main.rs index e1cad41..44fcc29 100644 --- a/src/main.rs +++ b/src/main.rs @@ -901,6 +901,35 @@ pub fn import_script( arch.write_header()?; return Ok(types::ScriptResult::Ok); } + #[cfg(feature = "image")] + if script.is_image() { + let out_type = arg.image_type.unwrap_or(types::ImageOutputType::Png); + let out_f = if is_dir { + let f = std::path::PathBuf::from(filename); + let mut pb = std::path::PathBuf::from(&imp_cfg.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 { + imp_cfg.output.clone() + }; + let data = utils::img::decode_img(out_type, &out_f)?; + let patched_f = if is_dir { + let f = std::path::PathBuf::from(filename); + let mut pb = std::path::PathBuf::from(&imp_cfg.patched); + if let Some(fname) = f.file_name() { + pb.push(fname); + } + pb.set_extension(builder.extensions().first().unwrap_or(&"")); + pb.to_string_lossy().into_owned() + } else { + imp_cfg.patched.clone() + }; + script.import_image_filename(data, &patched_f)?; + return Ok(types::ScriptResult::Ok); + } let mut of = match &arg.output_type { Some(t) => t.clone(), None => script.default_output_script_type(), @@ -1131,7 +1160,12 @@ pub fn unpack_archive( Ok(types::ScriptResult::Ok) } -pub fn create_file(input: &str, output: Option<&str>, arg: &args::Arg) -> anyhow::Result<()> { +pub fn create_file( + input: &str, + output: Option<&str>, + arg: &args::Arg, + _config: &types::ExtraConfig, +) -> anyhow::Result<()> { let typ = match &arg.script_type { Some(t) => t, None => { @@ -1143,6 +1177,32 @@ pub fn create_file(input: &str, output: Option<&str>, arg: &args::Arg) -> anyhow .find(|b| b.script_type() == typ) .ok_or_else(|| anyhow::anyhow!("Unsupported script type"))?; + #[cfg(feature = "image")] + if builder.is_image() { + if !builder.can_create_image_file() { + return Err(anyhow::anyhow!( + "Script type {:?} does not support image file creation", + typ + )); + } + let data = + utils::img::decode_img(arg.image_type.unwrap_or(types::ImageOutputType::Png), input)?; + let output = match output { + Some(output) => output.to_string(), + None => { + let mut pb = std::path::PathBuf::from(input); + let ext = builder.extensions().first().unwrap_or(&"unk"); + pb.set_extension(ext); + if pb.to_string_lossy() == input { + pb.set_extension(format!("{}.{}", ext, ext)); + } + pb.to_string_lossy().into_owned() + } + }; + builder.create_image_file_filename(data, &output, _config)?; + return Ok(()); + } + if !builder.can_create_file() { return Err(anyhow::anyhow!( "Script type {:?} does not support file creation", @@ -1196,6 +1256,8 @@ fn main() { image_type: arg.image_type.clone(), #[cfg(all(feature = "bgi-arc", feature = "bgi-img"))] bgi_is_sysgrp_arc: arg.bgi_is_sysgrp_arc.clone(), + #[cfg(feature = "bgi-img")] + bgi_img_scramble: arg.bgi_img_scramble.clone(), }; match &arg.command { args::Command::Export { input, output } => { @@ -1329,7 +1391,7 @@ fn main() { } } args::Command::Create { input, output } => { - let re = create_file(input, output.as_ref().map(|s| s.as_str()), &arg); + let re = create_file(input, output.as_ref().map(|s| s.as_str()), &arg, &cfg); if let Err(e) = re { COUNTER.inc_error(); eprintln!("Error creating file: {}", e); diff --git a/src/scripts/base.rs b/src/scripts/base.rs index dd2466f..80a2524 100644 --- a/src/scripts/base.rs +++ b/src/scripts/base.rs @@ -124,6 +124,7 @@ pub trait ScriptBuilder: std::fmt::Debug { &'a self, _data: ImageData, _writer: Box, + _options: &ExtraConfig, ) -> Result<()> { Err(anyhow::anyhow!( "This script type does not support creating an image file." @@ -131,10 +132,15 @@ pub trait ScriptBuilder: std::fmt::Debug { } #[cfg(feature = "image")] - fn create_image_file_filename(&self, data: ImageData, filename: &str) -> Result<()> { + fn create_image_file_filename( + &self, + data: ImageData, + filename: &str, + options: &ExtraConfig, + ) -> Result<()> { let f = std::fs::File::create(filename)?; let f = std::io::BufWriter::new(f); - self.create_image_file(data, Box::new(f)) + self.create_image_file(data, Box::new(f), options) } } diff --git a/src/scripts/bgi/image/img.rs b/src/scripts/bgi/image/img.rs index ed154ae..df116b3 100644 --- a/src/scripts/bgi/image/img.rs +++ b/src/scripts/bgi/image/img.rs @@ -1,6 +1,7 @@ use crate::ext::io::*; use crate::scripts::base::*; use crate::types::*; +use crate::utils::img::*; use anyhow::Result; fn try_parse(buf: &[u8]) -> Result { @@ -72,6 +73,19 @@ impl ScriptBuilder for BgiImageBuilder { } None } + + fn can_create_image_file(&self) -> bool { + true + } + + fn create_image_file<'a>( + &'a self, + data: ImageData, + writer: Box, + options: &ExtraConfig, + ) -> Result<()> { + create_image(data, writer, options.bgi_img_scramble.unwrap_or(false)) + } } #[derive(Debug)] @@ -81,10 +95,73 @@ pub struct BgiImage { height: u32, color_type: ImageColorType, is_scrambled: bool, + opt_is_scrambled: Option, +} + +fn create_image<'a>( + mut data: ImageData, + mut writer: Box, + scrambled: bool, +) -> Result<()> { + writer.write_u16(data.width as u16)?; + writer.write_u16(data.height as u16)?; + if data.depth != 8 { + return Err(anyhow::anyhow!("Unsupported image depth: {}", data.depth)); + } + match data.color_type { + ImageColorType::Bgr => {} + ImageColorType::Bgra => {} + ImageColorType::Grayscale => {} + ImageColorType::Rgb => { + convert_rgb_to_bgr(&mut data)?; + } + ImageColorType::Rgba => { + convert_rgba_to_bgra(&mut data)?; + } + } + let bpp = data.color_type.bpp(8); + writer.write_u16(bpp)?; + let flag = if scrambled { 1 } else { 0 }; + writer.write_u16(flag)?; + writer.write_u64(0)?; // Padding + let stride = data.width as usize * ((data.color_type.bpp(8) as usize + 7) / 8); + let buf_size = stride * data.height as usize; + if scrambled { + let bpp = data.color_type.bpp(1) as usize; + for i in 0..bpp { + let mut dst = i; + let mut incr = 0u8; + let mut h = data.height; + while h > 0 { + for _ in 0..data.width { + writer.write_u8(data.data[dst].wrapping_sub(incr))?; + incr = data.data[dst]; + dst += bpp; + } + h -= 1; + if h == 0 { + break; + } + dst += stride; + let mut pos = dst; + for _ in 0..data.width { + pos -= bpp; + writer.write_u8(data.data[pos].wrapping_sub(incr))?; + incr = data.data[pos]; + } + h -= 1; + } + } + } else { + // PNG sometimes return more padding data than expected + // We will write only the required size + writer.write_all(&data.data[..buf_size])?; + } + Ok(()) } impl BgiImage { - pub fn new(buf: Vec, _config: &ExtraConfig) -> Result { + 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; @@ -108,6 +185,7 @@ impl BgiImage { height, color_type, is_scrambled, + opt_is_scrambled: config.bgi_img_scramble, }) } } @@ -126,14 +204,41 @@ impl Script for BgiImage { } fn export_image(&self) -> Result { - let stride = self.width as usize * ((self.color_type.bbp(8) as usize + 7) / 8); + let stride = self.width as usize * ((self.color_type.bpp(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)?; + if self.is_scrambled { + let mut reader = self.data.to_ref(); + reader.pos = 0x10; + let bpp = self.color_type.bpp(1) as usize; + for i in 0..bpp { + let mut dst = i; + let mut incr = 0u8; + let mut h = self.height; + while h > 0 { + for _ in 0..self.width { + incr = incr.wrapping_add(reader.read_u8()?); + data[dst] = incr; + dst += bpp; + } + h -= 1; + if h == 0 { + break; + } + dst += stride; + let mut pos = dst; + for _ in 0..self.width { + pos -= bpp; + incr = incr.wrapping_add(reader.read_u8()?); + data[pos] = incr; + } + h -= 1; + } + } + } else { + self.data.cpeek_extract_at(0x10, &mut data)?; + } Ok(ImageData { width: self.width, height: self.height, @@ -142,4 +247,12 @@ impl Script for BgiImage { data, }) } + + fn import_image<'a>(&'a self, data: ImageData, file: Box) -> Result<()> { + create_image( + data, + file, + self.opt_is_scrambled.unwrap_or(self.is_scrambled), + ) + } } diff --git a/src/types.rs b/src/types.rs index b0ddb61..c8b2fbd 100644 --- a/src/types.rs +++ b/src/types.rs @@ -201,6 +201,8 @@ pub struct ExtraConfig { pub image_type: Option, #[cfg(all(feature = "bgi-arc", feature = "bgi-img"))] pub bgi_is_sysgrp_arc: Option, + #[cfg(feature = "bgi-img")] + pub bgi_img_scramble: Option, } #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)] @@ -320,7 +322,7 @@ pub enum ImageColorType { #[cfg(feature = "image")] impl ImageColorType { - pub fn bbp(&self, depth: u8) -> u16 { + pub fn bpp(&self, depth: u8) -> u16 { match self { ImageColorType::Grayscale => depth as u16, ImageColorType::Rgb => depth as u16 * 3, diff --git a/src/utils/img.rs b/src/utils/img.rs index a95661e..20795a9 100644 --- a/src/utils/img.rs +++ b/src/utils/img.rs @@ -52,6 +52,42 @@ pub fn convert_bgra_to_rgba(data: &mut ImageData) -> Result<()> { Ok(()) } +pub fn convert_rgb_to_bgr(data: &mut ImageData) -> Result<()> { + if data.color_type != ImageColorType::Rgb { + return Err(anyhow::anyhow!("Image is not RGB")); + } + if data.depth != 8 { + return Err(anyhow::anyhow!( + "RGB to BGR conversion only supports 8-bit depth" + )); + } + for i in (0..data.data.len()).step_by(3) { + let r = data.data[i]; + data.data[i] = data.data[i + 2]; + data.data[i + 2] = r; + } + data.color_type = ImageColorType::Bgr; + Ok(()) +} + +pub fn convert_rgba_to_bgra(data: &mut ImageData) -> Result<()> { + if data.color_type != ImageColorType::Rgba { + return Err(anyhow::anyhow!("Image is not RGBA")); + } + if data.depth != 8 { + return Err(anyhow::anyhow!( + "RGBA to BGRA conversion only supports 8-bit depth" + )); + } + for i in (0..data.data.len()).step_by(4) { + let r = data.data[i]; + data.data[i] = data.data[i + 2]; + data.data[i + 2] = r; + } + data.color_type = ImageColorType::Bgra; + Ok(()) +} + pub fn encode_img(mut data: ImageData, typ: ImageOutputType, filename: &str) -> Result<()> { match typ { ImageOutputType::Png => {