mirror of
https://github.com/lifegpc/msg-tool.git
synced 2026-06-18 17:04:50 +08:00
Add JXL image support
This commit is contained in:
20
Cargo.lock
generated
20
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
223
src/utils/jxl.rs
Normal 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)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user