From 70943f4a3ec1cddda4502f56b6aa8fa9e4ca4005 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Tue, 29 Jul 2025 14:58:30 +0800 Subject: [PATCH] Add decode support for crx --- Cargo.lock | 62 +++++ Cargo.toml | 4 +- msg_tool_macro/src/lib.rs | 217 +++++++++++++++--- src/ext/io.rs | 13 ++ src/scripts/circus/image/crx.rs | 391 ++++++++++++++++++++++++++++++++ src/scripts/circus/image/mod.rs | 1 + src/scripts/circus/mod.rs | 2 + src/scripts/mod.rs | 2 + src/types.rs | 3 + 9 files changed, 666 insertions(+), 29 deletions(-) create mode 100644 src/scripts/circus/image/crx.rs create mode 100644 src/scripts/circus/image/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 022f974..b423fad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,6 +130,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "cc" +version = "1.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -570,6 +581,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom", + "libc", +] + [[package]] name = "json" version = "0.12.4" @@ -648,6 +669,7 @@ dependencies = [ "url", "utf16string", "windows-sys", + "zstd", ] [[package]] @@ -681,6 +703,12 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "png" version = "0.17.16" @@ -843,6 +871,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "simd-adler32" version = "0.3.7" @@ -1149,3 +1183,31 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index cd8ec0d..d517d90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,9 +27,10 @@ sha1 = { version = "0.10", optional = true } unicode-segmentation = "1.12" url = { version = "2.5", optional = true } utf16string = "0.2" +zstd = { version = "0.13", optional = true } [features] -default = ["artemis", "artemis-arc", "bgi", "bgi-arc", "bgi-img", "cat-system", "cat-system-arc", "cat-system-img", "circus", "escude", "escude-arc", "hexen-haus", "kirikiri", "kirikiri-img", "will-plus", "yaneurao", "yaneurao-itufuru"] +default = ["artemis", "artemis-arc", "bgi", "bgi-arc", "bgi-img", "cat-system", "cat-system-arc", "cat-system-img", "circus", "circus-img", "escude", "escude-arc", "hexen-haus", "kirikiri", "kirikiri-img", "will-plus", "yaneurao", "yaneurao-itufuru"] artemis = ["utils-escape"] artemis-arc = ["artemis", "msg_tool_macro/artemis-arc", "sha1"] bgi = [] @@ -39,6 +40,7 @@ cat-system = ["fancy-regex", "flate2", "int-enum"] cat-system-arc = ["cat-system", "blowfish", "utils-crc32"] cat-system-img = ["cat-system", "flate2", "image", "utils-bit-stream"] circus = [] +circus-img = ["circus", "image", "flate2", "zstd"] escude = ["int-enum"] escude-arc = ["escude", "rand", "utils-bit-stream"] hexen-haus = ["memchr", "utils-str"] diff --git a/msg_tool_macro/src/lib.rs b/msg_tool_macro/src/lib.rs index 68c56b5..19f1fba 100644 --- a/msg_tool_macro/src/lib.rs +++ b/msg_tool_macro/src/lib.rs @@ -74,7 +74,10 @@ pub fn struct_unpack_impl_for_num(item: TokenStream) -> TokenStream { /// * `fvec = ` attribute can be used to specify a fixed vector length for Vec<_> fields. /// * `pstring()` attribute can be used to specify a packed string length for String fields, where `` can be `u8`, `u16`, `u32`, or `u64`. /// Length is read as a prefix before the string data. -#[proc_macro_derive(StructPack, attributes(skip_pack, fstring, fstring_pad, fvec, pstring))] +/// * `pvec()` attribute can be used to specify a packed vector length for Vec<_> fields, where `` can be `u8`, `u16`, `u32`, or `u64`. +/// Length is read as a prefix before the vector data. +/// * `skip_pack_if()` attribute can be used to skip packing a field if the expression evaluates to true. The expression must be a valid Rust expression that evaluates to a boolean. +#[proc_macro_derive(StructPack, attributes(skip_pack, fstring, fstring_pad, fvec, pstring, pvec, skip_pack_if))] pub fn struct_pack_derive(input: TokenStream) -> TokenStream { let a = syn::parse_macro_input!(input as PackStruct); match a { @@ -87,6 +90,9 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { let mut fixed_vec: Option = None; let mut fstring_pad = 0u8; // Default padding byte let mut pstring_type: Option = None; + let mut pvec_type: Option = None; + let mut cur = None; + let mut skip_if = None; for attr in &field.attrs { let path = attr.path(); if path.is_ident("skip_pack") { @@ -132,6 +138,27 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { Ok(()) }).unwrap(); } + } else if path.is_ident("pvec") { + if let syn::Meta::List(list) = &attr.meta { + list.parse_nested_meta(|meta| { + if meta.path.is_ident("u8") { + pvec_type = Some(syn::Ident::new("u8", meta.path.span())); + } else if meta.path.is_ident("u16") { + pvec_type = Some(syn::Ident::new("u16", meta.path.span())); + } else if meta.path.is_ident("u32") { + pvec_type = Some(syn::Ident::new("u32", meta.path.span())); + } else if meta.path.is_ident("u64") { + pvec_type = Some(syn::Ident::new("u64", meta.path.span())); + } else { + return Err(meta.error("Expected u8, u16, or u32 for pvec")); + } + Ok(()) + }).unwrap(); + } + } else if path.is_ident("skip_pack_if") { + if let syn::Meta::List(list) = &attr.meta { + skip_if = Some(list.parse_args::().unwrap()); + } } } if skipped { @@ -150,7 +177,7 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { if let Some(segment) = type_path.path.segments.last() { if segment.ident == "String" { if let Some(fixed_string) = fixed_string { - return quote::quote! { + cur = Some(quote::quote! { let s = encode_string(encoding, &self.#field_name, true)?; let mut slen = s.len(); if slen > #fixed_string { @@ -164,35 +191,54 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { for _ in slen..#fixed_string { writer.write_all(&[#fstring_pad])?; } - }; - } - if let Some(pstring_type) = pstring_type { + }); + } else if let Some(pstring_type) = pstring_type { let write_fn = syn::Ident::new(format!("write_{}", pstring_type).as_str(), pstring_type.span()); - return quote::quote! { + cur = Some(quote::quote! { let encoded = crate::utils::encoding::encode_string(encoding, &self.#field_name, true)?; writer.#write_fn(encoded.len() as #pstring_type)?; writer.write_all(&encoded)?; - }; + }); } } } if let Some(segment) = type_path.path.segments.first() { if segment.ident == "Vec" { if let Some(fixed_vec) = fixed_vec { - return quote::quote! { + cur = Some(quote::quote! { if self.#field_name.len() != #fixed_vec { return Err(anyhow::anyhow!("Vector length was not equal to {}", #fixed_vec)); } for item in &self.#field_name { item.pack(writer, big, encoding)?; } - }; + }); + } else if let Some(pvec_type) = pvec_type { + let write_fn = syn::Ident::new(format!("write_{}", pvec_type).as_str(), pvec_type.span()); + cur = Some(quote::quote! { + let len = self.#field_name.len() as #pvec_type; + writer.#write_fn(len)?; + for item in &self.#field_name { + item.pack(writer, big, encoding)?; + } + }); } } } } - quote::quote! { - self.#field_name.pack(writer, big, encoding)?; + let p = cur.unwrap_or_else(|| { + quote::quote! { + self.#field_name.pack(writer, big, encoding)?; + } + }); + if let Some(skip_if) = skip_if { + quote::quote! { + if !(#skip_if) { + #p + } + } + } else { + p } }); let output = quote::quote! { @@ -226,6 +272,10 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { let mut fixed_string: Option = None; let mut fixed_vec: Option = None; let mut fstring_pad = 0u8; // Default padding byte + let mut pstring_type: Option = None; + let mut pvec_type: Option = None; + let mut cur = None; + let mut skip_if = None; for attr in &field.attrs { let path = attr.path(); if path.is_ident("skip_pack") { @@ -254,6 +304,44 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { } } } + } else if path.is_ident("pstring") { + if let syn::Meta::List(list) = &attr.meta { + list.parse_nested_meta(|meta| { + if meta.path.is_ident("u8") { + pstring_type = Some(syn::Ident::new("u8", meta.path.span())); + } else if meta.path.is_ident("u16") { + pstring_type = Some(syn::Ident::new("u16", meta.path.span())); + } else if meta.path.is_ident("u32") { + pstring_type = Some(syn::Ident::new("u32", meta.path.span())); + } else if meta.path.is_ident("u64") { + pstring_type = Some(syn::Ident::new("u64", meta.path.span())); + } else { + return Err(meta.error("Expected u8, u16, or u32 for pstring")); + } + Ok(()) + }).unwrap(); + } + } else if path.is_ident("pvec") { + if let syn::Meta::List(list) = &attr.meta { + list.parse_nested_meta(|meta| { + if meta.path.is_ident("u8") { + pvec_type = Some(syn::Ident::new("u8", meta.path.span())); + } else if meta.path.is_ident("u16") { + pvec_type = Some(syn::Ident::new("u16", meta.path.span())); + } else if meta.path.is_ident("u32") { + pvec_type = Some(syn::Ident::new("u32", meta.path.span())); + } else if meta.path.is_ident("u64") { + pvec_type = Some(syn::Ident::new("u64", meta.path.span())); + } else { + return Err(meta.error("Expected u8, u16, or u32 for pvec")); + } + Ok(()) + }).unwrap(); + } + } else if path.is_ident("skip_pack_if") { + if let syn::Meta::List(list) = &attr.meta { + skip_if = Some(list.parse_args::().unwrap()); + } } } if skipped { @@ -273,7 +361,7 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { if let Some(segment) = type_path.path.segments.last() { if segment.ident == "String" { if let Some(fixed_string) = fixed_string { - return quote::quote! { + cur = Some(quote::quote! { let s = encode_string(encoding, &#field_name, true)?; let mut slen = s.len(); if slen > #fixed_string { @@ -287,27 +375,54 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { for _ in slen..#fixed_string { writer.write_all(&[#fstring_pad])?; } - }; + }); + } else if let Some(pstring_type) = pstring_type { + let write_fn = syn::Ident::new(format!("write_{}", pstring_type).as_str(), pstring_type.span()); + cur = Some(quote::quote! { + let encoded = crate::utils::encoding::encode_string(encoding, &#field_name, true)?; + writer.#write_fn(encoded.len() as #pstring_type)?; + writer.write_all(&encoded)?; + }); } } } if let Some(segment) = type_path.path.segments.first() { if segment.ident == "Vec" { if let Some(fixed_vec) = fixed_vec { - return quote::quote! { + cur = Some(quote::quote! { if #field_name.len() != #fixed_vec { return Err(anyhow::anyhow!("Vector length was not equal to {}", #fixed_vec)); } for item in &#field_name { item.pack(writer, big, encoding)?; } - }; + }); + } else if let Some(pvec_type) = pvec_type { + let write_fn = syn::Ident::new(format!("write_{}", pvec_type).as_str(), pvec_type.span()); + cur = Some(quote::quote! { + let len = #field_name.len() as #pvec_type; + writer.#write_fn(len)?; + for item in &#field_name { + item.pack(writer, big, encoding)?; + } + }); } } } } - quote::quote! { - #field_name.pack(writer, big, encoding)?; + let p = cur.unwrap_or_else(|| { + quote::quote! { + #field_name.pack(writer, big, encoding)?; + } + }); + if let Some(skip_if) = skip_if { + quote::quote! { + if !(#skip_if) { + #p + } + } + } else { + p } }).collect(); let idents = if is_struct_like { @@ -351,7 +466,10 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { /// * `fvec = ` attribute can be used to specify a fixed vector length for Vec<_> fields. /// * `pstring()` attribute can be used to specify a packed string length for String fields, where `` can be `u8`, `u16`, `u32` or `u64`. /// length is read as a prefix before the string data. -#[proc_macro_derive(StructUnpack, attributes(skip_unpack, fstring, fstring_no_trim, fvec, pstring))] +/// * `pvec()` attribute can be used to specify a packed vector length for Vec<_> fields, where `` can be `u8`, `u16`, `u32` or `u64`. +/// length is read as a prefix before the vector data. +/// * `skip_unpack_if()` attribute can be used to skip unpacking a field if the expression evaluates to true. The expression must be a valid Rust expression that evaluates to a boolean. +#[proc_macro_derive(StructUnpack, attributes(skip_unpack, fstring, fstring_no_trim, fvec, pstring, pvec, skip_unpack_if))] pub fn struct_unpack_derive(input: TokenStream) -> TokenStream { let sut = syn::parse_macro_input!(input as syn::ItemStruct); let name = sut.ident; @@ -364,6 +482,9 @@ pub fn struct_unpack_derive(input: TokenStream) -> TokenStream { let mut fstring_no_trim = false; let mut fixed_vec: Option = None; let mut pstring_type: Option = None; + let mut pvec_type: Option = None; + let mut cur = None; + let mut skip_if: Option = None; for attr in &field.attrs { let path = attr.path(); if path.is_ident("skip_unpack") { @@ -403,6 +524,27 @@ pub fn struct_unpack_derive(input: TokenStream) -> TokenStream { Ok(()) }).unwrap(); } + } else if path.is_ident("pvec") { + if let syn::Meta::List(list) = &attr.meta { + list.parse_nested_meta(|meta| { + if meta.path.is_ident("u8") { + pvec_type = Some(syn::Ident::new("u8", meta.path.span())); + } else if meta.path.is_ident("u16") { + pvec_type = Some(syn::Ident::new("u16", meta.path.span())); + } else if meta.path.is_ident("u32") { + pvec_type = Some(syn::Ident::new("u32", meta.path.span())); + } else if meta.path.is_ident("u64") { + pvec_type = Some(syn::Ident::new("u64", meta.path.span())); + } else { + return Err(meta.error("Expected u8, u16, or u32 for pvec")); + } + Ok(()) + }).unwrap(); + } + } else if path.is_ident("skip_unpack_if") { + if let syn::Meta::List(list) = &attr.meta { + skip_if = Some(list.parse_args::().unwrap()); + } } } let field_name = match &field.ident { @@ -426,32 +568,51 @@ pub fn struct_unpack_derive(input: TokenStream) -> TokenStream { if segment.ident == "String" { if let Some(fixed_string) = fixed_string { let trim = syn::LitBool::new(!fstring_no_trim, field.span()); - return quote::quote! { + cur = Some(quote::quote! { let #field_name = reader.read_fstring(#fixed_string, encoding, #trim)?; - }; - } - if let Some(pstring_type) = pstring_type { + }); + } else if let Some(pstring_type) = pstring_type { let read_fn = syn::Ident::new(format!("read_{}", pstring_type).as_str(), pstring_type.span()); - return quote::quote! { + cur = Some(quote::quote! { let len = reader.#read_fn()? as usize; let #field_name = reader.read_exact_vec(len)?; let #field_name = crate::utils::encoding::decode_to_string(encoding, &#field_name, true)?; - } + }); } } } if let Some(segment) = type_path.path.segments.first() { if segment.ident == "Vec" { if let Some(fixed_vec) = fixed_vec { - return quote::quote! { + cur = Some(quote::quote! { let #field_name = reader.read_struct_vec(#fixed_vec, big, encoding)?; - }; + }); + } else if let Some(pvec_type) = pvec_type { + let read_fn = syn::Ident::new(format!("read_{}", pvec_type).as_str(), pvec_type.span()); + cur = Some(quote::quote! { + let len = reader.#read_fn()? as usize; + let #field_name = reader.read_struct_vec(len, big, encoding)?; + }); } } } } - quote::quote! { - let #field_name = #field_type::unpack(&mut reader, big, encoding)?; + let p = cur.unwrap_or_else(|| { + quote::quote! { + let #field_name = #field_type::unpack(&mut reader, big, encoding)?; + } + }); + if let Some(skip_if) = skip_if { + quote::quote! { + let #field_name = if !(#skip_if) { + #p + #field_name + } else { + Default::default() + }; + } + } else { + p } }).collect(); let fields = if is_tuple_struct { diff --git a/src/ext/io.rs b/src/ext/io.rs index 322a37a..98abd13 100644 --- a/src/ext/io.rs +++ b/src/ext/io.rs @@ -922,6 +922,19 @@ impl WriteAt for T { } } +pub trait SeekExt { + fn stream_length(&mut self) -> Result; +} + +impl SeekExt for T{ + fn stream_length(&mut self) -> Result { + let current_pos = self.stream_position()?; + let length = self.seek(SeekFrom::End(0))?; + self.seek(SeekFrom::Start(current_pos))?; + Ok(length) + } +} + pub struct MemReader { pub data: Vec, pub pos: usize, diff --git a/src/scripts/circus/image/crx.rs b/src/scripts/circus/image/crx.rs new file mode 100644 index 0000000..d8e88cf --- /dev/null +++ b/src/scripts/circus/image/crx.rs @@ -0,0 +1,391 @@ +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::struct_pack::*; +use anyhow::Result; +use msg_tool_macro::*; +use std::io::{Read, Seek, Write}; + +#[derive(Debug)] +pub struct CrxImageBuilder {} + +impl CrxImageBuilder { + pub const fn new() -> Self { + CrxImageBuilder {} + } +} + +impl ScriptBuilder for CrxImageBuilder { + 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(CrxImage::new(MemReader::new(data), config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["crx"] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::CircusCrx + } + + fn is_image(&self) -> bool { + true + } + + fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option { + if buf_len >= 4 && buf.starts_with(b"CRXG") { + return Some(255); + } + None + } +} + +#[derive(Clone, Debug, StructPack, StructUnpack)] +struct Clip { + field_0: u32, + clip_width: u16, + clip_height: u16, + field_8: u16, + field_a: u16, + width: u16, + height: u16, +} + +#[derive(Clone, Debug, StructPack, StructUnpack)] +struct Header { + inner_x: u16, + inner_y: u16, + width: u16, + height: u16, + version: u16, + flags: u16, + bpp: u16, + mode: u16, + #[skip_pack_if(self.version != 3)] + #[skip_unpack_if(version != 3)] + #[pvec(u32)] + clips: Vec, +} + +pub struct CrxImage { + header: Header, + color_type: ImageColorType, + data: Vec, +} + +impl std::fmt::Debug for CrxImage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CrxImage") + .field("header", &self.header) + .field("color_type", &self.color_type) + .field("data_length", &self.data.len()) + .finish() + } +} + +impl CrxImage { + pub fn new(data: T, _config: &ExtraConfig) -> Result { + let mut reader = data; + let mut magic = [0; 4]; + reader.read_exact(&mut magic)?; + if magic != *b"CRXG" { + return Err(anyhow::anyhow!("Invalid CRX image magic")); + } + let header: Header = reader.read_struct(false, Encoding::Utf8)?; + if header.version < 2 || header.version > 3 { + return Err(anyhow::anyhow!( + "Unsupported CRX version: {}", + header.version + )); + } + let color_type = if header.bpp == 0 { + ImageColorType::Bgr + } else if header.bpp == 1 { + ImageColorType::Bgra + } else { + return Err(anyhow::anyhow!("Unsupported CRX bpp: {}", header.bpp)); + }; + let compressed_size = if (header.flags & 0x10) == 0 { + let len = reader.stream_length()?; + (len - reader.stream_position()?) as u32 + } else { + reader.read_u32()? + }; + let compressed_data = reader.read_exact_vec(compressed_size as usize)?; + let uncompessed = if compressed_data.starts_with(&[0x28, 0xb5, 0x2f, 0xfd]) { + let mut decoder = zstd::Decoder::new(MemReaderRef::new(&compressed_data))?; + let mut decompressed_data = Vec::new(); + decoder.read_to_end(&mut decompressed_data)?; + decompressed_data + } else { + let mut decompressed_data = Vec::new(); + flate2::read::ZlibDecoder::new(MemReaderRef::new(&compressed_data)) + .read_to_end(&mut decompressed_data)?; + decompressed_data + }; + Ok(CrxImage { + header, + color_type, + data: uncompessed, + }) + } + + fn decode_row0( + dst: &mut Vec, + mut dst_p: usize, + src: &[u8], + mut src_p: usize, + width: u16, + pixel_size: u8, + ) -> Result { + let mut prev_p = dst_p; + for _ in 0..pixel_size { + dst[dst_p] = src[src_p]; + dst_p += 1; + src_p += 1; + } + let remaining = width - 1; + for _ in 0..remaining { + for _ in 0..pixel_size { + dst[dst_p] = src[src_p].overflowing_add(dst[prev_p]).0; + dst_p += 1; + src_p += 1; + prev_p += 1; + } + } + Ok(src_p) + } + + fn decode_row1( + dst: &mut Vec, + mut dst_p: usize, + src: &[u8], + mut src_p: usize, + width: u16, + pixel_size: u8, + mut prev_row_p: usize, + ) -> Result { + for _ in 0..width { + for _ in 0..pixel_size { + dst[dst_p] = src[src_p].overflowing_add(dst[prev_row_p]).0; + dst_p += 1; + src_p += 1; + prev_row_p += 1; + } + } + Ok(src_p) + } + + fn decode_row2( + dst: &mut Vec, + mut dst_p: usize, + src: &[u8], + mut src_p: usize, + width: u16, + pixel_size: u8, + mut prev_row_p: usize, + ) -> Result { + for _ in 0..pixel_size { + dst[dst_p] = src[src_p]; + dst_p += 1; + src_p += 1; + } + let remaining = width - 1; + for _ in 0..remaining { + for _ in 0..pixel_size { + dst[dst_p] = src[src_p].overflowing_add(dst[prev_row_p]).0; + dst_p += 1; + src_p += 1; + prev_row_p += 1; + } + } + Ok(src_p) + } + + fn decode_row3( + dst: &mut Vec, + mut dst_p: usize, + src: &[u8], + mut src_p: usize, + width: u16, + pixel_size: u8, + mut prev_row_p: usize, + ) -> Result { + let count = width - 1; + prev_row_p += pixel_size as usize; + for _ in 0..count { + for _ in 0..pixel_size { + dst[dst_p] = src[src_p].overflowing_add(dst[prev_row_p]).0; + dst_p += 1; + src_p += 1; + prev_row_p += 1; + } + } + for _ in 0..pixel_size { + dst[dst_p] = src[src_p]; + dst_p += 1; + src_p += 1; + } + Ok(src_p) + } + + fn decode_row4( + dst: &mut Vec, + dst_p: usize, + src: &[u8], + mut src_p: usize, + width: u16, + pixel_size: u8, + ) -> Result { + for offset in 0..pixel_size { + let mut dst_c = dst_p + offset as usize; + let mut remaining = width; + let value = src[src_p]; + src_p += 1; + dst[dst_c] = value; + dst_c += pixel_size as usize; + remaining -= 1; + if remaining == 0 { + continue; + } + if value == src[src_p] { + src_p += 1; + let count = src[src_p] as u16; + src_p += 1; + remaining -= count; + for _ in 0..count { + dst[dst_c] = value; + dst_c += pixel_size as usize; + } + } + while remaining > 0 { + let value = src[src_p]; + src_p += 1; + dst[dst_c] = value; + dst_c += pixel_size as usize; + remaining -= 1; + if remaining == 0 { + break; + } + if value == src[src_p] { + src_p += 1; + let count = src[src_p] as u16; + src_p += 1; + remaining -= count; + for _ in 0..count { + dst[dst_c] = value; + dst_c += pixel_size as usize; + } + } + } + } + Ok(src_p) + } + + fn decode_image( + dst: &mut Vec, + src: &[u8], + width: u16, + height: u16, + pixel_size: u8, + encode_type: &mut Vec, + ) -> Result<()> { + let mut src_p = 0; + let mut dst_p = 0; + let mut prev_row_p = 0; + for _ in 0..height { + let data = src[src_p]; + encode_type.push(data); + src_p += 1; + match data { + 0 => { + src_p = Self::decode_row0(dst, dst_p, src, src_p, width, pixel_size)?; + } + 1 => { + src_p = + Self::decode_row1(dst, dst_p, src, src_p, width, pixel_size, prev_row_p)?; + } + 2 => { + src_p = + Self::decode_row2(dst, dst_p, src, src_p, width, pixel_size, prev_row_p)?; + } + 3 => { + src_p = + Self::decode_row3(dst, dst_p, src, src_p, width, pixel_size, prev_row_p)?; + } + 4 => { + src_p = Self::decode_row4(dst, dst_p, src, src_p, width, pixel_size)?; + } + _ => { + return Err(anyhow::anyhow!("Invalid row type: {}", data)); + } + } + prev_row_p = dst_p; + dst_p += pixel_size as usize * width as usize; + } + Ok(()) + } +} + +impl Script for CrxImage { + 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 data_size = self.color_type.bpp(1) as usize * self.header.width as usize * self.header.height as usize; + let mut data = vec![0; data_size]; + let mut encode_type = Vec::new(); + Self::decode_image( + &mut data, + &self.data, + self.header.width, + self.header.height, + self.color_type.bpp(1) as u8, + &mut encode_type, + )?; + if self.color_type.bpp(1) == 4 && self.header.mode != 1 { + let alpha_flip = if self.header.mode == 2 { + 0 + } else { + 0xFF + }; + for i in (0..data_size).step_by(4) { + let a = data[i]; + let b = data[i + 1]; + let g = data[i + 2]; + let r = data[i + 3]; + data[i] = b; + data[i + 1] = g; + data[i + 2] = r; + data[i + 3] = a ^ alpha_flip; + } + } + Ok(ImageData { + width: self.header.width as u32, + height: self.header.height as u32, + depth: 8, + color_type: self.color_type, + data, + }) + } +} diff --git a/src/scripts/circus/image/mod.rs b/src/scripts/circus/image/mod.rs new file mode 100644 index 0000000..93d9133 --- /dev/null +++ b/src/scripts/circus/image/mod.rs @@ -0,0 +1 @@ +pub mod crx; diff --git a/src/scripts/circus/mod.rs b/src/scripts/circus/mod.rs index 438a8b3..a74b4cb 100644 --- a/src/scripts/circus/mod.rs +++ b/src/scripts/circus/mod.rs @@ -1,2 +1,4 @@ +#[cfg(feature = "circus-img")] +pub mod image; mod info; pub mod script; diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 733e5f4..1262f5a 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -80,6 +80,8 @@ lazy_static::lazy_static! { Box::new(artemis::asb::ArtemisAsbBuilder::new()), #[cfg(feature = "hexen-haus")] Box::new(hexen_haus::bin::BinScriptBuilder::new()), + #[cfg(feature = "circus-img")] + Box::new(circus::image::crx::CrxImageBuilder::new()), ]; pub static ref ALL_EXTS: Vec = BUILDER.iter().flat_map(|b| b.extensions()).map(|s| s.to_string()).collect(); diff --git a/src/types.rs b/src/types.rs index d5c5827..cf71811 100644 --- a/src/types.rs +++ b/src/types.rs @@ -292,6 +292,9 @@ pub enum ScriptType { #[cfg(feature = "circus")] /// Circus MES script Circus, + #[cfg(feature = "circus-img")] + /// Circus CRX Image + CircusCrx, #[cfg(feature = "escude-arc")] /// Escude bin archive EscudeArc,