From 5b75b23060978b96028e823b7e28001f2d763896 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Tue, 5 Aug 2025 23:39:21 +0800 Subject: [PATCH] Add jpeg support --- Cargo.lock | 62 +++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 9 ++++++- check_features.py | 10 +++++++- src/args.rs | 13 ++++++++++ src/main.rs | 2 ++ src/types.rs | 6 +++++ src/utils/img.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 161 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e189e85..98ae999 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,6 +79,12 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "autocfg" version = "1.5.0" @@ -130,6 +136,12 @@ dependencies = [ "cipher", ] +[[package]] +name = "bytemuck" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" + [[package]] name = "byteorder" version = "1.5.0" @@ -304,6 +316,12 @@ dependencies = [ "syn", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -672,6 +690,31 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mozjpeg" +version = "0.10.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7891b80aaa86097d38d276eb98b3805d6280708c4e0a1e6f6aed9380c51fec9" +dependencies = [ + "arrayvec", + "bytemuck", + "libc", + "mozjpeg-sys", + "rgb", +] + +[[package]] +name = "mozjpeg-sys" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f0dc668bf9bf888c88e2fb1ab16a406d2c380f1d082b20d51dd540ab2aa70c1" +dependencies = [ + "cc", + "dunce", + "libc", + "nasm-rs", +] + [[package]] name = "msg_tool" version = "0.1.0" @@ -691,6 +734,7 @@ dependencies = [ "lazy_static", "libtlg-rs", "memchr", + "mozjpeg", "msg_tool_macro", "overf", "png", @@ -713,6 +757,15 @@ dependencies = [ "syn", ] +[[package]] +name = "nasm-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12fcfa1bd49e0342ec1d07ed2be83b59963e7acbeb9310e1bb2c07b69dadd959" +dependencies = [ + "jobserver", +] + [[package]] name = "nix" version = "0.30.1" @@ -876,6 +929,15 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +dependencies = [ + "bytemuck", +] + [[package]] name = "ryu" version = "1.0.20" diff --git a/Cargo.toml b/Cargo.toml index 9cc2ec2..89dc347 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ json = { version = "0.12", optional = true } lazy_static = "1.5.0" libtlg-rs = { version = "0.1", optional = true } memchr = { version = "2.7", optional = true } +mozjpeg = { version = "0.10.13", optional = true } msg_tool_macro = { path = "./msg_tool_macro" } overf = "0.1" png = { version = "0.17", optional = true } @@ -32,7 +33,12 @@ 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", "circus-arc", "circus-audio", "circus-img", "escude", "escude-arc", "ex-hibit", "hexen-haus", "kirikiri", "kirikiri-img", "will-plus", "yaneurao", "yaneurao-itufuru"] +default = ["all-fmt", "image-jpg"] +all-fmt = ["all-script", "all-img", "all-arc", "all-audio"] +all-script = ["artemis", "bgi", "cat-system", "circus", "escude", "ex-hibit", "hexen-haus", "kirikiri", "will-plus", "yaneurao", "yaneurao-itufuru"] +all-img = ["bgi-img", "cat-system-img", "circus-img", "kirikiri-img"] +all-arc = ["artemis-arc", "bgi-arc", "cat-system-arc", "circus-arc", "escude-arc"] +all-audio = ["circus-audio"] artemis = ["utils-escape"] artemis-arc = ["artemis", "msg_tool_macro/artemis-arc", "sha1"] bgi = [] @@ -56,6 +62,7 @@ yaneurao = [] yaneurao-itufuru = ["yaneurao"] # basic feature image = ["png"] +image-jpg = ["mozjpeg"] # utils feature utils-bit-stream = [] utils-crc32 = [] diff --git a/check_features.py b/check_features.py index d7d6cf0..371d0eb 100644 --- a/check_features.py +++ b/check_features.py @@ -2,6 +2,14 @@ import toml import subprocess import sys +def filter_name(name): + if name.startswith("utils-"): + return False + if name.startswith("all-"): + return False + return True + + def main(): # 检查cargo是否可用 try: @@ -31,7 +39,7 @@ def main(): features = cargo_toml.get("features", {}) feature_names = list(features.keys()) - feature_names = [name for name in feature_names if not name.startswith("utils-")] + feature_names = [name for name in feature_names if filter_name(name)] if not feature_names: print("No features defined in Cargo.toml.") diff --git a/src/args.rs b/src/args.rs index bfa0905..72a2b07 100644 --- a/src/args.rs +++ b/src/args.rs @@ -16,6 +16,15 @@ fn parse_compression_level(level: &str) -> Result { clap_num::number_range(level, 0, 9) } +#[cfg(feature = "mozjpeg")] +fn parse_jpeg_quality(quality: &str) -> Result { + let lower = quality.to_ascii_lowercase(); + if lower == "best" { + return Ok(100); + } + clap_num::number_range(quality, 0, 100) +} + #[cfg(feature = "zstd")] fn parse_zstd_compression_level(level: &str) -> Result { let lower = level.to_ascii_lowercase(); @@ -289,6 +298,10 @@ pub struct Arg { #[arg(long, global = true, value_name = "PATH")] /// Path to the ExHibit rld def keys file, which contains the keys in BINARY format. pub ex_hibit_rld_def_keys: Option, + #[cfg(feature = "mozjpeg")] + #[arg(long, global = true, default_value_t = 80, value_parser = parse_jpeg_quality)] + /// JPEG quality for output images, 0-100. 100 means best quality. + pub jpeg_quality: u8, #[command(subcommand)] /// Command pub command: Command, diff --git a/src/main.rs b/src/main.rs index a3564e5..fd282e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1631,6 +1631,8 @@ fn main() { arg.ex_hibit_rld_def_keys.as_ref(), ) .expect("Failed to load RLD DEF keys"), + #[cfg(feature = "mozjpeg")] + jpeg_quality: arg.jpeg_quality, }; match &arg.command { args::Command::Export { input, output } => { diff --git a/src/types.rs b/src/types.rs index a340cdc..ef2d63f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -255,6 +255,8 @@ pub struct ExtraConfig { pub ex_hibit_rld_keys: Option>, #[cfg(feature = "ex-hibit")] pub ex_hibit_rld_def_keys: Option>, + #[cfg(feature = "mozjpeg")] + pub jpeg_quality: u8, } #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)] @@ -465,6 +467,8 @@ impl ImageColorType { #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)] pub enum ImageOutputType { Png, + #[cfg(feature = "image-jpg")] + Jpg, } #[cfg(feature = "image")] @@ -472,6 +476,8 @@ impl AsRef for ImageOutputType { fn as_ref(&self) -> &str { match self { ImageOutputType::Png => "png", + #[cfg(feature = "image-jpg")] + ImageOutputType::Jpg => "jpg", } } } diff --git a/src/utils/img.rs b/src/utils/img.rs index 645b5b6..e878928 100644 --- a/src/utils/img.rs +++ b/src/utils/img.rs @@ -189,6 +189,36 @@ pub fn encode_img( writer.finish()?; Ok(()) } + #[cfg(feature = "image-jpg")] + ImageOutputType::Jpg => { + let file = crate::utils::files::write_file(filename)?; + let color_type = match data.color_type { + ImageColorType::Grayscale => mozjpeg::ColorSpace::JCS_GRAYSCALE, + ImageColorType::Rgb => mozjpeg::ColorSpace::JCS_RGB, + ImageColorType::Rgba => mozjpeg::ColorSpace::JCS_EXT_RGBA, + ImageColorType::Bgr => { + convert_bgr_to_rgb(&mut data)?; + mozjpeg::ColorSpace::JCS_RGB + } + ImageColorType::Bgra => { + convert_bgra_to_rgba(&mut data)?; + mozjpeg::ColorSpace::JCS_EXT_RGBA + } + }; + if data.depth != 8 { + return Err(anyhow::anyhow!( + "JPEG encoding only supports 8-bit depth, found: {}", + data.depth + )); + } + let mut encoder = mozjpeg::compress::Compress::new(color_type); + encoder.set_size(data.width as usize, data.height as usize); + encoder.set_quality(config.jpeg_quality as f32); + let mut start = encoder.start_compress(file)?; + start.write_scanlines(&data.data)?; + start.finish()?; + Ok(()) + } } } @@ -231,6 +261,37 @@ pub fn decode_img(typ: ImageOutputType, filename: &str) -> Result { let file = crate::utils::files::read_file(filename)?; load_png(&file[..]) } + #[cfg(feature = "image-jpg")] + ImageOutputType::Jpg => { + let file = crate::utils::files::read_file(filename)?; + let decoder = mozjpeg::decompress::Decompress::new_mem(&file)?; + let color_type = match decoder.color_space() { + mozjpeg::ColorSpace::JCS_GRAYSCALE => ImageColorType::Grayscale, + mozjpeg::ColorSpace::JCS_RGB => ImageColorType::Rgb, + mozjpeg::ColorSpace::JCS_EXT_RGBA => ImageColorType::Rgba, + _ => ImageColorType::Rgb, // Convert other types to RGB + }; + let width = decoder.width() as u32; + let height = decoder.height() as u32; + let stride = width as usize * color_type.bpp(8) as usize / 8; + let mut data = vec![0; stride * height as usize]; + let mut re = match color_type { + ImageColorType::Grayscale => decoder.grayscale()?, + ImageColorType::Rgb => decoder.rgb()?, + ImageColorType::Rgba => decoder.rgba()?, + _ => { + unreachable!(); // We already checked the color type above + } + }; + re.read_scanlines_into(&mut data)?; + Ok(ImageData { + width, + height, + depth: 8, + color_type, + data, + }) + } } }