From 1b5e9aa31783d38826a624da8b74cde624790e89 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 10 Oct 2024 21:13:10 +0200 Subject: [PATCH] feat(wallpaper): support JPEG XL wallpapers --- Cargo.lock | 173 ++++++++++++++++++++++++++++++++++++ pages/wallpapers/Cargo.toml | 3 + pages/wallpapers/src/lib.rs | 126 ++++++++++++++++++++++++-- 3 files changed, 294 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a69f279..d7cba3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1699,11 +1699,14 @@ dependencies = [ "cosmic-config", "cosmic-randr-shell", "dirs", + "eyre", + "fast_image_resize", "freedesktop-icons", "futures-lite 2.3.0", "futures-util", "image 0.25.2", "infer", + "jxl-oxide", "tokio", "tracing", ] @@ -2096,6 +2099,15 @@ dependencies = [ "smithay-clipboard", ] +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", +] + [[package]] name = "downcast-rs" version = "1.2.1" @@ -2296,6 +2308,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" +[[package]] +name = "fast_image_resize" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66a61fbfc84ef99a839499cf9e5a7c2951d2da874ea00f29ee938bc50d1b396" +dependencies = [ + "bytemuck", + "cfg-if", + "document-features", + "image 0.25.2", + "num-traits", + "thiserror", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -4018,6 +4044,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 = "kamadak-exif" version = "0.5.5" @@ -4307,6 +4474,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + [[package]] name = "locale_config" version = "0.3.0" diff --git a/pages/wallpapers/Cargo.toml b/pages/wallpapers/Cargo.toml index 50e8982..75bbe3e 100644 --- a/pages/wallpapers/Cargo.toml +++ b/pages/wallpapers/Cargo.toml @@ -10,10 +10,13 @@ cosmic-bg-config = { workspace = true } cosmic-config = { workspace = true } cosmic-randr-shell = { workspace = true } dirs = "5.0.1" +eyre = "0.6.12" +fast_image_resize = { version = "5.0.0", features = ["image"] } freedesktop-icons = "0.2.6" futures-lite = "2.3.0" futures-util = "0.3.30" image = "0.25.2" infer = "0.16.0" +jxl-oxide = "0.9.0" tokio = { version = "1.40.0", features = ["sync"] } tracing = "0.1.40" diff --git a/pages/wallpapers/src/lib.rs b/pages/wallpapers/src/lib.rs index 6dce108..77168b3 100644 --- a/pages/wallpapers/src/lib.rs +++ b/pages/wallpapers/src/lib.rs @@ -1,7 +1,10 @@ pub use cosmic_bg_config::{Color, Config, Entry, Gradient, ScalingMode, Source}; - +use eyre::{eyre, OptionExt}; +use fast_image_resize::SrcCropping; use futures_lite::Stream; +use image::imageops::FilterType; use image::{DynamicImage, ImageBuffer, Rgba, RgbaImage}; +use jxl_oxide::{EnumColourEncoding, JxlImage, PixelFormat}; use std::{ borrow::Cow, collections::{hash_map::DefaultHasher, BTreeSet, HashMap}, @@ -115,16 +118,22 @@ pub async fn load_each_from_path( if recurse && file_type.is_dir() { paths.push(path); } else if file_type.is_file() { - let Ok(Some(kind)) = infer::get_from_path(&path) else { + let path = if path.extension().map_or(false, |ext| ext == "jxl") { + path + } else if let Ok(Some(kind)) = infer::get_from_path(&path) { + if infer::MatcherType::Image == kind.matcher_type() { + path + } else { + continue; + } + } else { continue; }; - if infer::MatcherType::Image == kind.matcher_type() { - wallpapers.insert(path); + wallpapers.insert(path); - if wallpapers.len() > 99 { - break; - } + if wallpapers.len() > 99 { + break; } } } @@ -165,7 +174,7 @@ pub fn load_image_with_thumbnail( ImageOperation::Cached(thumbnail) => thumbnail.to_rgba8(), ImageOperation::GenerateThumbnail { path, image } => { - let image = image.thumbnail(300, 169).to_rgba8(); + let image = resize_thumbnail(&image, 300, 169).to_rgba8(); if let Some(path) = path { // Save thumbnail to disk without blocking. @@ -251,6 +260,16 @@ fn load_thumbnail( } fn open_image(input_buffer: &mut Vec, path: &Path) -> Option { + if path.extension().map_or(false, |ext| ext == "jxl") { + return match decode_jpegxl(path) { + Ok(image) => Some(image), + Err(why) => { + tracing::error!(?path, ?why, "image decode failed"); + None + } + }; + } + let capacity = match path.metadata() { Ok(metadata) => metadata.len() as usize, Err(why) => { @@ -409,3 +428,94 @@ fn border_radius( } } } + +/// Decodes JPEG XL image files into `image::DynamicImage` via `jxl-oxide`. +pub 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 => image::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 => image::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 => image::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 => image::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")), + } +} + +/// Use `fast-image-resize` crate for faster thumbnail generation. +fn resize_thumbnail( + img: &image::DynamicImage, + new_width: u32, + new_height: u32, +) -> image::DynamicImage { + let mut resizer = fast_image_resize::Resizer::new(); + let options = fast_image_resize::ResizeOptions { + algorithm: fast_image_resize::ResizeAlg::Convolution( + fast_image_resize::FilterType::Lanczos3, + ), + cropping: SrcCropping::FitIntoDestination((new_width as f64, new_height as f64)), + ..Default::default() + }; + let mut new_image = image::DynamicImage::new(new_width, new_height, img.color()); + if let Err(err) = resizer.resize(img, &mut new_image, &options) { + tracing::warn!(?err, "Failed to use `fast_image_resize`. Falling back."); + new_image = + image::imageops::resize(img, new_width, new_height, FilterType::Lanczos3).into(); + } + new_image +}