Add JXL image support

This commit is contained in:
2025-09-13 23:50:04 +08:00
parent 4391ad6de5
commit 2e7cd8119e
6 changed files with 269 additions and 1 deletions

20
Cargo.lock generated
View File

@@ -1145,6 +1145,25 @@ dependencies = [
"nasm-rs",
]
[[package]]
name = "msg-tool-jpegxl-src"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5b6b09d7b013a95614b62ef2bf668b268853d9a3d90b93ada715f2007c815b8"
dependencies = [
"cmake",
]
[[package]]
name = "msg-tool-jpegxl-sys"
version = "0.11.2+libjxl-0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb3d4c72bd5bb5a7d6d9e902365bae80088479c05e651947bc285cdfd384f201"
dependencies = [
"msg-tool-jpegxl-src",
"pkg-config",
]
[[package]]
name = "msg_tool"
version = "0.2.3"
@@ -1168,6 +1187,7 @@ dependencies = [
"markup5ever_rcdom",
"memchr",
"mozjpeg",
"msg-tool-jpegxl-sys",
"msg_tool_macro",
"num_cpus",
"overf",

View File

@@ -20,6 +20,7 @@ fancy-regex = { version = "0.16", optional = true }
flate2 = { version = "1.1", optional = true }
int-enum = { version = "1.2", optional = true }
json = { version = "0.12", optional = true }
jpegxl-sys = { package = "msg-tool-jpegxl-sys", version = "0.11", optional = true, features = ["vendored"] }
lazy_static = "1.5.0"
libflac-sys = { version = "0.3", optional = true }
libtlg-rs = { version = "0.2", optional = true, features = ["encode"] }
@@ -47,7 +48,7 @@ xml5ever = { version = "0.35", optional = true }
zstd = { version = "0.13", optional = true }
[features]
default = ["all-fmt", "image-jpg", "image-webp", "audio-flac"]
default = ["all-fmt", "image-jpg", "image-jxl", "image-webp", "audio-flac"]
all-fmt = ["all-script", "all-img", "all-arc", "all-audio"]
all-script = ["artemis", "artemis-panmimisoft", "bgi", "cat-system", "circus", "entis-gls", "escude", "ex-hibit", "favorite", "hexen-haus", "kirikiri", "silky", "softpal", "will-plus", "yaneurao", "yaneurao-itufuru"]
all-img = ["bgi-img", "cat-system-img", "circus-img", "emote-img", "kirikiri-img"]
@@ -84,6 +85,7 @@ yaneurao-itufuru = ["yaneurao"]
# basic feature
image = ["png"]
image-jpg = ["mozjpeg"]
image-jxl = ["jpegxl-sys"]
image-webp = ["webp"]
lossless-audio = ["utils-pcm"]
audio-flac = ["libflac-sys", "utils-pcm"]

View File

@@ -711,6 +711,9 @@ pub enum ImageOutputType {
#[cfg(feature = "image-webp")]
/// WebP image
Webp,
#[cfg(feature = "image-jxl")]
/// JPEG XL image
Jxl,
}
#[cfg(feature = "image")]
@@ -728,6 +731,8 @@ impl TryFrom<&str> for ImageOutputType {
"jpeg" => Ok(ImageOutputType::Jpg),
#[cfg(feature = "image-webp")]
"webp" => Ok(ImageOutputType::Webp),
#[cfg(feature = "image-jxl")]
"jxl" => Ok(ImageOutputType::Jxl),
_ => Err(anyhow::anyhow!("Unsupported image output type: {}", value)),
}
}
@@ -756,6 +761,8 @@ impl AsRef<str> for ImageOutputType {
ImageOutputType::Jpg => "jpg",
#[cfg(feature = "image-webp")]
ImageOutputType::Webp => "webp",
#[cfg(feature = "image-jxl")]
ImageOutputType::Jxl => "jxl",
}
}
}

View File

