diff --git a/Cargo.lock b/Cargo.lock index 069b55f..2ba4184 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,6 +165,15 @@ dependencies = [ "objc2", ] +[[package]] +name = "block_compression" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c3bb476aa920a10e349d32cc211783259ab86584b7b1af57785503a3eafdc1" +dependencies = [ + "bytemuck", +] + [[package]] name = "borsh" version = "1.6.0" @@ -201,6 +210,20 @@ name = "bytemuck" version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] [[package]] name = "byteorder" @@ -1333,6 +1356,7 @@ dependencies = [ "adler", "anyhow", "base64", + "block_compression", "byteorder", "clap 4.5.54", "crc32fast", diff --git a/Cargo.toml b/Cargo.toml index 93b9957..cfd239c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ exclude = [".github", "*.py", "AGENTS.md"] adler = { version = "1", optional = true } anyhow = "1" base64 = { version = "0.22", optional = true } +block_compression = { version = "0.9", optional = true, default-features = false, features = ["bc7"] } byteorder = { version = "1.5", default-features = false, optional = true} clap = { version = "4.5", features = ["derive"] } crc32fast = { version = "1.5", optional = true } @@ -78,7 +79,7 @@ circus = [] circus-arc = ["circus"] circus-audio = ["circus", "flate2", "int-enum", "lossless-audio"] circus-img = ["circus", "image", "flate2", "zstd"] -emote-img = ["base64", "emote-psb", "image", "json", "libtlg-rs", "url", "utils-psd"] +emote-img = ["base64", "block_compression", "emote-psb", "image", "json", "libtlg-rs", "url", "utils-psd"] entis-gls = ["xml5ever", "markup5ever", "markup5ever_rcdom", "int-enum"] escude = ["int-enum"] escude-arc = ["escude", "rand", "utils-bit-stream"] diff --git a/src/ext/psb.rs b/src/ext/psb.rs index c12bc8b..792b1cb 100644 --- a/src/ext/psb.rs +++ b/src/ext/psb.rs @@ -1140,6 +1140,11 @@ impl VirtualPsbFixed { self.header } + /// Return a mutable reference to the header of the PSB. + pub fn header_mut(&mut self) -> &mut PsbHeader { + &mut self.header + } + /// Returns a reference to the resources of the PSB. pub fn resources(&self) -> &Vec> { &self.resources diff --git a/src/scripts/emote/psb.rs b/src/scripts/emote/psb.rs index 12f4195..3fac62d 100644 --- a/src/scripts/emote/psb.rs +++ b/src/scripts/emote/psb.rs @@ -9,6 +9,8 @@ use crate::utils::files::*; use crate::utils::img::*; use anyhow::Result; use base64::Engine; +use block_compression::BC7Settings; +use clap::ValueEnum; use emote_psb::*; use libtlg_rs::*; use serde::{Deserialize, Serialize}; @@ -103,6 +105,26 @@ impl ScriptBuilder for PsbBuilder { } } +#[derive(Debug, ValueEnum, Clone, Copy)] +pub enum BC7Config { + /// Ultra fast settings. + UltraFast, + /// Very fast settings. + VeryFast, + /// Fast settings. + Fast, + /// Basic settings. + Basic, + /// Slow settings. + Slow, +} + +impl Default for BC7Config { + fn default() -> Self { + Self::Basic + } +} + #[derive(Debug)] pub struct Psb { psb: VirtualPsbFixed, @@ -132,8 +154,7 @@ impl Psb { ) -> Result { let mut res = Resource { path, - tlg: None, - rle: None, + ..Default::default() }; if self.config.psb_process_tlg && is_valid_tlg(&data) { let tlg = load_tlg(MemReaderRef::new(&data))?; @@ -176,8 +197,8 @@ impl Psb { ) -> Result { let mut res = Resource { path, - tlg: None, rle: Some(RLPixelInfo { width, height }), + ..Default::default() }; let decompressed = rl_decompress(MemReaderRef::new(data), 4, None)?; let outtype = self.config.image_type.unwrap_or(ImageOutputType::Png); @@ -198,6 +219,58 @@ impl Psb { encode_img(img, outtype, &path.to_string_lossy(), &self.config)?; Ok(res) } + + fn output_bc7_resource( + &self, + folder_path: &std::path::PathBuf, + path: String, + data: &[u8], + width: i64, + height: i64, + ) -> Result { + let mut res = Resource { + path, + bc7: Some(BC7PixelInfo { width, height }), + ..Default::default() + }; + let dst_size = (width * height * 4) as usize; + let mut decompressed_block = vec![0u8; dst_size]; + let variant = block_compression::CompressionVariant::BC7(BC7Settings::alpha_basic()); + let blocks_bytes = variant.blocks_byte_size(width as u32, height as u32); + if data.len() != blocks_bytes { + return Err(anyhow::anyhow!( + "BC7 compressed data size {} does not match expected size {} for image size {}x{}", + data.len(), + blocks_bytes, + width, + height + )); + } + block_compression::decode::decompress_blocks_as_rgba8( + variant, + width as u32, + height as u32, + data, + &mut decompressed_block, + ); + let outtype = self.config.image_type.unwrap_or(ImageOutputType::Png); + res.path = { + let mut pb = std::path::PathBuf::from(&res.path); + pb.set_extension(outtype.as_ref()); + pb.to_string_lossy().to_string() + }; + let path = folder_path.join(&res.path); + make_sure_dir_exists(&path)?; + let img = ImageData { + width: width as u32, + height: height as u32, + color_type: ImageColorType::Rgba, + depth: 8, + data: decompressed_block, + }; + encode_img(img, outtype, &path.to_string_lossy(), &self.config)?; + Ok(res) + } } #[derive(Debug, Deserialize, Serialize)] @@ -256,12 +329,20 @@ struct RLPixelInfo { } #[derive(Debug, Deserialize, Serialize)] +struct BC7PixelInfo { + width: i64, + height: i64, +} + +#[derive(Debug, Default, Deserialize, Serialize)] struct Resource { path: String, #[serde(skip_serializing_if = "Option::is_none")] tlg: Option, #[serde(skip_serializing_if = "Option::is_none")] rle: Option, + #[serde(skip_serializing_if = "Option::is_none")] + bc7: Option, } impl Script for Psb { @@ -303,6 +384,7 @@ impl Script for Psb { let width = pb_data["width"].as_i64(); let height = pb_data["height"].as_i64(); let compress = pb_data["compress"].as_str(); + let type_ = pb_data["type"].as_str(); if compress.is_some_and(|s| s == "RL") && (width.is_none() || height.is_none()) { eprintln!( @@ -311,6 +393,13 @@ impl Script for Psb { ); crate::COUNTER.inc_warning(); } + if type_.is_some_and(|s| s == "BC7") && (width.is_none() || height.is_none()) { + eprintln!( + "Warning: Resource {:?} is marked as BC7 compressed but width/height is missing (width={:?}, height={:?})", + path, pb_data["width"], pb_data["height"] + ); + crate::COUNTER.inc_warning(); + } if let (Some(w), Some(h), Some(c)) = (width, height, compress) { if c == "RL" { let res_name: Vec<_> = path @@ -326,6 +415,21 @@ impl Script for Psb { continue; } } + if let (Some(w), Some(h), Some(t)) = (width, height, type_) { + if t == "BC7" { + let res_name: Vec<_> = path + .iter() + .take(path.len() - 1) + .map(|s| s.to_string()) + .collect(); + let res_name = res_name.join("/"); + let res_name = sanitize_path(&res_name); + let res = + self.output_bc7_resource(&folder_path, res_name, data, w, h)?; + resources.push(res); + continue; + } + } } } let res_name = res_path @@ -432,15 +536,60 @@ fn read_resource( "Warning: Image width {} does not match RLE width {}", img.width, rle.width ); + crate::COUNTER.inc_warning(); } if img.height as i64 != rle.height { eprintln!( "Warning: Image height {} does not match RLE height {}", img.height, rle.height ); + crate::COUNTER.inc_warning(); } let compressed = rl_compress(MemReaderRef::new(&img.data), 4)?; Ok(compressed) + } else if let Some(bc7) = &res.bc7 { + let path = folder_path.join(&res.path); + let imgfmt = ImageOutputType::try_from(path.as_path())?; + let mut img = decode_img(imgfmt, &path.to_string_lossy())?; + if img.depth != 8 { + return Err(anyhow::anyhow!( + "Only 8-bit images are supported for BC7 conversion" + )); + } + if img.width % 4 != 0 || img.height % 4 != 0 { + return Err(anyhow::anyhow!( + "Image dimensions must be multiples of 4 for BC7 conversion (width={}, height={})", + img.width, + img.height + )); + } + if bc7.height != img.height as i64 { + eprintln!( + "Warning: Image height {} does not match BC7 height {}", + img.height, bc7.height + ); + crate::COUNTER.inc_warning(); + } + if bc7.width != img.width as i64 { + eprintln!( + "Warning: Image width {} does not match BC7 width {}", + img.width, bc7.width + ); + crate::COUNTER.inc_warning(); + } + convert_to_rgba(&mut img)?; + let variant = block_compression::CompressionVariant::BC7(BC7Settings::alpha_basic()); + let dst_size = variant.blocks_byte_size(img.width, img.height); + let mut compressed = vec![0u8; dst_size as usize]; + block_compression::encode::compress_rgba8( + variant, + &img.data, + &mut compressed, + img.width, + img.height, + img.width * 4, + ); + Ok(compressed) } else { let path = folder_path.join(&res.path); Ok(std::fs::read(&path)?) @@ -459,6 +608,15 @@ fn create_file<'a>( let resources: Vec = serde_json::from_str(&data["resources"].dump())?; let extra_resources: Vec = serde_json::from_str(&data["extra_resources"].dump())?; let mut psb = VirtualPsbFixed::with_json(&data)?; + if psb.header().version > 3 { + eprintln!( + "Warning: PSB version {} is higher than 3, downgrading to 3. Some features may not be supported.", + psb.header().version + ); + crate::COUNTER.inc_warning(); + psb.header_mut().version = 3; + } + psb.header_mut().encryption = 0; // We don't support encryption. let folder_path = { let mut pb = std::path::PathBuf::from(custom_filename); pb.set_extension("");