feat(wallpaper): support JPEG XL wallpapers

This commit is contained in:
Michael Aaron Murphy 2024-10-10 21:13:10 +02:00 committed by Michael Murphy
parent 6a753f5c12
commit 1b5e9aa317
3 changed files with 294 additions and 8 deletions

173
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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<u8>, path: &Path) -> Option<DynamicImage> {
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<DynamicImage> {
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::<Vec<_>>(),
)
.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::<Vec<_>>(),
)
.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::<Vec<_>>(),
)
.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::<Vec<_>>(),
)
.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
}