@@ -1,4 +1,6 @@
//! Image Utilities
#[cfg(feature = "image-jxl")]
use super::jxl::*;
use crate::ext::io::*;
use crate::types::*;
use anyhow::Result;
@@ -285,6 +287,13 @@ pub fn encode_img(
file.write_all(&re)?;
Ok(())
}
#[cfg(feature = "image-jxl")]
ImageOutputType::Jxl => {
let mut file = crate::utils::files::write_file(filename)?;
let data = encode_jxl(data, config)?;
file.write_all(&data)?;
Ok(())
}
}
}
@@ -401,6 +410,11 @@ pub fn decode_img(typ: ImageOutputType, filename: &str) -> Result<ImageData> {
data,
})
}
#[cfg(feature = "image-jxl")]
ImageOutputType::Jxl => {
let file = crate::utils::files::read_file(filename)?;
decode_jxl(&file[..])
}
}
}

223
src/utils/jxl.rs Normal file
View File

@@ -0,0 +1,223 @@
//! JPEG XL image support
use crate::types::*;
use anyhow::Result;
use jpegxl_sys::common::types::*;
use jpegxl_sys::decode::*;
use jpegxl_sys::encoder::encode::*;
use jpegxl_sys::metadata::codestream_header::*;
use std::io::Read;
struct JxlDecoderHandle {
handle: *mut JxlDecoder,
}
impl Drop for JxlDecoderHandle {
fn drop(&mut self) {
unsafe {
JxlDecoderDestroy(self.handle);
}
}
}
struct JxlEncoderHandle {
handle: *mut JxlEncoder,
}
impl Drop for JxlEncoderHandle {
fn drop(&mut self) {
unsafe {
JxlEncoderDestroy(self.handle);
}
}
}
fn check_decoder_status(status: JxlDecoderStatus) -> Result<()> {
match status {
JxlDecoderStatus::Success => Ok(()),
_ => Err(anyhow::anyhow!("JXL decoder error: {:?}", status)),
}
}
fn check_encoder_status(status: JxlEncoderStatus) -> Result<()> {
match status {
JxlEncoderStatus::Success => Ok(()),
_ => Err(anyhow::anyhow!("JXL encoder error: {:?}", status)),
}
}
fn default_basic_info() -> JxlBasicInfo {
let basic_info = std::mem::MaybeUninit::<JxlBasicInfo>::zeroed();
unsafe { basic_info.assume_init_read() }
}
/// Decode JXL image from reader
pub fn decode_jxl<R: Read>(mut r: R) -> Result<ImageData> {
let decoder = unsafe { JxlDecoderCreate(std::ptr::null()) };
if decoder.is_null() {
return Err(anyhow::anyhow!("Failed to create JXL decoder"));
}
let dh = JxlDecoderHandle { handle: decoder };
let events = JxlDecoderStatus::BasicInfo as i32
| JxlDecoderStatus::FullImage as i32
| JxlDecoderStatus::ColorEncoding as i32;
check_decoder_status(unsafe { JxlDecoderSubscribeEvents(dh.handle, events) })?;
let mut data = Vec::new();
r.read_to_end(&mut data)?;
check_decoder_status(unsafe { JxlDecoderSetInput(dh.handle, data.as_ptr(), data.len()) })?;
unsafe {
JxlDecoderCloseInput(dh.handle);
};
let mut basic_info = default_basic_info();
let mut color_type = ImageColorType::Rgb;
let mut buffer = Vec::new();
loop {
let status = unsafe { JxlDecoderProcessInput(dh.handle) };
match status {
JxlDecoderStatus::BasicInfo => {
check_decoder_status(unsafe {
JxlDecoderGetBasicInfo(dh.handle, &mut basic_info)
})?;
match basic_info.num_color_channels {
1 => color_type = ImageColorType::Grayscale,
3 => {
if basic_info.alpha_bits > 0 {
color_type = ImageColorType::Rgba;
} else {
color_type = ImageColorType::Rgb;
}
}
_ => {
return Err(anyhow::anyhow!(
"Unsupported number of color channels: {}",
basic_info.num_color_channels
));
}
}
if !matches!(basic_info.bits_per_sample, 8 | 16) {
return Err(anyhow::anyhow!(
"Unsupported bits per sample: {}",
basic_info.bits_per_sample
));
}
}
JxlDecoderStatus::NeedImageOutBuffer => {
let format = JxlPixelFormat {
num_channels: color_type.bpp(1) as u32,
data_type: if basic_info.bits_per_sample <= 8 {
JxlDataType::Uint8
} else {
JxlDataType::Uint16
},
endianness: JxlEndianness::Little,
align: 0,
};
let mut buffer_size: usize = 0;
check_decoder_status(unsafe {
JxlDecoderImageOutBufferSize(dh.handle, &format, &mut buffer_size)
})?;
buffer.resize(buffer_size, 0);
check_decoder_status(unsafe {
JxlDecoderSetImageOutBuffer(
dh.handle,
&format,
buffer.as_mut_ptr() as *mut _,
buffer_size,
)
})?;
}
JxlDecoderStatus::Success => {
break;
}
JxlDecoderStatus::Error => {
return Err(anyhow::anyhow!("JXL decoding error"));
}
_ => {}
}
}
Ok(ImageData {
width: basic_info.xsize,
height: basic_info.ysize,
color_type,
depth: basic_info.bits_per_sample as u8,
data: buffer,
})
}
/// Encode image data to JXL format
pub fn encode_jxl(img: ImageData, _config: &ExtraConfig) -> Result<Vec<u8>> {
let encoder = unsafe { JxlEncoderCreate(std::ptr::null()) };
if encoder.is_null() {
return Err(anyhow::anyhow!("Failed to create JXL encoder"));
}
let eh = JxlEncoderHandle { handle: encoder };
let mut basic_info = default_basic_info();
basic_info.xsize = img.width;
basic_info.ysize = img.height;
basic_info.bits_per_sample = match img.depth {
8 => 8,
16 => 16,
_ => {
return Err(anyhow::anyhow!(
"Unsupported bits per sample: {}",
img.depth
));
}
};
basic_info.alpha_bits = match img.color_type {
ImageColorType::Rgba | ImageColorType::Bgra => img.depth as u32,
_ => 0,
};
basic_info.num_color_channels = match img.color_type {
ImageColorType::Bgr | ImageColorType::Rgb | ImageColorType::Bgra | ImageColorType::Rgba => {
3
}
ImageColorType::Grayscale => 1,
};
basic_info.num_extra_channels = if basic_info.alpha_bits > 0 { 1 } else { 0 };
basic_info.orientation = JxlOrientation::Identity;
basic_info.uses_original_profile = JxlBool::True;
check_encoder_status(unsafe { JxlEncoderSetBasicInfo(eh.handle, &basic_info) })?;
let options = unsafe { JxlEncoderFrameSettingsCreate(eh.handle, std::ptr::null()) };
if options.is_null() {
return Err(anyhow::anyhow!(
"Failed to create JXL encoder frame settings"
));
}
check_encoder_status(unsafe { JxlEncoderSetFrameLossless(options, JxlBool::True) })?;
let format = JxlPixelFormat {
num_channels: img.color_type.bpp(1) as u32,
data_type: if img.depth <= 8 {
JxlDataType::Uint8
} else {
JxlDataType::Uint16
},
endianness: JxlEndianness::Little,
align: 0,
};
check_encoder_status(unsafe {
JxlEncoderAddImageFrame(
options,
&format,
img.data.as_ptr() as *const _,
img.data.len(),
)
})?;
unsafe { JxlEncoderCloseInput(eh.handle) };
let mut compressed_data = Vec::new();
let mut buffer = [0u8; 4096];
loop {
let mut avail_out = buffer.len();
let mut next_out = buffer.as_mut_ptr();
let status = unsafe { JxlEncoderProcessOutput(eh.handle, &mut next_out, &mut avail_out) };
let used = buffer.len() - avail_out;
compressed_data.extend_from_slice(&buffer[..used]);
match status {
JxlEncoderStatus::Success => break,
JxlEncoderStatus::NeedMoreOutput => {}
_ => {
return Err(anyhow::anyhow!("JXL encoding error: {:?}", status));
}
}
}
Ok(compressed_data)
}

View File

@@ -16,6 +16,8 @@ pub mod files;
pub mod flac;
#[cfg(feature = "image")]
pub mod img;
#[cfg(feature = "image-jxl")]
pub mod jxl;
#[cfg(feature = "lossless-audio")]
pub mod lossless_audio;
mod macros;