diff --git a/CMakeLists.txt b/CMakeLists.txt index e19c8a7..cfd0150 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,6 +60,9 @@ pkg_check_modules( pangocairo libjpeg libwebp + libjxl + libjxl_cms + libjxl_threads hyprlang>=0.2.0 hyprutils>=0.2.0) diff --git a/README.md b/README.md index 207b29d..f026787 100644 --- a/README.md +++ b/README.md @@ -29,18 +29,19 @@ The development files of these packages need to be installed on the system for ` - libglvnd-core - libjpeg-turbo - libwebp +- libjxl - hyprlang - hyprutils - hyprwayland-scanner To install all of these in Fedora, run this command: ``` -sudo dnf install wayland-devel wayland-protocols-devel hyprlang-devel pango-devel cairo-devel file-devel libglvnd-devel libglvnd-core-devel libjpeg-turbo-devel libwebp-devel gcc-c++ hyprutils-devel hyprwayland-scanner +sudo dnf install wayland-devel wayland-protocols-devel hyprlang-devel pango-devel cairo-devel file-devel libglvnd-devel libglvnd-core-devel libjpeg-turbo-devel libwebp-devel libjxl-devel gcc-c++ hyprutils-devel hyprwayland-scanner ``` On Arch: ``` -sudo pacman -S ninja gcc wayland-protocols libjpeg-turbo libwebp pango cairo pkgconf cmake libglvnd wayland hyprutils hyprwayland-scanner hyprlang +sudo pacman -S ninja gcc wayland-protocols libjpeg-turbo libwebp libjxl pango cairo pkgconf cmake libglvnd wayland hyprutils hyprwayland-scanner hyprlang ``` On OpenSUSE: @@ -89,7 +90,7 @@ splash = true ``` -Preload will tell Hyprland to load a particular image (supported formats: png, jpg, jpeg, webp). Wallpaper will apply the wallpaper to the selected output (`monitor` is the monitor's name, easily can be retrieved with `hyprctl monitors`. You can leave it empty to set all monitors without an active wallpaper. You can also use `desc:` followed by the monitor's description without the (PORT) at the end) +Preload will tell Hyprland to load a particular image (supported formats: png, jpg, jpeg, jpeg xl, webp). Wallpaper will apply the wallpaper to the selected output (`monitor` is the monitor's name, easily can be retrieved with `hyprctl monitors`. You can leave it empty to set all monitors without an active wallpaper. You can also use `desc:` followed by the monitor's description without the (PORT) at the end) You may add `contain:` or `tile:` before the file path in `wallpaper=` to set the mode to either contain or tile, respectively, instead of cover: diff --git a/nix/default.nix b/nix/default.nix index aa8e479..514a50f 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -13,6 +13,7 @@ libdatrie, libGL, libjpeg, + libjxl, libselinux, libsepol, libthai, @@ -66,6 +67,7 @@ stdenv.mkDerivation { libdatrie libGL libjpeg + libjxl libselinux libsepol libthai diff --git a/src/helpers/JpegXL.cpp b/src/helpers/JpegXL.cpp new file mode 100644 index 0000000..1174fc5 --- /dev/null +++ b/src/helpers/JpegXL.cpp @@ -0,0 +1,107 @@ +#include "JpegXL.hpp" + +#include +#include +#include +#include + +cairo_surface_t* JXL::createSurfaceFromJXL(const std::string& path) { + + if (!std::filesystem::exists(path)) { + Debug::log(ERR, "createSurfaceFromJXL: file doesn't exist??"); + exit(1); + } + + std::ifstream file(path, std::ios::binary | std::ios::ate); + file.exceptions(std::ifstream::failbit | std::ifstream::badbit | std::ifstream::eofbit); + std::vector bytes(file.tellg()); + file.seekg(0); + file.read(reinterpret_cast(bytes.data()), bytes.size()); + + JxlSignature signature = JxlSignatureCheck(bytes.data(), bytes.size()); + if (signature != JXL_SIG_CODESTREAM && signature != JXL_SIG_CONTAINER) { + Debug::log(ERR, "createSurfaceFromJXL: file is not JXL format"); + exit(1); + } + + auto dec = JxlDecoderMake(nullptr); + auto runner = JxlResizableParallelRunnerMake(nullptr); + if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec.get(), JxlResizableParallelRunner, runner.get())) { + Debug::log(ERR, "createSurfaceFromJXL: JxlDecoderSetParallelRunner failed"); + exit(1); + } + + if (JXL_DEC_SUCCESS != JxlDecoderSubscribeEvents(dec.get(), JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE)) { + Debug::log(ERR, "createSurfaceFromJXL: JxlDecoderSubscribeEvents failed"); + exit(1); + } + + JxlDecoderSetInput(dec.get(), bytes.data(), bytes.size()); + JxlDecoderCloseInput(dec.get()); + if (JXL_DEC_BASIC_INFO != JxlDecoderProcessInput(dec.get())) { + Debug::log(ERR, "createSurfaceFromJXL: JxlDecoderProcessInput failed"); + exit(1); + } + + JxlBasicInfo basicInfo; + if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec.get(), &basicInfo)) { + Debug::log(ERR, "createSurfaceFromJXL: JxlDecoderGetBasicInfo failed"); + exit(1); + } + + auto cairoSurface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, basicInfo.xsize, basicInfo.ysize); + if (cairo_surface_status(cairoSurface) != CAIRO_STATUS_SUCCESS) { + Debug::log(ERR, "createSurfaceFromJXL: Cairo Failed (?)"); + cairo_surface_destroy(cairoSurface); + exit(1); + } + + const auto CAIRODATA = cairo_image_surface_get_data(cairoSurface); + + JxlPixelFormat format = { + .num_channels = 4, + .data_type = JXL_TYPE_UINT8, + .endianness = JXL_LITTLE_ENDIAN, + .align = cairo_image_surface_get_stride(cairoSurface), + }; + + const auto OUTPUTSIZE = basicInfo.xsize * basicInfo.ysize * format.num_channels; + + for (;;) { + JxlDecoderStatus status = JxlDecoderProcessInput(dec.get()); + if (status == JXL_DEC_ERROR) { + Debug::log(ERR, "createSurfaceFromJXL: JxlDecoderProcessInput failed"); + cairo_surface_destroy(cairoSurface); + exit(1); + } else if (status == JXL_DEC_NEED_MORE_INPUT) { + Debug::log(ERR, "createSurfaceFromJXL: JxlDecoderProcessInput expected more input"); + cairo_surface_destroy(cairoSurface); + exit(1); + } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + JxlResizableParallelRunnerSetThreads(runner.get(), JxlResizableParallelRunnerSuggestThreads(basicInfo.xsize, basicInfo.ysize)); + size_t bufferSize; + if (JXL_DEC_SUCCESS != JxlDecoderImageOutBufferSize(dec.get(), &format, &bufferSize)) { + Debug::log(ERR, "createSurfaceFromJXL: JxlDecoderImageOutBufferSize failed"); + cairo_surface_destroy(cairoSurface); + exit(1); + } + if (bufferSize != OUTPUTSIZE) { + Debug::log(ERR, "createSurfaceFromJXL: invalid output buffer size"); + cairo_surface_destroy(cairoSurface); + exit(1); + } + if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer(dec.get(), &format, CAIRODATA, bufferSize)) { + Debug::log(ERR, "createSurfaceFromJXL: JxlDecoderSetImageOutBuffer failed"); + cairo_surface_destroy(cairoSurface); + exit(1); + } + } else if (status == JXL_DEC_FULL_IMAGE) { + for (size_t i = 0; i < OUTPUTSIZE - 2; i += format.num_channels) { + std::swap(CAIRODATA[i + 0], CAIRODATA[i + 2]); + } + cairo_surface_mark_dirty(cairoSurface); + cairo_surface_set_mime_data(cairoSurface, "image/jxl", bytes.data(), bytes.size(), nullptr, nullptr); + return cairoSurface; + } + } +} diff --git a/src/helpers/JpegXL.hpp b/src/helpers/JpegXL.hpp new file mode 100644 index 0000000..f4ef60f --- /dev/null +++ b/src/helpers/JpegXL.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include "../defines.hpp" + +namespace JXL { + cairo_surface_t* createSurfaceFromJXL(const std::string&); +}; diff --git a/src/render/WallpaperTarget.cpp b/src/render/WallpaperTarget.cpp index 6c2ab3b..f64fdf0 100644 --- a/src/render/WallpaperTarget.cpp +++ b/src/render/WallpaperTarget.cpp @@ -24,6 +24,8 @@ void CWallpaperTarget::create(const std::string& path) { m_bHasAlpha = false; } else if (path.find(".webp") == len - 5 || path.find(".WEBP") == len - 5) { CAIROSURFACE = WEBP::createSurfaceFromWEBP(path); + } else if (path.find(".jxl") == len - 4 || path.find(".JXL") == len - 4) { + CAIROSURFACE = JXL::createSurfaceFromJXL(path); } else { // magic is slow, so only use it when no recognized extension is found auto handle = magic_open(MAGIC_NONE | MAGIC_COMPRESS); diff --git a/src/render/WallpaperTarget.hpp b/src/render/WallpaperTarget.hpp index 9a28d92..0c79a32 100644 --- a/src/render/WallpaperTarget.hpp +++ b/src/render/WallpaperTarget.hpp @@ -4,6 +4,7 @@ #include "../helpers/Jpeg.hpp" #include "../helpers/Bmp.hpp" #include "../helpers/Webp.hpp" +#include "../helpers/JpegXL.hpp" class CWallpaperTarget { public: