Add jpeg support

This commit is contained in:
2025-08-05 23:39:21 +08:00
parent d9cf2f9202
commit 5b75b23060
7 changed files with 161 additions and 2 deletions

62
Cargo.lock generated
View File

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

View File

@@ -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 = []

View File

@@ -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.")

View File

@@ -16,6 +16,15 @@ fn parse_compression_level(level: &str) -> Result<u32, String> {
clap_num::number_range(level, 0, 9)
}
#[cfg(feature = "mozjpeg")]
fn parse_jpeg_quality(quality: &str) -> Result<u8, String> {
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<i32, String> {
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<String>,
#[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,

View File

@@ -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 } => {

View File

@@ -255,6 +255,8 @@ pub struct ExtraConfig {
pub ex_hibit_rld_keys: Option<Box<[u32; 0x100]>>,
#[cfg(feature = "ex-hibit")]
pub ex_hibit_rld_def_keys: Option<Box<[u32; 0x100]>>,
#[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<str> for ImageOutputType {
fn as_ref(&self) -> &str {
match self {
ImageOutputType::Png => "png",
#[cfg(feature = "image-jpg")]
ImageOutputType::Jpg => "jpg",
}
}
}

View File

@@ -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<ImageData> {
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,
})
}
}
}