Compare commits

...

17 commits
v0.1.1 ... main

Author SHA1 Message Date
Honkazel
6075491094
core: clang-tidy & comp changes (#15)
* some clang-tidyfy

* designated init in Color header

* some linkage changes

* just doin some casts explicit

* oeao

* bruh

* explicitly cast to size_t, not ptrdiff_t
2025-04-19 00:31:30 +02:00
Vaxry
9d7f2687c8 version: bump to 0.1.3 2025-04-06 16:28:42 +01:00
UjinT34
760d67a2a8
color: CM structs, constants & math (#14) 2025-04-06 17:27:46 +02:00
Mihai Fufezan
175c6b29b6
CI: remove deprecated magic-nix-cache-action 2025-02-08 23:11:11 +02:00
Jan Beich
575ae47b78
cmakelists: pass all libjxl CFLAGS/LDFLAGS after 52202272d8 (#13)
JXL_LIBRARIES contains `pkg-config --libs-only-l libjxl` but FreeBSD
also needs JXL_LIBRARY_DIRS aka `pkg-config --libs-only-L libjxl`.
2025-02-06 11:23:12 +00:00
Vaxry
e19ee9031a version: bump to 0.1.2 2025-02-04 16:55:19 +00:00
davc0n
5ac80e3686
tests: add a symlink test (#10)
* Add symlink test

* Add tests output dir to .gitignore
2025-02-01 20:10:59 +01:00
Vaxry
12cd7034e4 png: handle invalid buffer size returned by libspng
sometimes (no clue why) spng_decoded_image_size is just plain wrong. In those cases, just guess what the size should be with 32bpp.

fixes #9
2025-01-27 23:00:29 +00:00
Mihai Fufezan
23783b9603
CI/Arch/Clang: add libspng 2025-01-27 15:43:56 +02:00
Mihai Fufezan
6355b72d9c
Nix: add libspng dep 2025-01-27 15:42:22 +02:00
Vaxry
0c11438de4 core: move to libspng for png 2025-01-27 13:39:39 +00:00
Mihai Fufezan
0d77b4895a
flake.lock: update 2025-01-23 14:21:29 +02:00
Zach DeCook
52202272d8
core: Allow compiling without JXL support (#6)
* Allow compiling without JXL support

Remember to link the libraries and add the compile definitions

* tests: when compiled without JXL support, expect that to fail
2025-01-05 22:14:50 +00:00
mcwindy
b09980755d
README: Add Dependencies (#4) 2024-12-31 16:07:26 +00:00
Mihai Fufezan
6dea3fba08
flake.lock: update 2024-12-23 00:23:56 +02:00
Jan Beich
0f9b8ca692
color: add missing header for libc++ (#3)
src/color/Color.cpp:71:66: error: no member named 'min' in namespace 'std'; did you mean 'fmin'?
   71 |     const double vmax = std::max(std::max(r, g), b), vmin = std::min(std::min(r, g), b);
      |                                                             ~~~~~^~~
      |                                                                  fmin
/usr/include/c++/v1/cmath:447:9: note: 'fmin' declared here
  447 | using ::fmin _LIBCPP_USING_IF_EXISTS;
      |         ^
src/color/Color.cpp:71:75: error: no member named 'min' in namespace 'std'; did you mean 'fmin'?
   71 |     const double vmax = std::max(std::max(r, g), b), vmin = std::min(std::min(r, g), b);
      |                                                                      ~~~~~^~~
      |                                                                           fmin
/usr/include/c++/v1/cmath:447:9: note: 'fmin' declared here
  447 | using ::fmin _LIBCPP_USING_IF_EXISTS;
      |         ^
2024-12-18 17:59:51 +01:00
qxb3
fb2c026864
image: add symlink support (#1) 2024-12-08 19:53:39 +01:00
19 changed files with 503 additions and 63 deletions

101
.clang-tidy Normal file
View file

@ -0,0 +1,101 @@
WarningsAsErrors: '*'
HeaderFilterRegex: '.*\.hpp'
FormatStyle: 'file'
Checks: >
-*,
bugprone-*,
-bugprone-easily-swappable-parameters,
-bugprone-forward-declaration-namespace,
-bugprone-forward-declaration-namespace,
-bugprone-macro-parentheses,
-bugprone-narrowing-conversions,
-bugprone-branch-clone,
-bugprone-assignment-in-if-condition,
concurrency-*,
-concurrency-mt-unsafe,
cppcoreguidelines-*,
-cppcoreguidelines-owning-memory,
-cppcoreguidelines-avoid-magic-numbers,
-cppcoreguidelines-pro-bounds-constant-array-index,
-cppcoreguidelines-avoid-const-or-ref-data-members,
-cppcoreguidelines-non-private-member-variables-in-classes,
-cppcoreguidelines-avoid-goto,
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
-cppcoreguidelines-avoid-do-while,
-cppcoreguidelines-avoid-non-const-global-variables,
-cppcoreguidelines-special-member-functions,
-cppcoreguidelines-explicit-virtual-functions,
-cppcoreguidelines-avoid-c-arrays,
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
-cppcoreguidelines-narrowing-conversions,
-cppcoreguidelines-pro-type-union-access,
-cppcoreguidelines-pro-type-member-init,
-cppcoreguidelines-macro-usage,
-cppcoreguidelines-macro-to-enum,
-cppcoreguidelines-init-variables,
-cppcoreguidelines-pro-type-cstyle-cast,
-cppcoreguidelines-pro-type-vararg,
-cppcoreguidelines-pro-type-reinterpret-cast,
google-global-names-in-headers,
-google-readability-casting,
google-runtime-operator,
misc-*,
-misc-unused-parameters,
-misc-no-recursion,
-misc-non-private-member-variables-in-classes,
-misc-include-cleaner,
-misc-use-anonymous-namespace,
-misc-const-correctness,
modernize-*,
-modernize-return-braced-init-list,
-modernize-use-trailing-return-type,
-modernize-use-using,
-modernize-use-override,
-modernize-avoid-c-arrays,
-modernize-macro-to-enum,
-modernize-loop-convert,
-modernize-use-nodiscard,
-modernize-pass-by-value,
-modernize-use-auto,
performance-*,
-performance-avoid-endl,
-performance-unnecessary-value-param,
portability-std-allocator-const,
readability-*,
-readability-function-cognitive-complexity,
-readability-function-size,
-readability-identifier-length,
-readability-magic-numbers,
-readability-uppercase-literal-suffix,
-readability-braces-around-statements,
-readability-redundant-access-specifiers,
-readability-else-after-return,
-readability-container-data-pointer,
-readability-implicit-bool-conversion,
-readability-avoid-nested-conditional-operator,
-readability-redundant-member-init,
-readability-redundant-string-init,
-readability-avoid-const-params-in-decls,
-readability-named-parameter,
-readability-convert-member-functions-to-static,
-readability-qualified-auto,
-readability-make-member-function-const,
-readability-isolate-declaration,
-readability-inconsistent-declaration-parameter-name,
-clang-diagnostic-error,
CheckOptions:
performance-for-range-copy.WarnOnAllAutoCopies: true
performance-inefficient-string-concatenation.StrictMode: true
readability-braces-around-statements.ShortStatementLines: 0
readability-identifier-naming.ClassCase: CamelCase
readability-identifier-naming.ClassIgnoredRegexp: I.*
readability-identifier-naming.ClassPrefix: C # We can't use regex here?!?!?!?
readability-identifier-naming.EnumCase: CamelCase
readability-identifier-naming.EnumPrefix: e
readability-identifier-naming.EnumConstantCase: UPPER_CASE
readability-identifier-naming.FunctionCase: camelBack
readability-identifier-naming.NamespaceCase: CamelCase
readability-identifier-naming.NamespacePrefix: N
readability-identifier-naming.StructPrefix: S
readability-identifier-naming.StructCase: CamelCase

View file

@ -17,7 +17,7 @@ jobs:
run: | run: |
sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf
pacman --noconfirm --noprogressbar -Syyu 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 - name: Build hyprgraphics with gcc
run: | run: |
@ -44,7 +44,7 @@ jobs:
run: | run: |
sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf
pacman --noconfirm --noprogressbar -Syyu 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 clang - name: Build hyprgraphics with clang
run: | run: |

View file

@ -14,7 +14,6 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: cachix/install-nix-action@v26 - uses: cachix/install-nix-action@v26
- uses: DeterminateSystems/magic-nix-cache-action@main
# not needed (yet) # not needed (yet)
# - uses: cachix/cachix-action@v12 # - uses: cachix/cachix-action@v12

2
.gitignore vendored
View file

@ -44,3 +44,5 @@ Makefile
cmake_install.cmake cmake_install.cmake
compile_commands.json compile_commands.json
hyprutils.pc hyprutils.pc
tests/test_output/

View file

@ -21,6 +21,16 @@ set(LIBDIR ${CMAKE_INSTALL_FULL_LIBDIR})
configure_file(hyprgraphics.pc.in hyprgraphics.pc @ONLY) configure_file(hyprgraphics.pc.in hyprgraphics.pc @ONLY)
set(CMAKE_CXX_STANDARD 26) set(CMAKE_CXX_STANDARD 26)
set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)
# -Wno-missing-braces for clang
add_compile_options(
-Wall
-Wextra
-Wpedantic
-Wno-unused-parameter
-Wno-unused-value
-Wno-missing-field-initializers
-Wno-missing-braces)
if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG)
message(STATUS "Configuring hyprgraphics in Debug") message(STATUS "Configuring hyprgraphics in Debug")
@ -47,10 +57,22 @@ pkg_check_modules(
hyprutils hyprutils
libjpeg libjpeg
libwebp libwebp
libmagic
spng)
pkg_check_modules(
JXL
IMPORTED_TARGET
libjxl libjxl
libjxl_cms libjxl_cms
libjxl_threads libjxl_threads
libmagic) )
if(NOT JXL_FOUND)
file(GLOB_RECURSE JPEGXLFILES CONFIGURE_DEPENDS "src/*JpegXL.cpp")
list(REMOVE_ITEM SRCFILES ${JPEGXLFILES})
else()
add_compile_definitions(JXL_FOUND)
endif()
add_library(hyprgraphics SHARED ${SRCFILES}) add_library(hyprgraphics SHARED ${SRCFILES})
target_include_directories( target_include_directories(
@ -60,6 +82,9 @@ target_include_directories(
set_target_properties(hyprgraphics PROPERTIES VERSION ${HYPRGRAPHICS_VERSION} set_target_properties(hyprgraphics PROPERTIES VERSION ${HYPRGRAPHICS_VERSION}
SOVERSION 0) SOVERSION 0)
target_link_libraries(hyprgraphics PkgConfig::deps) target_link_libraries(hyprgraphics PkgConfig::deps)
if(JXL_FOUND)
target_link_libraries(hyprgraphics PkgConfig::JXL)
endif()
# tests # tests
add_custom_target(tests) add_custom_target(tests)

View file

@ -6,6 +6,22 @@ Hyprgraphics is a small C++ library with graphics / resource related utilities u
Hyprgraphics depends on the ABI stability of the stdlib implementation of your compiler. Sover bumps will be done only for hyprgraphics ABI breaks, not stdlib. Hyprgraphics depends on the ABI stability of the stdlib implementation of your compiler. Sover bumps will be done only for hyprgraphics ABI breaks, not stdlib.
## Dependencies
Requires a compiler with C++26 support.
Dep list:
- pixman-1
- cairo
- hyprutils
- libjpeg
- libwebp
- libjxl [optional]
- libjxl_cms [optional]
- libjxl_threads [optional]
- libmagic
- libspng
## Building ## Building
```sh ```sh

View file

@ -1 +1 @@
0.1.1 0.1.3

View file

@ -10,11 +10,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1732288281, "lastModified": 1737632363,
"narHash": "sha256-XTU9B53IjGeJiJ7LstOhuxcRjCOFkQFl01H78sT9Lg4=", "narHash": "sha256-X9I8POSlHxBVjD0fiX1O2j7U9Zi1+4rIkrsyHP0uHXY=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "hyprutils", "repo": "hyprutils",
"rev": "b26f33cc1c8a7fd5076e19e2cce3f062dca6351c", "rev": "006620eb29d54ea9086538891404c78563d1bae1",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -25,11 +25,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1732014248, "lastModified": 1737469691,
"narHash": "sha256-y/MEyuJ5oBWrWAic/14LaIr/u5E0wRVzyYsouYY3W6w=", "narHash": "sha256-nmKOgAU48S41dTPIXAq0AHZSehWUn6ZPrUKijHAMmIk=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "23e89b7da85c3640bbc2173fe04f4bd114342367", "rev": "9e4d5190a9482a1fb9d18adf0bdb83c6e506eaab",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <array>
namespace Hyprgraphics { namespace Hyprgraphics {
class CColor { class CColor {
public: public:
@ -18,6 +19,25 @@ namespace Hyprgraphics {
double l = 0, a = 0, b = 0; double l = 0, a = 0, b = 0;
}; };
// xy 0.0 - 1.0
struct xy {
double x = 0, y = 0;
bool operator==(const xy& p2) const {
return x == p2.x && y == p2.y;
}
};
// XYZ 0.0 - 1.0
struct XYZ {
double x = 0, y = 0, z = 0;
// per-component division
XYZ operator/(const XYZ& other) const {
return {.x = x / other.x, .y = y / other.y, .z = z / other.z};
}
};
CColor(); // black CColor(); // black
CColor(const SSRGB& rgb); CColor(const SSRGB& rgb);
CColor(const SHSL& hsl); CColor(const SHSL& hsl);
@ -27,7 +47,7 @@ namespace Hyprgraphics {
SHSL asHSL() const; SHSL asHSL() const;
SOkLab asOkLab() const; SOkLab asOkLab() const;
bool operator==(const CColor& other) const { bool operator==(const CColor& other) const {
return other.r == r && other.g == g && other.b == b; return other.r == r && other.g == g && other.b == b;
} }
@ -35,4 +55,40 @@ namespace Hyprgraphics {
// SRGB space for internal color storage // SRGB space for internal color storage
double r = 0, g = 0, b = 0; double r = 0, g = 0, b = 0;
}; };
// 3x3 matrix for CM transformations
class CMatrix3 {
public:
CMatrix3() = default;
CMatrix3(const std::array<std::array<double, 3>, 3>& values);
CMatrix3 invert() const;
CColor::XYZ operator*(const CColor::XYZ& xyz) const;
CMatrix3 operator*(const CMatrix3& other) const;
const std::array<std::array<double, 3>, 3>& mat();
static const CMatrix3& identity();
private:
std::array<std::array<double, 3>, 3> m = {
0, 0, 0, //
0, 0, 0, //
0, 0, 0, //
};
};
CColor::XYZ xy2xyz(const CColor::xy& xy);
CMatrix3 adaptWhite(const CColor::xy& src, const CColor::xy& dst);
struct SPCPRimaries {
CColor::xy red, green, blue, white;
bool operator==(const SPCPRimaries& p2) const {
return red == p2.red && green == p2.green && blue == p2.blue && white == p2.white;
}
CMatrix3 toXYZ() const; // toXYZ() * rgb -> xyz
CMatrix3 convertMatrix(const SPCPRimaries& dst) const; // convertMatrix(dst) * rgb with "this" primaries -> rgb with dst primaries
};
}; };

View file

@ -9,6 +9,7 @@
hyprutils, hyprutils,
libjpeg, libjpeg,
libjxl, libjxl,
libspng,
libwebp, libwebp,
pixman, pixman,
version ? "git", version ? "git",
@ -50,6 +51,7 @@ in
hyprutils hyprutils
libjpeg libjpeg
libjxl libjxl
libspng
libwebp libwebp
pixman pixman
]; ];

View file

@ -1,4 +1,5 @@
#include <hyprgraphics/color/Color.hpp> #include <hyprgraphics/color/Color.hpp>
#include <algorithm>
#include <cmath> #include <cmath>
using namespace Hyprgraphics; using namespace Hyprgraphics;
@ -8,7 +9,7 @@ static double gammaToLinear(const double in) {
} }
static double linearToGamma(const double in) { static double linearToGamma(const double in) {
return in >= 0.0031308 ? 1.055 * std::pow(in, 0.41666666666) - 0.055 : 12.92 * in; return in >= 0.0031308 ? (1.055 * std::pow(in, 0.41666666666)) - 0.055 : 12.92 * in;
} }
static double hueToRgb(double p, double q, double t) { static double hueToRgb(double p, double q, double t) {
@ -17,22 +18,128 @@ static double hueToRgb(double p, double q, double t) {
if (t > 1) if (t > 1)
t -= 1; t -= 1;
if (t < 1.0 / 6.0) if (t < 1.0 / 6.0)
return p + (q - p) * 6.0 * t; return p + ((q - p) * 6.0 * t);
if (t < 1.0 / 2.0) if (t < 1.0 / 2.0)
return q; return q;
if (t < 2.0 / 3.0) if (t < 2.0 / 3.0)
return p + (q - p) * (2.0 / 3.0 - t) * 6.0; return p + ((q - p) * (2.0 / 3.0 - t) * 6.0);
return p; return p;
} }
Hyprgraphics::CMatrix3::CMatrix3(const std::array<std::array<double, 3>, 3>& values) : m(values) {}
CMatrix3 Hyprgraphics::CMatrix3::invert() const {
double invDet = 1 /
(0 //
+ m[0][0] * (m[1][1] * m[2][2] - m[2][1] * m[1][2]) //
- m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) //
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]) //
);
return CMatrix3(std::array<std::array<double, 3>, 3>{
(m[1][1] * m[2][2] - m[2][1] * m[1][2]) * invDet, (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * invDet, (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * invDet, //
(m[1][2] * m[2][0] - m[1][0] * m[2][2]) * invDet, (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * invDet, (m[1][0] * m[0][2] - m[0][0] * m[1][2]) * invDet, //
(m[1][0] * m[2][1] - m[2][0] * m[1][1]) * invDet, (m[2][0] * m[0][1] - m[0][0] * m[2][1]) * invDet, (m[0][0] * m[1][1] - m[1][0] * m[0][1]) * invDet, //
});
}
CColor::XYZ Hyprgraphics::CMatrix3::operator*(const CColor::XYZ& value) const {
return CColor::XYZ{
.x = (m[0][0] * value.x) + (m[0][1] * value.y) + (m[0][2] * value.z), //
.y = (m[1][0] * value.x) + (m[1][1] * value.y) + (m[1][2] * value.z), //
.z = (m[2][0] * value.x) + (m[2][1] * value.y) + (m[2][2] * value.z), //
};
}
CMatrix3 Hyprgraphics::CMatrix3::operator*(const CMatrix3& other) const {
std::array<std::array<double, 3>, 3> res = {0, 0, 0, 0, 0, 0, 0, 0, 0};
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
for (int k = 0; k < 3; k++) {
res[i][j] += m[i][k] * other.m[k][j];
}
}
}
return CMatrix3(res);
}
const std::array<std::array<double, 3>, 3>& Hyprgraphics::CMatrix3::mat() {
return m;
};
const CMatrix3& CMatrix3::identity() {
static const CMatrix3 Identity3 = CMatrix3(std::array<std::array<double, 3>, 3>{
1, 0, 0, //
0, 1, 0, //
0, 0, 1, //
});
return Identity3;
}
CColor::XYZ Hyprgraphics::xy2xyz(const CColor::xy& xy) {
if (xy.y == 0.0)
return {.x = 0.0, .y = 0.0, .z = 0.0};
return {.x = xy.x / xy.y, .y = 1.0, .z = (1.0 - xy.x - xy.y) / xy.y};
}
static CMatrix3 Bradford = CMatrix3(std::array<std::array<double, 3>, 3>{
0.8951, 0.2664, -0.1614, //
-0.7502, 1.7135, 0.0367, //
0.0389, -0.0685, 1.0296, //
});
static CMatrix3 BradfordInv = Bradford.invert();
CMatrix3 Hyprgraphics::adaptWhite(const CColor::xy& src, const CColor::xy& dst) {
if (src == dst)
return CMatrix3::identity();
const auto srcXYZ = xy2xyz(src);
const auto dstXYZ = xy2xyz(dst);
const auto factors = (Bradford * dstXYZ) / (Bradford * srcXYZ);
return BradfordInv *
CMatrix3(std::array<std::array<double, 3>, 3>{
factors.x, 0.0, 0.0, //
0.0, factors.y, 0.0, //
0.0, 0.0, factors.z, //
}) *
Bradford;
}
CMatrix3 Hyprgraphics::SPCPRimaries::toXYZ() const {
const auto r = xy2xyz(red);
const auto g = xy2xyz(green);
const auto b = xy2xyz(blue);
const auto w = xy2xyz(white);
const auto invMat = CMatrix3(std::array<std::array<double, 3>, 3>{
r.x, g.x, b.x, //
r.y, g.y, b.y, //
r.z, g.z, b.z, //
})
.invert();
const auto s = invMat * w;
return std::array<std::array<double, 3>, 3>{
s.x * r.x, s.y * g.x, s.z * b.x, //
s.x * r.y, s.y * g.y, s.z * b.y, //
s.x * r.z, s.y * g.z, s.z * b.z, //
};
}
CMatrix3 Hyprgraphics::SPCPRimaries::convertMatrix(const SPCPRimaries& dst) const {
return dst.toXYZ().invert() * adaptWhite(white, dst.white) * toXYZ();
}
Hyprgraphics::CColor::CColor() { Hyprgraphics::CColor::CColor() {
; ;
} }
Hyprgraphics::CColor::CColor(const SSRGB& rgb) { Hyprgraphics::CColor::CColor(const SSRGB& rgb) : r(rgb.r), g(rgb.g), b(rgb.b) {
r = rgb.r; ;
g = rgb.g;
b = rgb.b;
} }
Hyprgraphics::CColor::CColor(const SHSL& hsl) { Hyprgraphics::CColor::CColor(const SHSL& hsl) {
@ -41,22 +148,22 @@ Hyprgraphics::CColor::CColor(const SHSL& hsl) {
g = hsl.l; g = hsl.l;
b = hsl.l; b = hsl.l;
} else { } else {
const double q = hsl.l < 0.5 ? hsl.l * (1.0 + hsl.s) : hsl.l + hsl.s - hsl.l * hsl.s; const double q = hsl.l < 0.5 ? hsl.l * (1.0 + hsl.s) : hsl.l + hsl.s - (hsl.l * hsl.s);
const double p = 2.0 * hsl.l - q; const double p = (2.0 * hsl.l) - q;
r = hueToRgb(p, q, hsl.h + 1.0 / 3.0); r = hueToRgb(p, q, hsl.h + (1.0 / 3.0));
g = hueToRgb(p, q, hsl.h); g = hueToRgb(p, q, hsl.h);
b = hueToRgb(p, q, hsl.h - 1.0 / 3.0); b = hueToRgb(p, q, hsl.h - (1.0 / 3.0));
} }
} }
Hyprgraphics::CColor::CColor(const SOkLab& lab) { Hyprgraphics::CColor::CColor(const SOkLab& lab) {
const double l = std::pow(lab.l + lab.a * 0.3963377774 + lab.b * 0.2158037573, 3); const double l = std::pow(lab.l + (lab.a * 0.3963377774) + (lab.b * 0.2158037573), 3);
const double m = std::pow(lab.l + lab.a * (-0.1055613458) + lab.b * (-0.0638541728), 3); const double m = std::pow(lab.l + (lab.a * (-0.1055613458)) + (lab.b * (-0.0638541728)), 3);
const double s = std::pow(lab.l + lab.a * (-0.0894841775) + lab.b * (-1.2914855480), 3); const double s = std::pow(lab.l + (lab.a * (-0.0894841775)) + (lab.b * (-1.2914855480)), 3);
r = linearToGamma(l * 4.0767416621 + m * -3.3077115913 + s * 0.2309699292); r = linearToGamma((l * 4.0767416621) + (m * -3.3077115913) + (s * 0.2309699292));
g = linearToGamma(l * (-1.2684380046) + m * 2.6097574011 + s * (-0.3413193965)); g = linearToGamma((l * (-1.2684380046)) + (m * 2.6097574011) + (s * (-0.3413193965)));
b = linearToGamma(l * (-0.0041960863) + m * (-0.7034186147) + s * 1.7076147010); b = linearToGamma((l * (-0.0041960863)) + (m * (-0.7034186147)) + (s * 1.7076147010));
} }
Hyprgraphics::CColor::SSRGB Hyprgraphics::CColor::asRgb() const { Hyprgraphics::CColor::SSRGB Hyprgraphics::CColor::asRgb() const {
@ -68,7 +175,7 @@ Hyprgraphics::CColor::SSRGB Hyprgraphics::CColor::asRgb() const {
} }
Hyprgraphics::CColor::SHSL Hyprgraphics::CColor::asHSL() const { Hyprgraphics::CColor::SHSL Hyprgraphics::CColor::asHSL() const {
const double vmax = std::max(std::max(r, g), b), vmin = std::min(std::min(r, g), b); const double vmax = std::max({r, g, b}), vmin = std::min({r, g, b});
double h = 0, s = 0, l = (vmax + vmin) / 2.0; double h = 0, s = 0, l = (vmax + vmin) / 2.0;
if (vmax == vmin) { if (vmax == vmin) {
@ -103,13 +210,13 @@ Hyprgraphics::CColor::SOkLab Hyprgraphics::CColor::asOkLab() const {
const double linG = gammaToLinear(g); const double linG = gammaToLinear(g);
const double linB = gammaToLinear(b); const double linB = gammaToLinear(b);
const double l = std::cbrtf(0.4122214708 * linR + 0.5363325363 * linG + 0.0514459929 * linB); const double l = std::cbrtf((0.4122214708 * linR) + (0.5363325363 * linG) + (0.0514459929 * linB));
const double m = std::cbrtf(0.2119034982 * linR + 0.6806995451 * linG + 0.1073969566 * linB); const double m = std::cbrtf((0.2119034982 * linR) + (0.6806995451 * linG) + (0.1073969566 * linB));
const double s = std::cbrtf(0.0883024619 * linR + 0.2817188376 * linG + 0.6299787005 * linB); const double s = std::cbrtf((0.0883024619 * linR) + (0.2817188376 * linG) + (0.6299787005 * linB));
return Hyprgraphics::CColor::SOkLab{ return Hyprgraphics::CColor::SOkLab{
.l = l * 0.2104542553 + m * 0.7936177850 + s * (-0.0040720468), .l = (l * 0.2104542553) + (m * 0.7936177850) + (s * (-0.0040720468)),
.a = l * 1.9779984951 + m * (-2.4285922050) + s * 0.4505937099, .a = (l * 1.9779984951) + (m * (-2.4285922050)) + (s * 0.4505937099),
.b = l * 0.0259040371 + m * 0.7827717662 + s * (-0.8086757660), .b = (l * 0.0259040371) + (m * 0.7827717662) + (s * (-0.8086757660)),
}; };
} }

View file

@ -1,8 +1,11 @@
#include <hyprgraphics/image/Image.hpp> #include <hyprgraphics/image/Image.hpp>
#include "formats/Bmp.hpp" #include "formats/Bmp.hpp"
#include "formats/Jpeg.hpp" #include "formats/Jpeg.hpp"
#ifdef JXL_FOUND
#include "formats/JpegXL.hpp" #include "formats/JpegXL.hpp"
#endif
#include "formats/Webp.hpp" #include "formats/Webp.hpp"
#include "formats/Png.hpp"
#include <magic.h> #include <magic.h>
#include <format> #include <format>
@ -13,41 +16,48 @@ Hyprgraphics::CImage::CImage(const std::string& path) : filepath(path) {
std::expected<cairo_surface_t*, std::string> CAIROSURFACE; std::expected<cairo_surface_t*, std::string> CAIROSURFACE;
const auto len = path.length(); const auto len = path.length();
if (path.find(".png") == len - 4 || path.find(".PNG") == len - 4) { 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"; 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) { } 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); CAIROSURFACE = JPEG::createSurfaceFromJPEG(path);
imageHasAlpha = false; imageHasAlpha = false;
mime = "image/jpeg"; mime = "image/jpeg";
} else if (path.find(".bmp") == len - 4 || path.find(".BMP") == len - 4) { } else if (path.find(".bmp") == len - 4 || path.find(".BMP") == len - 4) {
CAIROSURFACE = BMP::createSurfaceFromBMP(path); CAIROSURFACE = BMP::createSurfaceFromBMP(path);
imageHasAlpha = false; imageHasAlpha = false;
mime = "image/bmp"; mime = "image/bmp";
} else if (path.find(".webp") == len - 5 || path.find(".WEBP") == len - 5) { } else if (path.find(".webp") == len - 5 || path.find(".WEBP") == len - 5) {
CAIROSURFACE = WEBP::createSurfaceFromWEBP(path); CAIROSURFACE = WEBP::createSurfaceFromWEBP(path);
mime = "image/webp"; mime = "image/webp";
} else if (path.find(".jxl") == len - 4 || path.find(".JXL") == len - 4) { } else if (path.find(".jxl") == len - 4 || path.find(".JXL") == len - 4) {
#ifdef JXL_FOUND
CAIROSURFACE = JXL::createSurfaceFromJXL(path); CAIROSURFACE = JXL::createSurfaceFromJXL(path);
mime = "image/jxl"; mime = "image/jxl";
#else
lastError = "hyprgraphics compiled without JXL support";
return;
#endif
} else { } else {
// magic is slow, so only use it when no recognized extension is found // magic is slow, so only use it when no recognized extension is found
auto handle = magic_open(MAGIC_NONE | MAGIC_COMPRESS); auto handle = magic_open(MAGIC_NONE | MAGIC_COMPRESS | MAGIC_SYMLINK);
magic_load(handle, nullptr); magic_load(handle, nullptr);
const auto type_str = std::string(magic_file(handle, path.c_str())); const auto type_str = std::string(magic_file(handle, path.c_str()));
const auto first_word = type_str.substr(0, type_str.find(" ")); const auto first_word = type_str.substr(0, type_str.find(' '));
if (first_word == "PNG") { if (first_word == "PNG") {
CAIROSURFACE = cairo_image_surface_create_from_png(path.c_str()); CAIROSURFACE = PNG::createSurfaceFromPNG(path);
mime = "image/png"; mime = "image/png";
} else if (first_word == "JPEG") { } else if (first_word == "JPEG") {
CAIROSURFACE = JPEG::createSurfaceFromJPEG(path); CAIROSURFACE = JPEG::createSurfaceFromJPEG(path);
imageHasAlpha = false; imageHasAlpha = false;
mime = "image/jpeg"; mime = "image/jpeg";
} else if (first_word == "BMP") { } else if (first_word == "BMP") {
CAIROSURFACE = BMP::createSurfaceFromBMP(path); CAIROSURFACE = BMP::createSurfaceFromBMP(path);
imageHasAlpha = false; imageHasAlpha = false;
mime = "image/bmp"; mime = "image/bmp";
} else { } else {
lastError = "unrecognized image"; lastError = "unrecognized image";
return; return;

View file

@ -3,11 +3,12 @@
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/types.h> #include <sys/types.h>
#include <unistd.h> #include <unistd.h>
#include <cstddef>
#include <filesystem> #include <filesystem>
#include <optional> #include <optional>
#include <fstream> #include <fstream>
#include <vector> #include <vector>
#include <string.h> #include <cstring>
class BmpHeader { class BmpHeader {
public: public:
@ -34,7 +35,7 @@ class BmpHeader {
file.seekg(0, std::ios::beg); file.seekg(0, std::ios::beg);
file.read(reinterpret_cast<char*>(&format), sizeof(format)); file.read(reinterpret_cast<char*>(&format), sizeof(format));
if (!(format[0] == 66 && format[1] == 77)) if (format[0] != 66 || format[1] != 77)
return "Unable to parse bitmap header: wrong bmp file type"; return "Unable to parse bitmap header: wrong bmp file type";
file.read(reinterpret_cast<char*>(&sizeOfFile), sizeof(sizeOfFile)); file.read(reinterpret_cast<char*>(&sizeOfFile), sizeof(sizeOfFile));
@ -75,9 +76,9 @@ static void reflectImage(unsigned char* image, uint32_t numberOfRows, int stride
std::vector<unsigned char> temp; std::vector<unsigned char> temp;
temp.resize(stride); temp.resize(stride);
while (rowStart < rowEnd) { while (rowStart < rowEnd) {
memcpy(&temp[0], &image[rowStart * stride], stride); memcpy(&temp[0], &image[static_cast<size_t>(rowStart * stride)], stride);
memcpy(&image[rowStart * stride], &image[rowEnd * stride], stride); memcpy(&image[static_cast<size_t>(rowStart * stride)], &image[static_cast<size_t>(rowEnd * stride)], stride);
memcpy(&image[rowEnd * stride], &temp[0], stride); memcpy(&image[static_cast<size_t>(rowEnd * stride)], &temp[0], stride);
rowStart++; rowStart++;
rowEnd--; rowEnd--;
} }
@ -102,14 +103,14 @@ std::expected<cairo_surface_t*, std::string> BMP::createSurfaceFromBMP(const std
if (!std::filesystem::exists(path)) if (!std::filesystem::exists(path))
return std::unexpected("loading bmp: file doesn't exist"); return std::unexpected("loading bmp: file doesn't exist");
std::ifstream bitmapImageStream(path); std::ifstream bitmapImageStream(path);
BmpHeader bitmapHeader; BmpHeader bitmapHeader;
if (const auto RET = bitmapHeader.load(bitmapImageStream); RET.has_value()) if (const auto RET = bitmapHeader.load(bitmapImageStream); RET.has_value())
return std::unexpected("loading bmp: " + *RET); return std::unexpected("loading bmp: " + *RET);
cairo_format_t format = CAIRO_FORMAT_ARGB32; cairo_format_t format = CAIRO_FORMAT_ARGB32;
int stride = cairo_format_stride_for_width(format, bitmapHeader.width); int stride = cairo_format_stride_for_width(format, bitmapHeader.width);
unsigned char* imageData = (unsigned char*)malloc(bitmapHeader.height * stride); unsigned char* imageData = (unsigned char*)malloc(static_cast<size_t>(bitmapHeader.height * stride));
if (bitmapHeader.numberOfBitPerPixel == 24) if (bitmapHeader.numberOfBitPerPixel == 24)
convertRgbToArgb(bitmapImageStream, imageData, bitmapHeader.height * stride); convertRgbToArgb(bitmapImageStream, imageData, bitmapHeader.height * stride);
@ -122,4 +123,4 @@ std::expected<cairo_surface_t*, std::string> BMP::createSurfaceFromBMP(const std
bitmapImageStream.close(); bitmapImageStream.close();
reflectImage(imageData, bitmapHeader.height, stride); reflectImage(imageData, bitmapHeader.height, stride);
return cairo_image_surface_create_for_data(imageData, format, bitmapHeader.width, bitmapHeader.height, stride); return cairo_image_surface_create_for_data(imageData, format, bitmapHeader.width, bitmapHeader.height, stride);
} }

View file

@ -1,5 +1,6 @@
#include "Jpeg.hpp" #include "Jpeg.hpp"
#include <cstddef>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <vector> #include <vector>
@ -47,7 +48,7 @@ std::expected<cairo_surface_t*, std::string> JPEG::createSurfaceFromJPEG(const s
JSAMPROW rowRead; JSAMPROW rowRead;
while (decompressStruct.output_scanline < decompressStruct.output_height) { while (decompressStruct.output_scanline < decompressStruct.output_height) {
const auto PROW = CAIRODATA + (decompressStruct.output_scanline * CAIROSTRIDE); const auto PROW = CAIRODATA + (static_cast<size_t>(decompressStruct.output_scanline * CAIROSTRIDE));
rowRead = PROW; rowRead = PROW;
jpeg_read_scanlines(&decompressStruct, &rowRead, 1); jpeg_read_scanlines(&decompressStruct, &rowRead, 1);
} }

94
src/image/formats/Png.cpp Normal file
View file

@ -0,0 +1,94 @@
#include "Png.hpp"
#include <spng.h>
#include <cstddef>
#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{.width = 0};
if (int ret = spng_get_ihdr(ctx, &ihdr); ret)
return std::unexpected(std::string{"loading png: spng_get_ihdr failed: "} + spng_strerror(ret));
int fmt = SPNG_FMT_PNG;
if (ihdr.color_type == SPNG_COLOR_TYPE_INDEXED)
fmt = SPNG_FMT_RGB8;
size_t imageLength = 0;
if (int ret = spng_decoded_image_size(ctx, fmt, &imageLength); ret)
return std::unexpected(std::string{"loading png: spng_decoded_image_size failed: "} + spng_strerror(ret));
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
bool succeededDecode = false;
int ret = spng_decode_image(ctx, imageData, imageLength, SPNG_FMT_RGBA8, 0);
if (!ret)
succeededDecode = true;
if (!succeededDecode && ret == SPNG_EBUFSIZ) {
// hack, but I don't know why decoded_image_size is sometimes wrong
imageLength = static_cast<size_t>(ihdr.height * ihdr.width * 4) /* FIXME: this is wrong if we doing >32bpp!!!! */;
imageData = (uint8_t*)realloc(imageData, imageLength);
ret = spng_decode_image(ctx, imageData, imageLength, SPNG_FMT_RGBA8, 0);
}
if (!ret)
succeededDecode = true;
if (!succeededDecode) {
free(imageData);
return std::unexpected(std::string{"loading png: spng_decode_image failed: "} + spng_strerror(ret) + " (bad 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;
}

View file

@ -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&);
};

View file

@ -1,5 +1,6 @@
#include "Webp.hpp" #include "Webp.hpp"
#include <cstddef>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <webp/decode.h> #include <webp/decode.h>
@ -46,7 +47,7 @@ std::expected<cairo_surface_t*, std::string> WEBP::createSurfaceFromWEBP(const s
config.options.no_fancy_upsampling = 1; config.options.no_fancy_upsampling = 1;
config.output.u.RGBA.rgba = CAIRODATA; config.output.u.RGBA.rgba = CAIRODATA;
config.output.u.RGBA.stride = CAIROSTRIDE; config.output.u.RGBA.stride = CAIROSTRIDE;
config.output.u.RGBA.size = CAIROSTRIDE * HEIGHT; config.output.u.RGBA.size = static_cast<size_t>(CAIROSTRIDE * HEIGHT);
config.output.is_external_memory = 1; config.output.is_external_memory = 1;
config.output.width = WIDTH; config.output.width = WIDTH;
config.output.height = HEIGHT; config.output.height = HEIGHT;

View file

@ -1,3 +1,4 @@
#include <algorithm>
#include <print> #include <print>
#include <format> #include <format>
#include <filesystem> #include <filesystem>
@ -6,7 +7,7 @@
using namespace Hyprgraphics; using namespace Hyprgraphics;
bool tryLoadImage(const std::string& path) { static bool tryLoadImage(const std::string& path) {
auto image = CImage(path); auto image = CImage(path);
if (!image.success()) { if (!image.success()) {
@ -16,7 +17,17 @@ 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()); 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::ranges::replace(name, '/', '_');
//NOLINTNEXTLINE
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) { int main(int argc, char** argv, char** envp) {
@ -25,8 +36,12 @@ int main(int argc, char** argv, char** envp) {
for (auto& file : std::filesystem::directory_iterator("./resource/images/")) { for (auto& file : std::filesystem::directory_iterator("./resource/images/")) {
if (!file.is_regular_file()) if (!file.is_regular_file())
continue; continue;
auto expectation = true;
EXPECT(tryLoadImage(file.path()), true); #ifndef JXL_FOUND
if (file.path().filename() == "hyprland.jxl")
expectation = false;
#endif
EXPECT(tryLoadImage(file.path()), expectation);
} }
return ret; return ret;

View file

@ -0,0 +1 @@
hyprland.png