diff --git a/.gitignore b/.gitignore index 259148f..041d4c5 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,17 @@ *.exe *.out *.app + +build/ +.vscode/ +.cache/ + +.cmake/ +CMakeCache.txt +CMakeFiles/ +CTestTestfile.cmake +DartConfiguration.tcl +Makefile +cmake_install.cmake +compile_commands.json +hyprutils.pc diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..92319cf --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,61 @@ +cmake_minimum_required(VERSION 3.19) + +file(READ "${CMAKE_SOURCE_DIR}/VERSION" VER_RAW) +string(STRIP ${VER_RAW} HYPRGRAPHICS_VERSION) +add_compile_definitions(HYPRGRAPHICS_VERSION="${HYPRGRAPHICS_VERSION}") + +project( + hyprgraphics + VERSION ${HYPRGRAPHICS_VERSION} + DESCRIPTION "Small C++ library for utilities used across the Hypr* ecosystem") + +include(CTest) +include(GNUInstallDirs) + +set(PREFIX ${CMAKE_INSTALL_PREFIX}) +set(INCLUDE ${CMAKE_INSTALL_FULL_INCLUDEDIR}) +set(LIBDIR ${CMAKE_INSTALL_FULL_LIBDIR}) + +configure_file(hyprgraphics.pc.in hyprgraphics.pc @ONLY) + +set(CMAKE_CXX_STANDARD 23) + +if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) + message(STATUS "Configuring hyprgraphics in Debug") + add_compile_definitions(HYPRGRAPHICS_DEBUG) +else() + add_compile_options(-O3) + message(STATUS "Configuring hyprgraphics in Release") +endif() + +file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp" "include/*.hpp") +file(GLOB_RECURSE PUBLIC_HEADERS CONFIGURE_DEPENDS "include/*.hpp") + +find_package(PkgConfig REQUIRED) +pkg_check_modules(deps REQUIRED IMPORTED_TARGET pixman-1 cairo hyprutils libjpeg libwebp libjxl libjxl_cms libjxl_threads libmagic) + +add_library(hyprgraphics SHARED ${SRCFILES}) +target_include_directories( + hyprgraphics + PUBLIC "./include" + PRIVATE "./src") +set_target_properties(hyprgraphics PROPERTIES VERSION ${HYPRGRAPHICS_VERSION} + SOVERSION 0) +target_link_libraries(hyprgraphics PkgConfig::deps) + +# tests +add_custom_target(tests) + +add_executable(hyprgraphics_image "tests/image.cpp") +target_link_libraries(hyprgraphics_image PRIVATE hyprgraphics PkgConfig::deps) +add_test( + NAME "Image" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests + COMMAND hyprgraphics_image "image") +add_dependencies(tests hyprgraphics_image) + +# Installation +install(TARGETS hyprgraphics) +install(DIRECTORY "include/hyprgraphics" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +install(FILES ${CMAKE_BINARY_DIR}/hyprgraphics.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) diff --git a/README.md b/README.md index 4ce67f6..c45f51c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,17 @@ # hyprgraphics -Hyprland graphics / resource utilities + +Hyprgraphics is a small C++ library with graphics / resource related utilities used across the hypr* ecosystem. + +## Stability + +Hyprutils depends on the ABI stability of the stdlib implementation of your compiler. Sover bumps will be done only for hyprutils ABI breaks, not stdlib. + +## Building + +```sh +git clone https://github.com/hyprwm/hyprgraphics +cd hyprgraphics/ +cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:PATH=/usr -S . -B ./build +cmake --build ./build --config Release --target all -j`nproc 2>/dev/null || getconf _NPROCESSORS_CONF` +sudo cmake --install build +``` diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/hyprgraphics.pc.in b/hyprgraphics.pc.in new file mode 100644 index 0000000..6a29d34 --- /dev/null +++ b/hyprgraphics.pc.in @@ -0,0 +1,10 @@ +prefix=@PREFIX@ +includedir=@INCLUDE@ +libdir=@LIBDIR@ + +Name: hyprgraphics +URL: https://github.com/hyprwm/hyprgraphics +Description: Hyprland graphics utilities library used across the ecosystem +Version: @HYPRGRAPHICS_VERSION@ +Cflags: -I${includedir} +Libs: -L${libdir} -lhyprgraphics diff --git a/include/hyprgraphics/cairo/CairoSurface.hpp b/include/hyprgraphics/cairo/CairoSurface.hpp new file mode 100644 index 0000000..6e23f74 --- /dev/null +++ b/include/hyprgraphics/cairo/CairoSurface.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include + +namespace Hyprgraphics { + + // A simple cairo surface wrapper. Will destroy the surface in the ~dtor. + class CCairoSurface { + public: + CCairoSurface(cairo_surface_t* surf); + ~CCairoSurface(); + + CCairoSurface(CCairoSurface&&) = delete; + CCairoSurface(const CCairoSurface&) = delete; + CCairoSurface& operator=(const CCairoSurface&) = delete; + CCairoSurface& operator=(CCairoSurface&&) = delete; + + cairo_surface_t* cairo(); + Hyprutils::Math::Vector2D size(); + int status(); + uint8_t* data(); + int stride(); + + private: + cairo_surface_t* pSurface = nullptr; + }; +}; diff --git a/include/hyprgraphics/image/Image.hpp b/include/hyprgraphics/image/Image.hpp new file mode 100644 index 0000000..5cf0a1c --- /dev/null +++ b/include/hyprgraphics/image/Image.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include "../cairo/CairoSurface.hpp" +#include + +namespace Hyprgraphics { + class CImage { + public: + // create an image from a provided path. + CImage(const std::string& path); + ~CImage(); + + bool success(); + bool hasAlpha(); + std::string getError(); + std::string getMime(); + + Hyprutils::Memory::CSharedPointer cairoSurface(); + + private: + std::string lastError, filepath, mime; + Hyprutils::Memory::CSharedPointer pCairoSurface; + bool imageHasAlpha = true, loadSuccess = false; + }; +}; diff --git a/src/cairo/CairoSurface.cpp b/src/cairo/CairoSurface.cpp new file mode 100644 index 0000000..b98ebe4 --- /dev/null +++ b/src/cairo/CairoSurface.cpp @@ -0,0 +1,32 @@ +#include + +using namespace Hyprgraphics; + +Hyprgraphics::CCairoSurface::CCairoSurface(cairo_surface_t* surf) : pSurface(surf) { + ; +} + +Hyprgraphics::CCairoSurface::~CCairoSurface() { + if (pSurface) + cairo_surface_destroy(pSurface); +} + +cairo_surface_t* Hyprgraphics::CCairoSurface::cairo() { + return pSurface; +} + +Hyprutils::Math::Vector2D Hyprgraphics::CCairoSurface::size() { + return {cairo_image_surface_get_width(pSurface), cairo_image_surface_get_height(pSurface)}; +} + +int Hyprgraphics::CCairoSurface::status() { + return cairo_surface_status(pSurface); +} + +uint8_t* Hyprgraphics::CCairoSurface::data() { + return (uint8_t*)cairo_image_surface_get_data(pSurface); +} + +int Hyprgraphics::CCairoSurface::stride() { + return cairo_image_surface_get_stride(pSurface); +} diff --git a/src/image/Image.cpp b/src/image/Image.cpp new file mode 100644 index 0000000..6ba88f5 --- /dev/null +++ b/src/image/Image.cpp @@ -0,0 +1,93 @@ +#include +#include "formats/Bmp.hpp" +#include "formats/Jpeg.hpp" +#include "formats/JpegXL.hpp" +#include "formats/Webp.hpp" +#include +#include + +using namespace Hyprgraphics; +using namespace Hyprutils::Memory; + +Hyprgraphics::CImage::CImage(const std::string& path) : filepath(path) { + std::expected 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()); + 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); + imageHasAlpha = false; + mime = "image/jpeg"; + } else if (path.find(".bmp") == len - 4 || path.find(".BMP") == len - 4) { + CAIROSURFACE = BMP::createSurfaceFromBMP(path); + imageHasAlpha = false; + mime = "image/bmp"; + } else if (path.find(".webp") == len - 5 || path.find(".WEBP") == len - 5) { + CAIROSURFACE = WEBP::createSurfaceFromWEBP(path); + mime = "image/webp"; + } else if (path.find(".jxl") == len - 4 || path.find(".JXL") == len - 4) { + CAIROSURFACE = JXL::createSurfaceFromJXL(path); + mime = "image/jxl"; + } else { + // magic is slow, so only use it when no recognized extension is found + auto handle = magic_open(MAGIC_NONE | MAGIC_COMPRESS); + magic_load(handle, nullptr); + + const auto type_str = std::string(magic_file(handle, path.c_str())); + 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()); + mime = "image/png"; + } else if (first_word == "JPEG") { + CAIROSURFACE = JPEG::createSurfaceFromJPEG(path); + imageHasAlpha = false; + mime = "image/jpeg"; + } else if (first_word == "BMP") { + CAIROSURFACE = BMP::createSurfaceFromBMP(path); + imageHasAlpha = false; + mime = "image/bmp"; + } else { + lastError = "unrecognized image"; + return; + } + } + + if (!CAIROSURFACE) { + lastError = CAIROSURFACE.error(); + return; + } + + if (const auto STATUS = cairo_surface_status(*CAIROSURFACE); STATUS != CAIRO_STATUS_SUCCESS) { + lastError = std::format("Could not create surface: {}", cairo_status_to_string(STATUS)); + return; + } + + loadSuccess = true; + pCairoSurface = makeShared(CAIROSURFACE.value()); +} + +Hyprgraphics::CImage::~CImage() { + ; +} + +bool Hyprgraphics::CImage::success() { + return loadSuccess; +} + +bool Hyprgraphics::CImage::hasAlpha() { + return imageHasAlpha; +} + +std::string Hyprgraphics::CImage::getError() { + return lastError; +} + +Hyprutils::Memory::CSharedPointer Hyprgraphics::CImage::cairoSurface() { + return pCairoSurface; +} + +std::string Hyprgraphics::CImage::getMime() { + return mime; +} diff --git a/src/image/formats/Bmp.cpp b/src/image/formats/Bmp.cpp new file mode 100644 index 0000000..78994c4 --- /dev/null +++ b/src/image/formats/Bmp.cpp @@ -0,0 +1,123 @@ +#include "Bmp.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +class BmpHeader { + public: + unsigned char format[2]; + uint32_t sizeOfFile; + uint16_t reserved1; + uint16_t reserved2; + uint32_t dataOffset; + uint32_t sizeOfBitmapHeader; + uint32_t width; + uint32_t height; + uint16_t numberOfColors; + uint16_t numberOfBitPerPixel; + uint32_t compressionMethod; + uint32_t imageSize; + uint32_t horizontalResolutionPPM; + uint32_t verticalResolutionPPM; + uint32_t numberOfCollors; + uint32_t numberOfImportantCollors; + + std::optional load(std::ifstream& file) { + file.seekg(0, std::ios::end); + uint32_t streamLength = file.tellg(); + file.seekg(0, std::ios::beg); + + file.read(reinterpret_cast(&format), sizeof(format)); + if (!(format[0] == 66 && format[1] == 77)) + return "Unable to parse bitmap header: wrong bmp file type"; + + file.read(reinterpret_cast(&sizeOfFile), sizeof(sizeOfFile)); + + if (sizeOfFile != streamLength) + return "Unable to parse bitmap header: wrong value of file size header"; + + file.read(reinterpret_cast(&reserved1), sizeof(reserved1)); + file.read(reinterpret_cast(&reserved2), sizeof(reserved2)); + file.read(reinterpret_cast(&dataOffset), sizeof(dataOffset)); + file.read(reinterpret_cast(&sizeOfBitmapHeader), sizeof(sizeOfBitmapHeader)); + file.read(reinterpret_cast(&width), sizeof(width)); + file.read(reinterpret_cast(&height), sizeof(height)); + file.read(reinterpret_cast(&numberOfColors), sizeof(numberOfColors)); + file.read(reinterpret_cast(&numberOfBitPerPixel), sizeof(numberOfBitPerPixel)); + file.read(reinterpret_cast(&compressionMethod), sizeof(compressionMethod)); + file.read(reinterpret_cast(&imageSize), sizeof(imageSize)); + file.read(reinterpret_cast(&horizontalResolutionPPM), sizeof(horizontalResolutionPPM)); + file.read(reinterpret_cast(&verticalResolutionPPM), sizeof(verticalResolutionPPM)); + file.read(reinterpret_cast(&numberOfCollors), sizeof(numberOfCollors)); + file.read(reinterpret_cast(&numberOfImportantCollors), sizeof(numberOfImportantCollors)); + + if (!imageSize) + imageSize = sizeOfFile - dataOffset; + + if (imageSize != (width * height * numberOfBitPerPixel / 8)) + return "Unable to parse bitmap header: wrong image size"; + + file.seekg(dataOffset); + }; +}; + +static void reflectImage(unsigned char* image, uint32_t numberOfRows, int stride) { + int rowStart = 0; + int rowEnd = numberOfRows - 1; + std::vector temp; + temp.resize(stride); + while (rowStart < rowEnd) { + memcpy(&temp[0], &image[rowStart * stride], stride); + memcpy(&image[rowStart * stride], &image[rowEnd * stride], stride); + memcpy(&image[rowEnd * stride], &temp[0], stride); + rowStart++; + rowEnd--; + } +}; + +static void convertRgbToArgb(std::ifstream& imageStream, unsigned char* outputImage, uint32_t newImageSize) { + uint8_t forthBitCounter = 0; + unsigned long imgCursor = 0; + while (imgCursor < newImageSize) { + imageStream.read(reinterpret_cast(&outputImage[imgCursor]), 1); + imgCursor++; + forthBitCounter++; + if (forthBitCounter == 3) { + outputImage[imgCursor] = 0; + imgCursor++; + forthBitCounter = 0; + } + } +}; + +std::expected BMP::createSurfaceFromBMP(const std::string& path) { + if (!std::filesystem::exists(path)) + return std::unexpected("loading bmp: file doesn't exist"); + + std::ifstream bitmapImageStream(path); + BmpHeader bitmapHeader; + if (const auto RET = bitmapHeader.load(bitmapImageStream); RET.has_value()) + return std::unexpected("loading bmp: " + *RET); + + cairo_format_t format = CAIRO_FORMAT_ARGB32; + int stride = cairo_format_stride_for_width(format, bitmapHeader.width); + unsigned char* imageData = (unsigned char*)malloc(bitmapHeader.height * stride); + + if (bitmapHeader.numberOfBitPerPixel == 24) + convertRgbToArgb(bitmapImageStream, imageData, bitmapHeader.height * stride); + else if (bitmapHeader.numberOfBitPerPixel == 32) + bitmapImageStream.read(reinterpret_cast(&imageData), bitmapHeader.imageSize); + else { + bitmapImageStream.close(); + return std::unexpected("loading bmp: unsupported bmp stream"); + } + bitmapImageStream.close(); + reflectImage(imageData, bitmapHeader.height, stride); + return cairo_image_surface_create_for_data(imageData, format, bitmapHeader.width, bitmapHeader.height, stride); +} \ No newline at end of file diff --git a/src/image/formats/Bmp.hpp b/src/image/formats/Bmp.hpp new file mode 100644 index 0000000..0a479e6 --- /dev/null +++ b/src/image/formats/Bmp.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include +#include +#include + +namespace BMP { + std::expected createSurfaceFromBMP(const std::string&); +}; \ No newline at end of file diff --git a/src/image/formats/Jpeg.cpp b/src/image/formats/Jpeg.cpp new file mode 100644 index 0000000..6ba39bc --- /dev/null +++ b/src/image/formats/Jpeg.cpp @@ -0,0 +1,61 @@ +#include "Jpeg.hpp" + +#include +#include +#include + +std::expected JPEG::createSurfaceFromJPEG(const std::string& path) { + + if (!std::filesystem::exists(path)) + return std::unexpected("loading jpeg: file doesn't exist"); + + if (__BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__) + return std::unexpected("loading jpeg: cannot load on big endian"); + + 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()); + + // now the JPEG is in the memory + + jpeg_decompress_struct decompressStruct = {}; + jpeg_error_mgr errorManager = {}; + + decompressStruct.err = jpeg_std_error(&errorManager); + jpeg_create_decompress(&decompressStruct); + jpeg_mem_src(&decompressStruct, bytes.data(), bytes.size()); + jpeg_read_header(&decompressStruct, true); + +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + decompressStruct.out_color_space = JCS_EXT_BGRA; +#else + decompressStruct.out_color_space = JCS_EXT_ARGB; +#endif + + // decompress + jpeg_start_decompress(&decompressStruct); + + auto cairoSurface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, decompressStruct.output_width, decompressStruct.output_height); + + if (cairo_surface_status(cairoSurface) != CAIRO_STATUS_SUCCESS) + return std::unexpected("loading jpeg: cairo failed to create an image surface"); + + const auto CAIRODATA = cairo_image_surface_get_data(cairoSurface); + const auto CAIROSTRIDE = cairo_image_surface_get_stride(cairoSurface); + JSAMPROW rowRead; + + while (decompressStruct.output_scanline < decompressStruct.output_height) { + const auto PROW = CAIRODATA + (decompressStruct.output_scanline * CAIROSTRIDE); + rowRead = PROW; + jpeg_read_scanlines(&decompressStruct, &rowRead, 1); + } + + cairo_surface_mark_dirty(cairoSurface); + cairo_surface_set_mime_data(cairoSurface, CAIRO_MIME_TYPE_JPEG, bytes.data(), bytes.size(), nullptr, nullptr); + jpeg_finish_decompress(&decompressStruct); + jpeg_destroy_decompress(&decompressStruct); + + return cairoSurface; +} diff --git a/src/image/formats/Jpeg.hpp b/src/image/formats/Jpeg.hpp new file mode 100644 index 0000000..d320545 --- /dev/null +++ b/src/image/formats/Jpeg.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include +#include +#include +#include + +namespace JPEG { + std::expected createSurfaceFromJPEG(const std::string&); +}; \ No newline at end of file diff --git a/src/image/formats/JpegXL.cpp b/src/image/formats/JpegXL.cpp new file mode 100644 index 0000000..2740516 --- /dev/null +++ b/src/image/formats/JpegXL.cpp @@ -0,0 +1,92 @@ +#include "JpegXL.hpp" + +#include +#include +#include +#include +#include + +std::expected JXL::createSurfaceFromJXL(const std::string& path) { + + if (!std::filesystem::exists(path)) + return std::unexpected("loading jxl: file doesn't exist"); + + 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) + return std::unexpected("loading jxl: not jxl"); + + auto dec = JxlDecoderMake(nullptr); + auto runner = JxlResizableParallelRunnerMake(nullptr); + if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec.get(), JxlResizableParallelRunner, runner.get())) + return std::unexpected("loading jxl: JxlResizableParallelRunner failed"); + + if (JXL_DEC_SUCCESS != JxlDecoderSubscribeEvents(dec.get(), JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE)) + return std::unexpected("loading jxl: JxlDecoderSubscribeEvents failed"); + + JxlDecoderSetInput(dec.get(), bytes.data(), bytes.size()); + JxlDecoderCloseInput(dec.get()); + if (JXL_DEC_BASIC_INFO != JxlDecoderProcessInput(dec.get())) + return std::unexpected("loading jxl: JxlDecoderProcessInput failed"); + + JxlBasicInfo basicInfo; + if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec.get(), &basicInfo)) + return std::unexpected("loading jxl: JxlDecoderGetBasicInfo failed"); + + auto cairoSurface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, basicInfo.xsize, basicInfo.ysize); + if (cairo_surface_status(cairoSurface) != CAIRO_STATUS_SUCCESS) { + cairo_surface_destroy(cairoSurface); + return std::unexpected("loading jxl: cairo failed"); + } + + 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) { + cairo_surface_destroy(cairoSurface); + return std::unexpected("loading jxl: JxlDecoderProcessInput failed"); + } else if (status == JXL_DEC_NEED_MORE_INPUT) { + cairo_surface_destroy(cairoSurface); + return std::unexpected("loading jxl: JxlDecoderProcessInput expected more input"); + } 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)) { + cairo_surface_destroy(cairoSurface); + return std::unexpected("loading jxl: JxlDecoderImageOutBufferSize failed"); + } + if (bufferSize != OUTPUTSIZE) { + cairo_surface_destroy(cairoSurface); + return std::unexpected("loading jxl: invalid output size"); + } + if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer(dec.get(), &format, CAIRODATA, bufferSize)) { + cairo_surface_destroy(cairoSurface); + return std::unexpected("loading jxl: JxlDecoderSetImageOutBuffer failed"); + } + } 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; + } + } + + return nullptr; +} diff --git a/src/image/formats/JpegXL.hpp b/src/image/formats/JpegXL.hpp new file mode 100644 index 0000000..2e034aa --- /dev/null +++ b/src/image/formats/JpegXL.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include +#include +#include + +namespace JXL { + std::expected createSurfaceFromJXL(const std::string&); +}; diff --git a/src/image/formats/Webp.cpp b/src/image/formats/Webp.cpp new file mode 100644 index 0000000..df2710c --- /dev/null +++ b/src/image/formats/Webp.cpp @@ -0,0 +1,63 @@ +#include "Webp.hpp" + +#include +#include +#include +#include + +std::expected WEBP::createSurfaceFromWEBP(const std::string& path) { + + if (!std::filesystem::exists(path)) + return std::unexpected("loading webp: file doesn't exist"); + + 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()); + + // now the WebP is in the memory + + WebPDecoderConfig config; + if (!WebPInitDecoderConfig(&config)) + return std::unexpected("loading webp: WebPInitDecoderConfig failed"); + + if (WebPGetFeatures(bytes.data(), bytes.size(), &config.input) != VP8_STATUS_OK) + return std::unexpected("loading webp: file is not valid webp"); + + const auto HEIGHT = config.input.height; + const auto WIDTH = config.input.width; + + auto cairoSurface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, WIDTH, HEIGHT); + if (cairo_surface_status(cairoSurface) != CAIRO_STATUS_SUCCESS) { + cairo_surface_destroy(cairoSurface); + return std::unexpected("loading webp: cairo failed"); + } + +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + config.output.colorspace = MODE_bgrA; +#else + config.output.colorspace = MODE_Argb; +#endif + + const auto CAIRODATA = cairo_image_surface_get_data(cairoSurface); + const auto CAIROSTRIDE = cairo_image_surface_get_stride(cairoSurface); + + config.options.no_fancy_upsampling = 1; + config.output.u.RGBA.rgba = CAIRODATA; + config.output.u.RGBA.stride = CAIROSTRIDE; + config.output.u.RGBA.size = CAIROSTRIDE * HEIGHT; + config.output.is_external_memory = 1; + config.output.width = WIDTH; + config.output.height = HEIGHT; + + if (WebPDecode(bytes.data(), bytes.size(), &config) != VP8_STATUS_OK) + return std::unexpected("loading webp: webp decode failed"); + + cairo_surface_mark_dirty(cairoSurface); + cairo_surface_set_mime_data(cairoSurface, "image/webp", bytes.data(), bytes.size(), nullptr, nullptr); + + WebPFreeDecBuffer(&config.output); + + return cairoSurface; +} diff --git a/src/image/formats/Webp.hpp b/src/image/formats/Webp.hpp new file mode 100644 index 0000000..b875a61 --- /dev/null +++ b/src/image/formats/Webp.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include +#include +#include + +namespace WEBP { + std::expected createSurfaceFromWEBP(const std::string&); +}; diff --git a/tests/image.cpp b/tests/image.cpp new file mode 100644 index 0000000..4cd1287 --- /dev/null +++ b/tests/image.cpp @@ -0,0 +1,33 @@ +#include +#include +#include +#include +#include "shared.hpp" + +using namespace Hyprgraphics; + +bool tryLoadImage(const std::string& path) { + auto image = CImage(path); + + if (!image.success()) { + std::println("Failed to load {}: {}", path, image.getError()); + return false; + } + + std::println("Loaded {} successfully: Image is {}x{} of type {}", path, image.cairoSurface()->size().x, image.cairoSurface()->size().y, image.getMime()); + + return true; +} + +int main(int argc, char** argv, char** envp) { + int ret = 0; + + for (auto& file : std::filesystem::directory_iterator("./resource/images/")) { + if (!file.is_regular_file()) + continue; + + EXPECT(tryLoadImage(file.path()), true); + } + + return ret; +} diff --git a/tests/resource/images/hyprland.jpg b/tests/resource/images/hyprland.jpg new file mode 100644 index 0000000..44281fd Binary files /dev/null and b/tests/resource/images/hyprland.jpg differ diff --git a/tests/resource/images/hyprland.jxl b/tests/resource/images/hyprland.jxl new file mode 100644 index 0000000..d9be7e5 Binary files /dev/null and b/tests/resource/images/hyprland.jxl differ diff --git a/tests/resource/images/hyprland.png b/tests/resource/images/hyprland.png new file mode 100755 index 0000000..14ea6a4 Binary files /dev/null and b/tests/resource/images/hyprland.png differ diff --git a/tests/resource/images/hyprland.webp b/tests/resource/images/hyprland.webp new file mode 100644 index 0000000..85ab15c Binary files /dev/null and b/tests/resource/images/hyprland.webp differ diff --git a/tests/shared.hpp b/tests/shared.hpp new file mode 100644 index 0000000..33109f8 --- /dev/null +++ b/tests/shared.hpp @@ -0,0 +1,32 @@ +#pragma once +#include + +namespace Colors { + constexpr const char* RED = "\x1b[31m"; + constexpr const char* GREEN = "\x1b[32m"; + constexpr const char* YELLOW = "\x1b[33m"; + constexpr const char* BLUE = "\x1b[34m"; + constexpr const char* MAGENTA = "\x1b[35m"; + constexpr const char* CYAN = "\x1b[36m"; + constexpr const char* RESET = "\x1b[0m"; +}; + +#define EXPECT(expr, val) \ + if (const auto RESULT = expr; RESULT != (val)) { \ + std::cout << Colors::RED << "Failed: " << Colors::RESET << #expr << ", expected " << val << " but got " << RESULT << "\n"; \ + ret = 1; \ + } else { \ + std::cout << Colors::GREEN << "Passed " << Colors::RESET << #expr << ". Got " << val << "\n"; \ + } +#define EXPECT_VECTOR2D(expr, val) \ + do { \ + const auto& RESULT = expr; \ + const auto& EXPECTED = val; \ + if (!(std::abs(RESULT.x - EXPECTED.x) < 1e-6 && std::abs(RESULT.y - EXPECTED.y) < 1e-6)) { \ + std::cout << Colors::RED << "Failed: " << Colors::RESET << #expr << ", expected (" << EXPECTED.x << ", " << EXPECTED.y << ") but got (" << RESULT.x << ", " \ + << RESULT.y << ")\n"; \ + ret = 1; \ + } else { \ + std::cout << Colors::GREEN << "Passed " << Colors::RESET << #expr << ". Got (" << RESULT.x << ", " << RESULT.y << ")\n"; \ + } \ + } while (0)