From fd44edf79b5ffbced27540fd222e37aa05daeeba Mon Sep 17 00:00:00 2001 From: Michael Murphy Date: Wed, 9 Oct 2024 22:47:25 +0200 Subject: [PATCH] feat: add JPEG XL support via jxl-oxide Co-authored-by: Mitchel Stewart <74831516+Quackdoc@users.noreply.github.com> --- Cargo.lock | 154 +++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + README.md | 2 +- src/wallpaper.rs | 122 +++++++++++++++++++++++++++++++------ 4 files changed, 255 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c821131..cd18709 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -255,6 +255,7 @@ dependencies = [ "eyre", "fast_image_resize", "image", + "jxl-oxide", "notify", "rand", "ron", @@ -794,6 +795,147 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jxl-bitstream" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5855ff16398ffbcf81fee52c41ca65326499c8764b21bb9952c367ace98995fb" +dependencies = [ + "tracing", +] + +[[package]] +name = "jxl-coding" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da5b5093904e940bc11ef50e872c7bdf7b6e88653f012b925f8479daf212b5c9" +dependencies = [ + "jxl-bitstream", + "tracing", +] + +[[package]] +name = "jxl-color" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97f0dd548fabf9c094f9f2304059c86764f606b9040c0bfcfac55f155f423b55" +dependencies = [ + "jxl-bitstream", + "jxl-coding", + "jxl-grid", + "jxl-threadpool", + "tracing", +] + +[[package]] +name = "jxl-frame" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4719f285ebfff5e64f352d0ef149a5244aef4f8e6b5aa666ba6241e90b50632f" +dependencies = [ + "jxl-bitstream", + "jxl-coding", + "jxl-grid", + "jxl-image", + "jxl-modular", + "jxl-threadpool", + "jxl-vardct", + "tracing", +] + +[[package]] +name = "jxl-grid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e941628e8de1dc6ba1d2bba8ebc68a69f8ff50cc7ddce5bc821658d1f4ea6e59" +dependencies = [ + "tracing", +] + +[[package]] +name = "jxl-image" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3824c81613c05c19a9e4329d569145d3f460c0fcadb3965bd8418162d43f7f4" +dependencies = [ + "jxl-bitstream", + "jxl-color", + "jxl-grid", + "tracing", +] + +[[package]] +name = "jxl-modular" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f48a5d003627f380004c29d35e51672da06ae343a2e6fe8d9c84295b9a3e843" +dependencies = [ + "jxl-bitstream", + "jxl-coding", + "jxl-grid", + "jxl-threadpool", + "tracing", +] + +[[package]] +name = "jxl-oxide" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c209f66ef0fe72df77b44ee6aae98eb87bc2dd236d6981e44e143cc37f33f6e" +dependencies = [ + "jxl-bitstream", + "jxl-color", + "jxl-frame", + "jxl-grid", + "jxl-image", + "jxl-render", + "jxl-threadpool", + "tracing", +] + +[[package]] +name = "jxl-render" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aec53c004c9171e89f15ad1f029d6b638cbd70d3a70276746bb8c75f9393bb64" +dependencies = [ + "jxl-bitstream", + "jxl-coding", + "jxl-color", + "jxl-frame", + "jxl-grid", + "jxl-image", + "jxl-modular", + "jxl-threadpool", + "jxl-vardct", + "tracing", +] + +[[package]] +name = "jxl-threadpool" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2860c68899a3c6266044fc26c6a0041e9f27145f58cc69b6eedc1b77f5ee13" +dependencies = [ + "rayon", + "rayon-core", + "tracing", +] + +[[package]] +name = "jxl-vardct" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15da4b49b832b3d8a67329f47e2a1732e0847667938bb9b4a37d99a4668775c2" +dependencies = [ + "jxl-bitstream", + "jxl-coding", + "jxl-grid", + "jxl-modular", + "jxl-threadpool", + "tracing", +] + [[package]] name = "known-folders" version = "1.2.0" @@ -1137,9 +1279,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "png" @@ -1501,18 +1643,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 7ced179..d95cc19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ dirs = "5.0.1" eyre = "0.6.12" fast_image_resize = { version = "4.2.1", features = ["image"] } image = { workspace = true, features = ["hdr", "jpeg", "png", "rayon", "webp"] } +jxl-oxide = "0.9.0" notify = "6.1.1" rand = "0.8" ron = { workspace = true } diff --git a/README.md b/README.md index 2a4725b..04d76f8 100644 --- a/README.md +++ b/README.md @@ -42,4 +42,4 @@ Any contribution intentionally submitted for inclusion in the work by you shall ``` // SPDX-License-Identifier: MPL-2.0 -``` \ No newline at end of file +``` diff --git a/src/wallpaper.rs b/src/wallpaper.rs index 2460981..196481f 100644 --- a/src/wallpaper.rs +++ b/src/wallpaper.rs @@ -11,7 +11,9 @@ use std::{ use cosmic_bg_config::{state::State, Color, Entry, SamplingMethod, ScalingMode, Source}; use cosmic_config::CosmicConfigEntry; -use image::{DynamicImage, ImageReader}; +use eyre::{eyre, OptionExt}; +use image::{DynamicImage, GrayAlphaImage, GrayImage, ImageReader, RgbImage, RgbaImage}; +use jxl_oxide::{EnumColourEncoding, JxlImage, PixelFormat}; use notify::{RecommendedWatcher, RecursiveMode, Watcher}; use rand::{seq::SliceRandom, thread_rng}; use sctk::reexports::{ @@ -123,27 +125,42 @@ impl Wallpaper { tracing::info!("No source for wallpaper"); continue; }; + cur_resized_img = match source { Source::Path(ref path) => { if self.current_image.is_none() { - self.current_image = Some(match ImageReader::open(&path) { - Ok(img) => { - match img - .with_guessed_format() - .ok() - .and_then(|f| f.decode().ok()) - { - Some(img) => img, - None => { - tracing::warn!( - "Could not decode image: {}", - path.display() - ); - continue; + self.current_image = Some(match path.extension() { + Some(ext) if ext == "jxl" => match decode_jpegxl(&path) { + Ok(image) => image, + Err(why) => { + tracing::warn!( + ?why, + "jpegl-xl image decode failed: {}", + path.display() + ); + continue; + } + }, + + _ => match ImageReader::open(&path) { + Ok(img) => { + match img + .with_guessed_format() + .ok() + .and_then(|f| f.decode().ok()) + { + Some(img) => img, + None => { + tracing::warn!( + "could not decode image: {}", + path.display() + ); + continue; + } } } - } - Err(_) => continue, + Err(_) => continue, + }, }); } let img = self.current_image.as_ref().unwrap(); @@ -380,3 +397,74 @@ fn current_image(output: &str) -> Option { wallpaper.map(|(_name, path)| path) } + +/// Decodes JPEG XL image files into `image::DynamicImage` via `jxl-oxide`. +fn decode_jpegxl(path: &std::path::Path) -> eyre::Result { + let mut image = JxlImage::builder() + .open(path) + .map_err(|why| eyre!("failed to read image header: {why}"))?; + + image.request_color_encoding(EnumColourEncoding::srgb( + jxl_oxide::RenderingIntent::Relative, + )); + + let render = image + .render_frame(0) + .map_err(|why| eyre!("failed to render image frame: {why}"))?; + + let framebuffer = render.image_all_channels(); + + match image.pixel_format() { + PixelFormat::Graya => GrayAlphaImage::from_raw( + framebuffer.width() as u32, + framebuffer.height() as u32, + framebuffer + .buf() + .iter() + .map(|x| x * 255. + 0.5) + .map(|x| x as u8) + .collect::>(), + ) + .map(DynamicImage::ImageLumaA8) + .ok_or_eyre("Can't decode gray alpha buffer"), + PixelFormat::Gray => GrayImage::from_raw( + framebuffer.width() as u32, + framebuffer.height() as u32, + framebuffer + .buf() + .iter() + .map(|x| x * 255. + 0.5) + .map(|x| x as u8) + .collect::>(), + ) + .map(DynamicImage::ImageLuma8) + .ok_or_eyre("Can't decode gray buffer"), + PixelFormat::Rgba => RgbaImage::from_raw( + framebuffer.width() as u32, + framebuffer.height() as u32, + framebuffer + .buf() + .iter() + .map(|x| x * 255. + 0.5) + .map(|x| x as u8) + .collect::>(), + ) + .map(DynamicImage::ImageRgba8) + .ok_or_eyre("Can't decode rgba buffer"), + PixelFormat::Rgb => RgbImage::from_raw( + framebuffer.width() as u32, + framebuffer.height() as u32, + framebuffer + .buf() + .iter() + .map(|x| x * 255. + 0.5) + .map(|x| x as u8) + .collect::>(), + ) + .map(DynamicImage::ImageRgb8) + .ok_or_eyre("Can't decode rgb buffer"), + //TODO: handle this + PixelFormat::Cmyk => Err(eyre!("unsupported pixel format: CMYK")), + PixelFormat::Cmyka => Err(eyre!("unsupported pixel format: CMYKA")), + } +}