From d2bcc4d4bb389903ede85240bebe9572f4daec75 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Tue, 6 Jan 2026 12:24:37 +0800 Subject: [PATCH] Add zopfli support for xp3 pack --- Cargo.lock | 19 ++++++ Cargo.toml | 3 +- src/args.rs | 23 ++++++- src/main.rs | 8 +++ .../kirikiri/archive/xp3pack/writer.rs | 63 ++++++++++++++++++- src/types.rs | 19 ++++++ 6 files changed, 130 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fbdf03c..c1371fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,6 +184,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + [[package]] name = "bytecount" version = "0.6.9" @@ -1371,6 +1377,7 @@ dependencies = [ "windows-sys 0.61.2", "xml5ever", "xp3", + "zopfli", "zstd", ] @@ -2538,6 +2545,18 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index a271193..01d0140 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ utf16string = "0.2" webp = { version = "0.3", default-features = false, optional = true } xml5ever = { version = "0.36", optional = true } xp3 = { version = "0.3", optional = true} +zopfli = { version = "0.8", optional = true } zstd = { version = "0.13", optional = true } [features] @@ -88,7 +89,7 @@ hexen-haus = ["memchr", "utils-str"] hexen-haus-arc = ["hexen-haus"] hexen-haus-img = ["hexen-haus", "image"] kirikiri = ["emote-psb", "fancy-regex", "flate2", "json", "lz4", "utils-escape"] -kirikiri-arc = ["kirikiri", "adler", "fastcdc", "flate2", "parse-size", "sha2", "xp3", "zstd"] +kirikiri-arc = ["kirikiri", "adler", "fastcdc", "flate2", "parse-size", "sha2", "xp3", "zopfli", "zstd"] kirikiri-img = ["kirikiri", "image", "libtlg-rs"] musica = [] musica-arc = ["musica", "crc32fast", "flate2", "include-flate", "utils-blowfish", "utils-rc4", "utils-serde-base64bytes", "utils-xored-stream"] diff --git a/src/args.rs b/src/args.rs index d6e830d..75566d9 100644 --- a/src/args.rs +++ b/src/args.rs @@ -106,6 +106,7 @@ pub fn get_musica_game_title_value_parser() -> Vec group = ArgGroup::new("webp_qualityg").multiple(false), group = ArgGroup::new("cat_system_int_encrypt_passwordg").multiple(false), group = ArgGroup::new("kirikiri_chat_jsong").multiple(false), + group = ArgGroup::new("xp3-compression").multiple(false), )] #[command( version, @@ -539,10 +540,14 @@ pub struct Arg { /// Workers count for compress files in Kirikiri XP3 archive when creating in parallel. pub xp3_compress_workers: usize, #[cfg(feature = "kirikiri-arc")] - #[arg(long, global = true)] + #[arg(long, global = true, group = "xp3-compression")] /// Use zstd compression for files in Kirikiri XP3 archive when creating. (Warning: Kirikiri engine don't support this. Hook is required.) pub xp3_zstd: bool, #[cfg(feature = "kirikiri-arc")] + #[arg(long, global = true, group = "xp3-compression")] + /// Use zopfli compression for files in Kirikiri XP3 archive when creating. This is very slow. + pub xp3_zopfli: bool, + #[cfg(feature = "kirikiri-arc")] #[arg( long, global = true, @@ -587,6 +592,22 @@ pub struct Arg { /// Add an additional space at the end of message in BGI scripts when importing. /// This may help BGI engine to display the message correctly in save/load screen for some games. pub bgi_add_space: bool, + #[cfg(feature = "zopfli")] + #[arg(long, global = true, default_value_t = std::num::NonZeroU64::new(15).unwrap(), visible_alias = "zp-ic")] + /// Maximum amount of times to rerun forward and backward pass to optimize LZ77 compression cost. + /// Good values: 10, 15 for small files, 5 for files over several MB in size or it will be too slow. + /// Default is 15. + pub zopfli_iteration_count: std::num::NonZeroU64, + #[cfg(feature = "zopfli")] + #[arg(long, global = true, default_value_t = std::num::NonZeroU64::new(u64::MAX).unwrap(), visible_alias = "zp-iwi")] + /// Stop after rerunning forward and backward pass this many times without finding a smaller representation of the block. + /// Default value: practically infinite (maximum u64 value) + pub zopfli_iterations_without_improvement: std::num::NonZeroU64, + #[cfg(feature = "zopfli")] + #[arg(long, global = true, default_value_t = 15, visible_alias = "zp-mbs")] + /// Maximum amount of blocks to split into (0 for unlimited, but this can give extreme results that hurt compression on some files). + /// Default value: 15. + pub zopfli_maximum_block_splits: u16, #[command(subcommand)] /// Command pub command: Command, diff --git a/src/main.rs b/src/main.rs index e77226f..e31467e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3280,6 +3280,8 @@ fn main() { #[cfg(feature = "kirikiri-arc")] xp3_zstd: arg.xp3_zstd, #[cfg(feature = "kirikiri-arc")] + xp3_zopfli: arg.xp3_zopfli, + #[cfg(feature = "kirikiri-arc")] xp3_pack_workers: arg.xp3_pack_workers, #[cfg(feature = "kirikiri")] kirikiri_language_insert: arg.kirikiri_language_insert, @@ -3295,6 +3297,12 @@ fn main() { bgi_add_space: arg.bgi_add_space, #[cfg(feature = "escude")] escude_op: arg.escude_op, + #[cfg(feature = "zopfli")] + zopfli_iteration_count: arg.zopfli_iteration_count, + #[cfg(feature = "zopfli")] + zopfli_iterations_without_improvement: arg.zopfli_iterations_without_improvement, + #[cfg(feature = "zopfli")] + zopfli_maximum_block_splits: arg.zopfli_maximum_block_splits, }); match &arg.command { args::Command::Export { input, output } => { diff --git a/src/scripts/kirikiri/archive/xp3pack/writer.rs b/src/scripts/kirikiri/archive/xp3pack/writer.rs index eb459be..0783eef 100644 --- a/src/scripts/kirikiri/archive/xp3pack/writer.rs +++ b/src/scripts/kirikiri/archive/xp3pack/writer.rs @@ -76,6 +76,14 @@ pub struct Xp3ArchiveWriter { use_zstd: bool, zstd_compression_level: i32, no_adler: bool, + #[cfg(feature = "zopfli")] + use_zopfli: bool, + #[cfg(feature = "zopfli")] + zopfli_iteration_count: std::num::NonZeroU64, + #[cfg(feature = "zopfli")] + zopfli_iterations_without_improvement: std::num::NonZeroU64, + #[cfg(feature = "zopfli")] + zopfli_maximum_block_splits: u16, } impl Xp3ArchiveWriter> { @@ -119,6 +127,14 @@ impl Xp3ArchiveWriter> { use_zstd: config.xp3_zstd, zstd_compression_level: config.zstd_compression_level, no_adler: config.xp3_no_adler, + #[cfg(feature = "zopfli")] + use_zopfli: config.xp3_zopfli, + #[cfg(feature = "zopfli")] + zopfli_iteration_count: config.zopfli_iteration_count, + #[cfg(feature = "zopfli")] + zopfli_iterations_without_improvement: config.zopfli_iterations_without_improvement, + #[cfg(feature = "zopfli")] + zopfli_maximum_block_splits: config.zopfli_maximum_block_splits, }) } } @@ -219,6 +235,14 @@ impl Archive for Xp3ArchiveWriter { }; let processiong_segments = self.processing_segments.clone(); let use_zstd = self.use_zstd; + #[cfg(feature = "zopfli")] + let use_zopfli = self.use_zopfli; + #[cfg(feature = "zopfli")] + let zopfli_iteration_count = self.zopfli_iteration_count; + #[cfg(feature = "zopfli")] + let zopfli_iterations_without_improvement = self.zopfli_iterations_without_improvement; + #[cfg(feature = "zopfli")] + let zopfli_maximum_block_splits = self.zopfli_maximum_block_splits; let zstd_compression_level = self.zstd_compression_level; self.runner.execute( move |_| { @@ -260,7 +284,18 @@ impl Archive for Xp3ArchiveWriter { workers.execute( move |_| { let data = { - if use_zstd { + if use_zopfli { + let option = zopfli::Options { + iteration_count: zopfli_iteration_count, + iterations_without_improvement: + zopfli_iterations_without_improvement, + maximum_block_splits: + zopfli_maximum_block_splits, + }; + let mut e = zopfli::ZlibEncoder::new(option, zopfli::BlockType::Dynamic, Vec::new())?; + e.write_all(&seg)?; + e.finish()? + } else if use_zstd { let mut e = zstd::stream::Encoder::new( Vec::new(), zstd_compression_level, @@ -410,7 +445,19 @@ impl Archive for Xp3ArchiveWriter { let start = file.seek(std::io::SeekFrom::End(0))?; let size = { let mut writer = if is_compressed { - if use_zstd { + if use_zopfli { + let e = zopfli::ZlibEncoder::new( + zopfli::Options { + iteration_count: zopfli_iteration_count, + iterations_without_improvement: + zopfli_iterations_without_improvement, + maximum_block_splits: zopfli_maximum_block_splits, + }, + zopfli::BlockType::Dynamic, + &mut *file, + )?; + Box::new(e) as Box + } else if use_zstd { let e = zstd::stream::Encoder::new( &mut *file, zstd_compression_level, @@ -521,7 +568,17 @@ impl Archive for Xp3ArchiveWriter { } let index_data = index_data.into_inner(); if self.compress_index { - let compressed_index = if self.use_zstd { + let compressed_index = if self.use_zopfli { + let option = zopfli::Options { + iteration_count: self.zopfli_iteration_count, + iterations_without_improvement: self.zopfli_iterations_without_improvement, + maximum_block_splits: self.zopfli_maximum_block_splits, + }; + let mut e = + zopfli::ZlibEncoder::new(option, zopfli::BlockType::Dynamic, Vec::new())?; + e.write_all(&index_data)?; + e.finish()? + } else if self.use_zstd { let mut e = zstd::stream::Encoder::new(Vec::new(), self.zstd_compression_level)?; e.write_all(&index_data)?; e.finish()? diff --git a/src/types.rs b/src/types.rs index b7ae827..ee9ed74 100644 --- a/src/types.rs +++ b/src/types.rs @@ -511,6 +511,9 @@ pub struct ExtraConfig { /// Use zstd compression for files in Kirikiri XP3 archive when creating. (Warning: Kirikiri engine don't support this. Hook is required.) pub xp3_zstd: bool, #[cfg(feature = "kirikiri-arc")] + /// Use zopfli compression for files in Kirikiri XP3 archive when creating. This is very slow. + pub xp3_zopfli: bool, + #[cfg(feature = "kirikiri-arc")] #[default(1)] /// Workers count for packing file in Kirikiri XP3 archive in parallel. Default is 1. /// This not works when segment is disabled. @@ -538,6 +541,22 @@ pub struct ExtraConfig { #[cfg(feature = "escude")] /// Escude game title pub escude_op: Option, + #[cfg(feature = "zopfli")] + #[default(std::num::NonZeroU64::new(15).unwrap())] + /// Maximum amount of times to rerun forward and backward pass to optimize LZ77 compression cost. + /// Good values: 10, 15 for small files, 5 for files over several MB in size or it will be too slow. + /// Default is 15. + pub zopfli_iteration_count: std::num::NonZeroU64, + #[cfg(feature = "zopfli")] + #[default(std::num::NonZeroU64::new(u64::MAX).unwrap())] + /// Stop after rerunning forward and backward pass this many times without finding a smaller representation of the block. + /// Default value: practically infinite (maximum u64 value) + pub zopfli_iterations_without_improvement: std::num::NonZeroU64, + #[cfg(feature = "zopfli")] + #[default(15)] + /// Maximum amount of blocks to split into (0 for unlimited, but this can give extreme results that hurt compression on some files). + /// Default value: 15. + pub zopfli_maximum_block_splits: u16, } #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)]