diff --git a/Cargo.lock b/Cargo.lock index ca9d1c5..47b7e55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 3f622d9..5f7d3cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] diff --git a/src/types.rs b/src/types.rs index bd1574f..e1eb1f8 100644 --- a/src/types.rs +++ b/src/types.rs @@ -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 for ImageOutputType { ImageOutputType::Jpg => "jpg", #[cfg(feature = "image-webp")] ImageOutputType::Webp => "webp", + #[cfg(feature = "image-jxl")] + ImageOutputType::Jxl => "jxl", } } } diff --git a/src/utils/img.rs b/src/utils/img.rs index 950aa53..06423db 100644 --- a/src/utils/img.rs +++ b/src/utils/img.rs @@ -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 { data, }) } + #[cfg(feature = "image-jxl")] + ImageOutputType::Jxl => { + let file = crate::utils::files::read_file(filename)?; + decode_jxl(&file[..]) + } } } diff --git a/src/utils/jxl.rs b/src/utils/jxl.rs new file mode 100644 index 0000000..15ea23d --- /dev/null +++ b/src/utils/jxl.rs @@ -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::::zeroed(); + unsafe { basic_info.assume_init_read() } +} + +/// Decode JXL image from reader +pub fn decode_jxl(mut r: R) -> Result { + 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> { + 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) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 809d30d..2b0b9e6 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -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;