Add BGI Image decode support

Fix DSC decompress
This commit is contained in:
2025-06-12 21:52:01 +08:00
parent 8bed66d4f1
commit 242d501af5
13 changed files with 395 additions and 14 deletions

View File

@@ -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"]

View File

@@ -16,6 +16,10 @@ pub struct Arg {
#[arg(short = 'T', long, value_enum, global = true)]
/// Output script type
pub output_type: Option<OutputScriptType>,
#[cfg(feature = "image")]
#[arg(short = 'i', long, value_enum, global = true)]
/// Output image type
pub image_type: Option<ImageOutputType>,
#[arg(short = 'e', long, value_enum, global = true, group = "encodingg")]
/// Script encoding
pub encoding: Option<TextEncoding>,

View File

@@ -1040,14 +1040,14 @@ impl<'a> CPeek for MemReaderRef<'a> {
fn cpeek(&self, buf: &mut [u8]) -> Result<usize> {
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<usize> {
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)
}
}

View File

@@ -4,17 +4,21 @@ pub trait VecExt<T> {
}
impl<T: Copy> VecExt<T> for Vec<T> {
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];
}
}

View File

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

View File

@@ -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<dyn WriteSeek + 'a>,
) -> 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<Box<dyn Iterator<Item = Result<Box<dyn ArchiveContent>>> + 'a>> {
Ok(Box::new(std::iter::empty()))
}
#[cfg(feature = "image")]
fn is_image(&self) -> bool {
false
}
#[cfg(feature = "image")]
fn export_image(&self) -> Result<ImageData> {
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<dyn WriteSeek + 'a>) -> 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 {

View File

@@ -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<u8>,
_filename: &str,
_encoding: Encoding,
_archive_encoding: Encoding,
config: &ExtraConfig,
) -> Result<Box<dyn Script>> {
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<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;
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<ImageData> {
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,
})
}
}

View File

@@ -0,0 +1 @@
pub mod img;

View File

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

View File

@@ -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")]

View File

@@ -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<ImageOutputType>,
}
#[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<str> 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<u8>,
}

126
src/utils/img.rs Normal file
View File

@@ -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<ImageData> {
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,
})
}
}
}

View File

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