mirror of
https://github.com/lifegpc/msg-tool.git
synced 2026-06-16 18:14:19 +08:00
Add BGI Image decode support
Fix DSC decompress
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
32
src/main.rs
32
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 } => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
112
src/scripts/bgi/image/img.rs
Normal file
112
src/scripts/bgi/image/img.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
1
src/scripts/bgi/image/mod.rs
Normal file
1
src/scripts/bgi/image/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod img;
|
||||
@@ -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;
|
||||
|
||||
@@ -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")]
|
||||
|
||||
46
src/types.rs
46
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<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
126
src/utils/img.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user