Add support to create BGI sysgrp image

This commit is contained in:
2025-06-14 16:44:42 +08:00
parent 1e70036d47
commit d807b9fe5b
6 changed files with 235 additions and 11 deletions

View File

@@ -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<bool>,
#[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<bool>,
#[command(subcommand)]
/// Command
pub command: Command,

View File

@@ -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);

View File

@@ -124,6 +124,7 @@ pub trait ScriptBuilder: std::fmt::Debug {
&'a self,
_data: ImageData,
_writer: Box<dyn WriteSeek + 'a>,
_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)
}
}

View File

@@ -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<u8> {
@@ -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<dyn WriteSeek + 'a>,
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<bool>,
}
fn create_image<'a>(
mut data: ImageData,
mut writer: Box<dyn WriteSeek + 'a>,
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<u8>, _config: &ExtraConfig) -> Result<Self> {
pub fn new(buf: Vec<u8>, config: &ExtraConfig) -> Result<Self> {
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<ImageData> {
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<dyn WriteSeek + 'a>) -> Result<()> {
create_image(
data,
file,
self.opt_is_scrambled.unwrap_or(self.is_scrambled),
)
}
}

View File

@@ -201,6 +201,8 @@ pub struct ExtraConfig {
pub image_type: Option<ImageOutputType>,
#[cfg(all(feature = "bgi-arc", feature = "bgi-img"))]
pub bgi_is_sysgrp_arc: Option<bool>,
#[cfg(feature = "bgi-img")]
pub bgi_img_scramble: Option<bool>,
}
#[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,

View File

@@ -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 => {