Add support for BC7 encryption

This commit is contained in:
2026-03-25 12:44:38 +08:00
parent fd5b78e788
commit 2b6df23b9e
4 changed files with 192 additions and 4 deletions

24
Cargo.lock generated
View File

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

View File

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

View File

@@ -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<Vec<u8>> {
&self.resources

View File

@@ -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<Resource> {
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<Resource> {
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<Resource> {
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<TlgInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
rle: Option<RLPixelInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
bc7: Option<BC7PixelInfo>,
}
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<Resource> = serde_json::from_str(&data["resources"].dump())?;
let extra_resources: Vec<Resource> = 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("");