mirror of
https://github.com/lifegpc/msg-tool.git
synced 2026-06-22 20:04:20 +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",
|
"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]]
|
[[package]]
|
||||||
name = "msg_tool"
|
name = "msg_tool"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
@@ -1168,6 +1187,7 @@ dependencies = [
|
|||||||
"markup5ever_rcdom",
|
"markup5ever_rcdom",
|
||||||
"memchr",
|
"memchr",
|
||||||
"mozjpeg",
|
"mozjpeg",
|
||||||
|
"msg-tool-jpegxl-sys",
|
||||||
"msg_tool_macro",
|
"msg_tool_macro",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"overf",
|
"overf",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ fancy-regex = { version = "0.16", optional = true }
|
|||||||
flate2 = { version = "1.1", optional = true }
|
flate2 = { version = "1.1", optional = true }
|
||||||
int-enum = { version = "1.2", optional = true }
|
int-enum = { version = "1.2", optional = true }
|
||||||
json = { version = "0.12", 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"
|
lazy_static = "1.5.0"
|
||||||
libflac-sys = { version = "0.3", optional = true }
|
libflac-sys = { version = "0.3", optional = true }
|
||||||
libtlg-rs = { version = "0.2", optional = true, features = ["encode"] }
|
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 }
|
zstd = { version = "0.13", optional = true }
|
||||||
|
|
||||||
[features]
|
[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-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-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"]
|
all-img = ["bgi-img", "cat-system-img", "circus-img", "emote-img", "kirikiri-img"]
|
||||||
@@ -84,6 +85,7 @@ yaneurao-itufuru = ["yaneurao"]
|
|||||||
# basic feature
|
# basic feature
|
||||||
image = ["png"]
|
image = ["png"]
|
||||||
image-jpg = ["mozjpeg"]
|
image-jpg = ["mozjpeg"]
|
||||||
|
image-jxl = ["jpegxl-sys"]
|
||||||
image-webp = ["webp"]
|
image-webp = ["webp"]
|
||||||
lossless-audio = ["utils-pcm"]
|
lossless-audio = ["utils-pcm"]
|
||||||
audio-flac = ["libflac-sys", "utils-pcm"]
|
audio-flac = ["libflac-sys", "utils-pcm"]
|
||||||
|
|||||||
@@ -711,6 +711,9 @@ pub enum ImageOutputType {
|
|||||||
#[cfg(feature = "image-webp")]
|
#[cfg(feature = "image-webp")]
|
||||||
/// WebP image
|
/// WebP image
|
||||||
Webp,
|
Webp,
|
||||||
|
#[cfg(feature = "image-jxl")]
|
||||||
|
/// JPEG XL image
|
||||||
|
Jxl,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "image")]
|
#[cfg(feature = "image")]
|
||||||
@@ -728,6 +731,8 @@ impl TryFrom<&str> for ImageOutputType {
|
|||||||
"jpeg" => Ok(ImageOutputType::Jpg),
|
"jpeg" => Ok(ImageOutputType::Jpg),
|
||||||
#[cfg(feature = "image-webp")]
|
#[cfg(feature = "image-webp")]
|
||||||
"webp" => Ok(ImageOutputType::Webp),
|
"webp" => Ok(ImageOutputType::Webp),
|
||||||
|
#[cfg(feature = "image-jxl")]
|
||||||
|
"jxl" => Ok(ImageOutputType::Jxl),
|
||||||
_ => Err(anyhow::anyhow!("Unsupported image output type: {}", value)),
|
_ => Err(anyhow::anyhow!("Unsupported image output type: {}", value)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -756,6 +761,8 @@ impl AsRef<str> for ImageOutputType {
|
|||||||
ImageOutputType::Jpg => "jpg",
|
ImageOutputType::Jpg => "jpg",
|
||||||
#[cfg(feature = "image-webp")]
|
#[cfg(feature = "image-webp")]
|
||||||
ImageOutputType::Webp => "webp",
|
ImageOutputType::Webp => "webp",
|
||||||
|
#[cfg(feature = "image-jxl")]
|
||||||
|
ImageOutputType::Jxl => "jxl",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
//! Image Utilities
|
//! Image Utilities
|
||||||
|
#[cfg(feature = "image-jxl")]
|
||||||
|
use super::jxl::*;
|
||||||
use crate::ext::io::*;
|
use crate::ext::io::*;
|
||||||
use crate::types::*;
|
use crate::types::*;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -285,6 +287,13 @@ pub fn encode_img(
|
|||||||
file.write_all(&re)?;
|
file.write_all(&re)?;
|
||||||
Ok(())
|
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,
|
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;
|
pub mod flac;
|
||||||
#[cfg(feature = "image")]
|
#[cfg(feature = "image")]
|
||||||
pub mod img;
|
pub mod img;
|
||||||
|
#[cfg(feature = "image-jxl")]
|
||||||
|
pub mod jxl;
|
||||||
#[cfg(feature = "lossless-audio")]
|
#[cfg(feature = "lossless-audio")]
|
||||||
pub mod lossless_audio;
|
pub mod lossless_audio;
|
||||||
mod macros;
|
mod macros;
|
||||||
|
|||||||
Reference in New Issue
Block a user