diff --git a/.github/workflows/arch.yml b/.github/workflows/arch.yml index 543967c..ad945d8 100644 --- a/.github/workflows/arch.yml +++ b/.github/workflows/arch.yml @@ -17,7 +17,7 @@ jobs: run: | sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf pacman --noconfirm --noprogressbar -Syyu - pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang libc++ pixman cairo hyprutils libjpeg-turbo libjxl libwebp + pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang libc++ pixman cairo hyprutils libjpeg-turbo libjxl libwebp libspng - name: Build hyprgraphics with gcc run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 072b7b4..492db50 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,7 +47,8 @@ pkg_check_modules( hyprutils libjpeg libwebp - libmagic) + libmagic + spng) pkg_check_modules( JXL diff --git a/README.md b/README.md index 223c12e..a930831 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Dep list: - libjxl_cms [optional] - libjxl_threads [optional] - libmagic + - libspng ## Building diff --git a/src/image/Image.cpp b/src/image/Image.cpp index 82467e9..f87d088 100644 --- a/src/image/Image.cpp +++ b/src/image/Image.cpp @@ -5,6 +5,7 @@ #include "formats/JpegXL.hpp" #endif #include "formats/Webp.hpp" +#include "formats/Png.hpp" #include <magic.h> #include <format> @@ -15,7 +16,7 @@ Hyprgraphics::CImage::CImage(const std::string& path) : filepath(path) { std::expected<cairo_surface_t*, std::string> CAIROSURFACE; const auto len = path.length(); if (path.find(".png") == len - 4 || path.find(".PNG") == len - 4) { - CAIROSURFACE = cairo_image_surface_create_from_png(path.c_str()); + CAIROSURFACE = PNG::createSurfaceFromPNG(path); mime = "image/png"; } else if (path.find(".jpg") == len - 4 || path.find(".JPG") == len - 4 || path.find(".jpeg") == len - 5 || path.find(".JPEG") == len - 5) { CAIROSURFACE = JPEG::createSurfaceFromJPEG(path); @@ -47,7 +48,7 @@ Hyprgraphics::CImage::CImage(const std::string& path) : filepath(path) { const auto first_word = type_str.substr(0, type_str.find(" ")); if (first_word == "PNG") { - CAIROSURFACE = cairo_image_surface_create_from_png(path.c_str()); + CAIROSURFACE = PNG::createSurfaceFromPNG(path); mime = "image/png"; } else if (first_word == "JPEG") { CAIROSURFACE = JPEG::createSurfaceFromJPEG(path); diff --git a/src/image/formats/Png.cpp b/src/image/formats/Png.cpp new file mode 100644 index 0000000..7a01379 --- /dev/null +++ b/src/image/formats/Png.cpp @@ -0,0 +1,77 @@ +#include "Png.hpp" +#include <spng.h> +#include <vector> +#include <fstream> +#include <filesystem> +#include <cstdint> +#include <hyprutils/utils/ScopeGuard.hpp> +using namespace Hyprutils::Utils; + +static std::vector<unsigned char> readBinaryFile(const std::string& filename) { + std::ifstream f(filename, std::ios::binary); + if (!f.good()) + return {}; + f.unsetf(std::ios::skipws); + return {std::istreambuf_iterator<char>(f), std::istreambuf_iterator<char>()}; +} + +std::expected<cairo_surface_t*, std::string> PNG::createSurfaceFromPNG(const std::string& path) { + if (!std::filesystem::exists(path)) + return std::unexpected("loading png: file doesn't exist"); + + spng_ctx* ctx = spng_ctx_new(0); + + CScopeGuard x([&] { spng_ctx_free(ctx); }); + + const auto PNGCONTENT = readBinaryFile(path); + + if (PNGCONTENT.empty()) + return std::unexpected("loading png: file content was empty (bad file?)"); + + spng_set_png_buffer(ctx, PNGCONTENT.data(), PNGCONTENT.size()); + + spng_ihdr ihdr{0}; + if (spng_get_ihdr(ctx, &ihdr)) + return std::unexpected("loading png: file content was empty (bad file?)"); + + int fmt = SPNG_FMT_PNG; + if (ihdr.color_type == SPNG_COLOR_TYPE_INDEXED) + fmt = SPNG_FMT_RGB8; + + size_t imageLength = 0; + if (spng_decoded_image_size(ctx, fmt, &imageLength)) + return std::unexpected("loading png: spng_decoded_image_size failed"); + + uint8_t* imageData = (uint8_t*)malloc(imageLength); + + if (!imageData) + return std::unexpected("loading png: mallocing failed, out of memory?"); + + // TODO: allow proper decode of high bitrate images + if (spng_decode_image(ctx, imageData, imageLength, SPNG_FMT_RGBA8, 0)) { + free(imageData); + return std::unexpected("loading png: spng_decode_image failed (invalid image?)"); + } + + // convert RGBA8888 -> ARGB8888 premult for cairo + for (size_t i = 0; i < imageLength; i += 4) { + uint8_t r, g, b, a; + a = ((*((uint32_t*)(imageData + i))) & 0xFF000000) >> 24; + b = ((*((uint32_t*)(imageData + i))) & 0x00FF0000) >> 16; + g = ((*((uint32_t*)(imageData + i))) & 0x0000FF00) >> 8; + r = (*((uint32_t*)(imageData + i))) & 0x000000FF; + + r *= ((float)a / 255.F); + g *= ((float)a / 255.F); + b *= ((float)a / 255.F); + + *((uint32_t*)(imageData + i)) = (((uint32_t)a) << 24) | (((uint32_t)r) << 16) | (((uint32_t)g) << 8) | (uint32_t)b; + } + + auto CAIROSURFACE = cairo_image_surface_create_for_data(imageData, CAIRO_FORMAT_ARGB32, ihdr.width, ihdr.height, ihdr.width * 4); + + if (!CAIROSURFACE) + return std::unexpected("loading png: cairo failed"); + + return CAIROSURFACE; +} \ No newline at end of file diff --git a/src/image/formats/Png.hpp b/src/image/formats/Png.hpp new file mode 100644 index 0000000..dec8ee3 --- /dev/null +++ b/src/image/formats/Png.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include <cairo/cairo.h> +#include <string> +#include <expected> + +namespace PNG { + std::expected<cairo_surface_t*, std::string> createSurfaceFromPNG(const std::string&); +}; \ No newline at end of file diff --git a/tests/image.cpp b/tests/image.cpp index 7067f9a..42a1082 100644 --- a/tests/image.cpp +++ b/tests/image.cpp @@ -16,7 +16,16 @@ bool tryLoadImage(const std::string& path) { std::println("Loaded {} successfully: Image is {}x{} of type {}", path, image.cairoSurface()->size().x, image.cairoSurface()->size().y, image.getMime()); - return true; + const auto TEST_DIR = std::filesystem::current_path().string() + "/test_output"; + + // try to write it for inspection + if (!std::filesystem::exists(TEST_DIR)) + std::filesystem::create_directory(TEST_DIR); + + std::string name = image.getMime(); + std::replace(name.begin(), name.end(), '/', '_'); + + return cairo_surface_write_to_png(image.cairoSurface()->cairo(), (TEST_DIR + "/" + name + ".png").c_str()) == CAIRO_STATUS_SUCCESS; } int main(int argc, char** argv, char** envp) { @@ -27,7 +36,8 @@ int main(int argc, char** argv, char** envp) { continue; auto expectation = true; #ifndef JXL_FOUND - if (file.path().filename() == "hyprland.jxl") expectation = false; + if (file.path().filename() == "hyprland.jxl") + expectation = false; #endif EXPECT(tryLoadImage(file.path()), expectation); }