mirror of
https://github.com/hyprwm/hyprgraphics.git
synced 2024-12-25 18:39:49 +01:00
core: Add image and cairo
This commit is contained in:
parent
8b0000af17
commit
292597d55a
23 changed files with 724 additions and 1 deletions
14
.gitignore
vendored
14
.gitignore
vendored
|
@ -30,3 +30,17 @@
|
||||||
*.exe
|
*.exe
|
||||||
*.out
|
*.out
|
||||||
*.app
|
*.app
|
||||||
|
|
||||||
|
build/
|
||||||
|
.vscode/
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
.cmake/
|
||||||
|
CMakeCache.txt
|
||||||
|
CMakeFiles/
|
||||||
|
CTestTestfile.cmake
|
||||||
|
DartConfiguration.tcl
|
||||||
|
Makefile
|
||||||
|
cmake_install.cmake
|
||||||
|
compile_commands.json
|
||||||
|
hyprutils.pc
|
||||||
|
|
61
CMakeLists.txt
Normal file
61
CMakeLists.txt
Normal file
|
@ -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)
|
17
README.md
17
README.md
|
@ -1,2 +1,17 @@
|
||||||
# hyprgraphics
|
# 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
|
||||||
|
```
|
||||||
|
|
1
VERSION
Normal file
1
VERSION
Normal file
|
@ -0,0 +1 @@
|
||||||
|
0.1.0
|
10
hyprgraphics.pc.in
Normal file
10
hyprgraphics.pc.in
Normal file
|
@ -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
|
29
include/hyprgraphics/cairo/CairoSurface.hpp
Normal file
29
include/hyprgraphics/cairo/CairoSurface.hpp
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cairo/cairo.h>
|
||||||
|
#include <hyprutils/math/Vector2D.hpp>
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
};
|
27
include/hyprgraphics/image/Image.hpp
Normal file
27
include/hyprgraphics/image/Image.hpp
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <cairo/cairo.h>
|
||||||
|
#include "../cairo/CairoSurface.hpp"
|
||||||
|
#include <hyprutils/memory/SharedPtr.hpp>
|
||||||
|
|
||||||
|
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<CCairoSurface> cairoSurface();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string lastError, filepath, mime;
|
||||||
|
Hyprutils::Memory::CSharedPointer<CCairoSurface> pCairoSurface;
|
||||||
|
bool imageHasAlpha = true, loadSuccess = false;
|
||||||
|
};
|
||||||
|
};
|
32
src/cairo/CairoSurface.cpp
Normal file
32
src/cairo/CairoSurface.cpp
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
#include <hyprgraphics/cairo/CairoSurface.hpp>
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
93
src/image/Image.cpp
Normal file
93
src/image/Image.cpp
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
#include <hyprgraphics/image/Image.hpp>
|
||||||
|
#include "formats/Bmp.hpp"
|
||||||
|
#include "formats/Jpeg.hpp"
|
||||||
|
#include "formats/JpegXL.hpp"
|
||||||
|
#include "formats/Webp.hpp"
|
||||||
|
#include <magic.h>
|
||||||
|
#include <format>
|
||||||
|
|
||||||
|
using namespace Hyprgraphics;
|
||||||
|
using namespace Hyprutils::Memory;
|
||||||
|
|
||||||
|
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());
|
||||||
|
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<CCairoSurface>(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<CCairoSurface> Hyprgraphics::CImage::cairoSurface() {
|
||||||
|
return pCairoSurface;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Hyprgraphics::CImage::getMime() {
|
||||||
|
return mime;
|
||||||
|
}
|
123
src/image/formats/Bmp.cpp
Normal file
123
src/image/formats/Bmp.cpp
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
#include "Bmp.hpp"
|
||||||
|
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
|
#include <fstream>
|
||||||
|
#include <vector>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
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<std::string> 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<char*>(&format), sizeof(format));
|
||||||
|
if (!(format[0] == 66 && format[1] == 77))
|
||||||
|
return "Unable to parse bitmap header: wrong bmp file type";
|
||||||
|
|
||||||
|
file.read(reinterpret_cast<char*>(&sizeOfFile), sizeof(sizeOfFile));
|
||||||
|
|
||||||
|
if (sizeOfFile != streamLength)
|
||||||
|
return "Unable to parse bitmap header: wrong value of file size header";
|
||||||
|
|
||||||
|
file.read(reinterpret_cast<char*>(&reserved1), sizeof(reserved1));
|
||||||
|
file.read(reinterpret_cast<char*>(&reserved2), sizeof(reserved2));
|
||||||
|
file.read(reinterpret_cast<char*>(&dataOffset), sizeof(dataOffset));
|
||||||
|
file.read(reinterpret_cast<char*>(&sizeOfBitmapHeader), sizeof(sizeOfBitmapHeader));
|
||||||
|
file.read(reinterpret_cast<char*>(&width), sizeof(width));
|
||||||
|
file.read(reinterpret_cast<char*>(&height), sizeof(height));
|
||||||
|
file.read(reinterpret_cast<char*>(&numberOfColors), sizeof(numberOfColors));
|
||||||
|
file.read(reinterpret_cast<char*>(&numberOfBitPerPixel), sizeof(numberOfBitPerPixel));
|
||||||
|
file.read(reinterpret_cast<char*>(&compressionMethod), sizeof(compressionMethod));
|
||||||
|
file.read(reinterpret_cast<char*>(&imageSize), sizeof(imageSize));
|
||||||
|
file.read(reinterpret_cast<char*>(&horizontalResolutionPPM), sizeof(horizontalResolutionPPM));
|
||||||
|
file.read(reinterpret_cast<char*>(&verticalResolutionPPM), sizeof(verticalResolutionPPM));
|
||||||
|
file.read(reinterpret_cast<char*>(&numberOfCollors), sizeof(numberOfCollors));
|
||||||
|
file.read(reinterpret_cast<char*>(&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<unsigned char> 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<char*>(&outputImage[imgCursor]), 1);
|
||||||
|
imgCursor++;
|
||||||
|
forthBitCounter++;
|
||||||
|
if (forthBitCounter == 3) {
|
||||||
|
outputImage[imgCursor] = 0;
|
||||||
|
imgCursor++;
|
||||||
|
forthBitCounter = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::expected<cairo_surface_t*, std::string> 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<char*>(&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);
|
||||||
|
}
|
9
src/image/formats/Bmp.hpp
Normal file
9
src/image/formats/Bmp.hpp
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cairo/cairo.h>
|
||||||
|
#include <string>
|
||||||
|
#include <expected>
|
||||||
|
|
||||||
|
namespace BMP {
|
||||||
|
std::expected<cairo_surface_t*, std::string> createSurfaceFromBMP(const std::string&);
|
||||||
|
};
|
61
src/image/formats/Jpeg.cpp
Normal file
61
src/image/formats/Jpeg.cpp
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
#include "Jpeg.hpp"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
std::expected<cairo_surface_t*, std::string> 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<uint8_t> bytes(file.tellg());
|
||||||
|
file.seekg(0);
|
||||||
|
file.read(reinterpret_cast<char*>(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;
|
||||||
|
}
|
10
src/image/formats/Jpeg.hpp
Normal file
10
src/image/formats/Jpeg.hpp
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cairo/cairo.h>
|
||||||
|
#include <string>
|
||||||
|
#include <jpeglib.h>
|
||||||
|
#include <expected>
|
||||||
|
|
||||||
|
namespace JPEG {
|
||||||
|
std::expected<cairo_surface_t*, std::string> createSurfaceFromJPEG(const std::string&);
|
||||||
|
};
|
92
src/image/formats/JpegXL.cpp
Normal file
92
src/image/formats/JpegXL.cpp
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
#include "JpegXL.hpp"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <jxl/decode_cxx.h>
|
||||||
|
#include <jxl/resizable_parallel_runner_cxx.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
std::expected<cairo_surface_t*, std::string> 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<uint8_t> bytes(file.tellg());
|
||||||
|
file.seekg(0);
|
||||||
|
file.read(reinterpret_cast<char*>(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;
|
||||||
|
}
|
9
src/image/formats/JpegXL.hpp
Normal file
9
src/image/formats/JpegXL.hpp
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cairo/cairo.h>
|
||||||
|
#include <string>
|
||||||
|
#include <expected>
|
||||||
|
|
||||||
|
namespace JXL {
|
||||||
|
std::expected<cairo_surface_t*, std::string> createSurfaceFromJXL(const std::string&);
|
||||||
|
};
|
63
src/image/formats/Webp.cpp
Normal file
63
src/image/formats/Webp.cpp
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
#include "Webp.hpp"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <webp/decode.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
std::expected<cairo_surface_t*, std::string> 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<uint8_t> bytes(file.tellg());
|
||||||
|
file.seekg(0);
|
||||||
|
file.read(reinterpret_cast<char*>(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;
|
||||||
|
}
|
9
src/image/formats/Webp.hpp
Normal file
9
src/image/formats/Webp.hpp
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cairo/cairo.h>
|
||||||
|
#include <string>
|
||||||
|
#include <expected>
|
||||||
|
|
||||||
|
namespace WEBP {
|
||||||
|
std::expected<cairo_surface_t*, std::string> createSurfaceFromWEBP(const std::string&);
|
||||||
|
};
|
33
tests/image.cpp
Normal file
33
tests/image.cpp
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
#include <print>
|
||||||
|
#include <format>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <hyprgraphics/image/Image.hpp>
|
||||||
|
#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;
|
||||||
|
}
|
BIN
tests/resource/images/hyprland.jpg
Normal file
BIN
tests/resource/images/hyprland.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 82 KiB |
BIN
tests/resource/images/hyprland.jxl
Normal file
BIN
tests/resource/images/hyprland.jxl
Normal file
Binary file not shown.
BIN
tests/resource/images/hyprland.png
Executable file
BIN
tests/resource/images/hyprland.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 346 KiB |
BIN
tests/resource/images/hyprland.webp
Normal file
BIN
tests/resource/images/hyprland.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 76 KiB |
32
tests/shared.hpp
Normal file
32
tests/shared.hpp
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
#pragma once
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
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)
|
Loading…
Reference in a new issue