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: |
sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf
pacman --noconfirm --noprogressbar -Syyu
pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang libc++ pixman cairo hyprutils libjpeg-turbo libjxl libwebp
pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang libc++ pixman cairo hyprutils libjpeg-turbo libjxl libwebp libspng
- name: Build hyprgraphics with gcc
run: |
@ -44,7 +44,7 @@ jobs:
run: |
sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf
pacman --noconfirm --noprogressbar -Syyu
pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang libc++ pixman cairo hyprutils libjpeg-turbo libjxl libwebp
pacman --noconfirm --noprogressbar -Sy gcc base-devel cmake clang libc++ pixman cairo hyprutils libjpeg-turbo libjxl libwebp libspng
- name: Build hyprgraphics with clang
run: |

View file

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

2
.gitignore vendored
View file

@ -44,3 +44,5 @@ Makefile
cmake_install.cmake
compile_commands.json
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)
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)
message(STATUS "Configuring hyprgraphics in Debug")
@ -47,10 +57,22 @@ pkg_check_modules(
hyprutils
libjpeg
libwebp
libmagic
spng)
pkg_check_modules(
JXL
IMPORTED_TARGET
libjxl
libjxl_cms
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})
target_include_directories(
@ -60,6 +82,9 @@ target_include_directories(
set_target_properties(hyprgraphics PROPERTIES VERSION ${HYPRGRAPHICS_VERSION}
SOVERSION 0)
target_link_libraries(hyprgraphics PkgConfig::deps)
if(JXL_FOUND)
target_link_libraries(hyprgraphics PkgConfig::JXL)
endif()
# 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.
## 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
```sh

View file

@ -1 +1 @@
0.1.1
0.1.3

View file

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

View file

@ -1,5 +1,6 @@
#pragma once
#include <array>
namespace Hyprgraphics {
class CColor {
public:
@ -18,6 +19,25 @@ namespace Hyprgraphics {
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(const SSRGB& rgb);
CColor(const SHSL& hsl);
@ -27,7 +47,7 @@ namespace Hyprgraphics {
SHSL asHSL() 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;
}
@ -35,4 +55,40 @@ namespace Hyprgraphics {
// SRGB space for internal color storage
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,
libjpeg,
libjxl,
libspng,
libwebp,
pixman,
version ? "git",
@ -50,6 +51,7 @@ in
hyprutils
libjpeg
libjxl
libspng
libwebp
pixman
];

View file

@ -1,4 +1,5 @@
#include <hyprgraphics/color/Color.hpp>
#include <algorithm>
#include <cmath>
using namespace Hyprgraphics;
@ -8,7 +9,7 @@ static double gammaToLinear(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) {
@ -17,22 +18,128 @@ static double hueToRgb(double p, double q, double t) {
if (t > 1)
t -= 1;
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)
return q;
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;
}
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(const SSRGB& rgb) {
r = rgb.r;
g = rgb.g;
b = rgb.b;
Hyprgraphics::CColor::CColor(const SSRGB& rgb) : r(rgb.r), g(rgb.g), b(rgb.b) {
;
}
Hyprgraphics::CColor::CColor(const SHSL& hsl) {
@ -41,22 +148,22 @@ Hyprgraphics::CColor::CColor(const SHSL& hsl) {
g = hsl.l;
b = hsl.l;
} else {
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;
r = hueToRgb(p, q, hsl.h + 1.0 / 3.0);
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;
r = hueToRgb(p, q, hsl.h + (1.0 / 3.0));
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) {
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 s = std::pow(lab.l + lab.a * (-0.0894841775) + lab.b * (-1.2914855480), 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 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);
g = linearToGamma(l * (-1.2684380046) + m * 2.6097574011 + s * (-0.3413193965));
b = linearToGamma(l * (-0.0041960863) + m * (-0.7034186147) + s * 1.7076147010);
r = linearToGamma((l * 4.0767416621) + (m * -3.3077115913) + (s * 0.2309699292));
g = linearToGamma((l * (-1.2684380046)) + (m * 2.6097574011) + (s * (-0.3413193965)));
b = linearToGamma((l * (-0.0041960863)) + (m * (-0.7034186147)) + (s * 1.7076147010));
}
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 {
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;
if (vmax == vmin) {
@ -103,13 +210,13 @@ Hyprgraphics::CColor::SOkLab Hyprgraphics::CColor::asOkLab() const {
const double linG = gammaToLinear(g);
const double linB = gammaToLinear(b);
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 s = std::cbrtf(0.0883024619 * linR + 0.2817188376 * linG + 0.6299787005 * 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 s = std::cbrtf((0.0883024619 * linR) + (0.2817188376 * linG) + (0.6299787005 * linB));
return Hyprgraphics::CColor::SOkLab{
.l = l * 0.2104542553 + m * 0.7936177850 + s * (-0.0040720468),
.a = l * 1.9779984951 + m * (-2.4285922050) + s * 0.4505937099,
.b = l * 0.0259040371 + m * 0.7827717662 + s * (-0.8086757660),
.l = (l * 0.2104542553) + (m * 0.7936177850) + (s * (-0.0040720468)),
.a = (l * 1.9779984951) + (m * (-2.4285922050)) + (s * 0.4505937099),
.b = (l * 0.0259040371) + (m * 0.7827717662) + (s * (-0.8086757660)),
};
}

View file

@ -1,8 +1,11 @@
#include <hyprgraphics/image/Image.hpp>
#include "formats/Bmp.hpp"
#include "formats/Jpeg.hpp"
#ifdef JXL_FOUND
#include "formats/JpegXL.hpp"
#endif
#include "formats/Webp.hpp"
#include "formats/Png.hpp"
#include <magic.h>
#include <format>
@ -13,41 +16,48 @@ 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";
CAIROSURFACE = PNG::createSurfaceFromPNG(path);
mime = "image/png";
} else if (path.find(".jpg") == len - 4 || path.find(".JPG") == len - 4 || path.find(".jpeg") == len - 5 || path.find(".JPEG") == len - 5) {
CAIROSURFACE = JPEG::createSurfaceFromJPEG(path);
imageHasAlpha = false;
mime = "image/jpeg";
mime = "image/jpeg";
} else if (path.find(".bmp") == len - 4 || path.find(".BMP") == len - 4) {
CAIROSURFACE = BMP::createSurfaceFromBMP(path);
imageHasAlpha = false;
mime = "image/bmp";
mime = "image/bmp";
} else if (path.find(".webp") == len - 5 || path.find(".WEBP") == len - 5) {
CAIROSURFACE = WEBP::createSurfaceFromWEBP(path);
mime = "image/webp";
mime = "image/webp";
} else if (path.find(".jxl") == len - 4 || path.find(".JXL") == len - 4) {
#ifdef JXL_FOUND
CAIROSURFACE = JXL::createSurfaceFromJXL(path);
mime = "image/jxl";
mime = "image/jxl";
#else
lastError = "hyprgraphics compiled without JXL support";
return;
#endif
} else {
// 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);
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") {
CAIROSURFACE = cairo_image_surface_create_from_png(path.c_str());
mime = "image/png";
CAIROSURFACE = PNG::createSurfaceFromPNG(path);
mime = "image/png";
} else if (first_word == "JPEG") {
CAIROSURFACE = JPEG::createSurfaceFromJPEG(path);
imageHasAlpha = false;
mime = "image/jpeg";
mime = "image/jpeg";
} else if (first_word == "BMP") {
CAIROSURFACE = BMP::createSurfaceFromBMP(path);
imageHasAlpha = false;
mime = "image/bmp";
mime = "image/bmp";
} else {
lastError = "unrecognized image";
return;

View file

@ -3,11 +3,12 @@
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <cstddef>
#include <filesystem>
#include <optional>
#include <fstream>
#include <vector>
#include <string.h>
#include <cstring>
class BmpHeader {
public:
@ -34,7 +35,7 @@ class BmpHeader {
file.seekg(0, std::ios::beg);
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";
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;
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);
memcpy(&temp[0], &image[static_cast<size_t>(rowStart * stride)], stride);
memcpy(&image[static_cast<size_t>(rowStart * stride)], &image[static_cast<size_t>(rowEnd * stride)], stride);
memcpy(&image[static_cast<size_t>(rowEnd * stride)], &temp[0], stride);
rowStart++;
rowEnd--;
}
@ -102,14 +103,14 @@ std::expected<cairo_surface_t*, std::string> BMP::createSurfaceFromBMP(const std
if (!std::filesystem::exists(path))
return std::unexpected("loading bmp: file doesn't exist");
std::ifstream bitmapImageStream(path);
BmpHeader bitmapHeader;
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);
unsigned char* imageData = (unsigned char*)malloc(static_cast<size_t>(bitmapHeader.height * stride));
if (bitmapHeader.numberOfBitPerPixel == 24)
convertRgbToArgb(bitmapImageStream, imageData, bitmapHeader.height * stride);

View file

@ -1,5 +1,6 @@
#include "Jpeg.hpp"
#include <cstddef>
#include <filesystem>
#include <fstream>
#include <vector>
@ -47,7 +48,7 @@ std::expected<cairo_surface_t*, std::string> JPEG::createSurfaceFromJPEG(const s
JSAMPROW rowRead;
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;
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 <cstddef>
#include <filesystem>
#include <fstream>
#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.output.u.RGBA.rgba = CAIRODATA;
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.width = WIDTH;
config.output.height = HEIGHT;

View file

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

View file

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