diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 8cbe3e9..2717ca2 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -68,5 +68,17 @@ if(COLORCPP_BUILD_EXAMPLES) add_executable(harmony_example harmony_example.cpp) target_link_libraries(harmony_example colorcpp::colorcpp) - + + add_executable(adobe_rgb_example adobe_rgb_example.cpp) + target_link_libraries(adobe_rgb_example colorcpp::colorcpp) + + add_executable(chromatic_adaptation_example chromatic_adaptation_example.cpp) + target_link_libraries(chromatic_adaptation_example colorcpp::colorcpp) + + add_executable(prophoto_rgb_example prophoto_rgb_example.cpp) + target_link_libraries(prophoto_rgb_example colorcpp::colorcpp) + + add_executable(rec2020_example rec2020_example.cpp) + target_link_libraries(rec2020_example colorcpp::colorcpp) + endif() diff --git a/examples/adobe_rgb_example.cpp b/examples/adobe_rgb_example.cpp new file mode 100644 index 0000000..dde94a3 --- /dev/null +++ b/examples/adobe_rgb_example.cpp @@ -0,0 +1,75 @@ +/** + * @file adobe_rgb_example.cpp + * @brief Demonstrates Adobe RGB (A98-RGB) wide-gamut color space usage. + * + * Build: cmake -DCOLORCPP_BUILD_EXAMPLES=ON && make adobe_rgb_example + */ + +#include +#include + +using namespace colorcpp; +using namespace colorcpp::io::ansi; + +int main() { + std::cout << "=== Adobe RGB (A98-RGB) Wide-Gamut Color Space ===\n\n"; + + // 1. Construct Adobe RGB colors + std::cout << "1. Construction\n"; + core::adobe_rgbf_t pure_red(1.0f, 0.0f, 0.0f); + core::adobe_rgbf_t pure_green(0.0f, 1.0f, 0.0f); + core::adobe_rgbf_t pure_blue(0.0f, 0.0f, 1.0f); + std::cout << " Red: " << pure_red << "\n"; + std::cout << " Green: " << pure_green << "\n"; + std::cout << " Blue: " << pure_blue << "\n\n"; + + // 2. Convert sRGB → Adobe RGB → sRGB (show gamut differences) + std::cout << "2. sRGB ↔ Adobe RGB round-trip\n"; + core::rgbf_t srgb_green(0.0f, 1.0f, 0.0f); + auto adobe_green = conversion::color_cast(srgb_green); + auto back_to_srgb = conversion::color_cast(adobe_green); + std::cout << " sRGB green: " << srgb_green << "\n"; + std::cout << " → Adobe RGB: " << adobe_green << "\n"; + std::cout << " → back to sRGB: " << back_to_srgb << "\n\n"; + + // 3. Gamma encoding: show linear values + std::cout << "3. Gamma encoding (Adobe RGB → Linear Adobe RGB)\n"; + auto linear_adobe = conversion::color_cast(adobe_green); + std::cout << " Adobe RGB (gamma): " << adobe_green << "\n"; + std::cout << " Linear Adobe RGB: " << linear_adobe << "\n"; + std::cout << " (Pure gamma 2.2 — mid-tones are darker in linear)\n\n"; + + // 4. Cross-space chain: Adobe RGB → Display P3 → Rec.2020 → OKLab + std::cout << "4. Cross-space chain: Adobe RGB → Display P3 → Rec.2020 → OKLab\n"; + core::adobe_rgbf_t test_color(0.7f, 0.2f, 0.4f); + auto dp3 = conversion::color_cast(test_color); + auto r2020 = conversion::color_cast(dp3); + auto oklab = conversion::color_cast(r2020); + std::cout << " Input (Adobe RGB): " << test_color << "\n"; + std::cout << " → Display P3: " << dp3 << "\n"; + std::cout << " → Rec.2020: " << r2020 << "\n"; + std::cout << " → OKLab: " << oklab << "\n\n"; + + // 5. CSS parsing + std::cout << "5. CSS color(a98-rgb ...) parsing\n"; + auto css = colorcpp::io::css::parse_css_color("color(a98-rgb 0.64 0.33 0.21)"); + if (css) { + std::cout << " Parsed: " << *css << "\n"; + std::cout << " CSS string: " + << colorcpp::io::css::to_css_color_string(conversion::color_cast(*css)) << "\n"; + } + std::cout << "\n"; + + // 6. ANSI swatch + std::cout << "6. Color swatches\n"; + std::cout << " Adobe Red "; + print_swatch(std::cout, conversion::color_cast(core::adobe_rgbf_t{0.64f, 0.1f, 0.1f})); + std::cout << "\n Adobe Green"; + print_swatch(std::cout, conversion::color_cast(core::adobe_rgbf_t{0.1f, 0.64f, 0.1f})); + std::cout << "\n Adobe Blue "; + print_swatch(std::cout, conversion::color_cast(core::adobe_rgbf_t{0.1f, 0.1f, 0.64f})); + std::cout << "\n"; + + std::cout << "Done.\n"; + return 0; +} diff --git a/examples/ansi_example.cpp b/examples/ansi_example.cpp index 03d78be..c4e63b3 100644 --- a/examples/ansi_example.cpp +++ b/examples/ansi_example.cpp @@ -3,8 +3,8 @@ * @brief Example: ANSI terminal color utilities for debugging and visualization. */ -#include #include +#include #include using namespace colorcpp; diff --git a/examples/blend_example.cpp b/examples/blend_example.cpp index b2a8c5f..1543a54 100644 --- a/examples/blend_example.cpp +++ b/examples/blend_example.cpp @@ -7,7 +7,8 @@ #include #include -// implementation note: examples reflect the current encoded rgbaf_t working-space path described in docs/reference/blend.rst +// implementation note: examples reflect the current encoded rgbaf_t working-space path described in +// docs/reference/blend.rst using namespace colorcpp; using namespace colorcpp::operations::conversion; diff --git a/examples/chromatic_adaptation_example.cpp b/examples/chromatic_adaptation_example.cpp new file mode 100644 index 0000000..5179f1b --- /dev/null +++ b/examples/chromatic_adaptation_example.cpp @@ -0,0 +1,72 @@ +/** + * @file chromatic_adaptation_example.cpp + * @brief Demonstrates chromatic adaptation (white point conversion). + * + * Build: cmake -DCOLORCPP_BUILD_EXAMPLES=ON && make chromatic_adaptation_example + */ + +#include +#include + +using namespace colorcpp; + +int main() { + std::cout << "=== Chromatic Adaptation (White Point Conversion) ===\n\n"; + + using namespace algorithms::chromatic_adaptation; + + // 1. White point constants + std::cout << "1. Standard white points (CIE XYZ, Y=1)\n"; + std::cout << " D65 (noon daylight): " << WHITEPOINT_D65 << "\n"; + std::cout << " D50 (horizon daylight): " << WHITEPOINT_D50 << "\n"; + std::cout << " A (tungsten): " << WHITEPOINT_A << "\n"; + std::cout << " D55 (mid-day): " << WHITEPOINT_D55 << "\n\n"; + + // 2. Bradford D65 → D50 adaptation + std::cout << "2. Bradford adaptation: D65 → D50\n"; + core::xyz_t color_d65(0.5f, 0.4f, 0.3f); + auto color_d50 = bradford_adapt(color_d65, WHITEPOINT_D65, WHITEPOINT_D50); + std::cout << " Color under D65: " << color_d65 << "\n"; + std::cout << " → under D50: " << color_d50 << "\n\n"; + + // 3. Round-trip verification + std::cout << "3. Round-trip: D65 → D50 → D65\n"; + auto back_to_d65 = bradford_adapt(color_d50, WHITEPOINT_D50, WHITEPOINT_D65); + std::cout << " Original: " << color_d65 << "\n"; + std::cout << " After D65→D50→D65: " << back_to_d65 << "\n"; + std::cout << " (Should match original within floating-point precision)\n\n"; + + // 4. Tungsten (A) simulation — warm color shift + std::cout << "4. Daylight → Tungsten (D65 → A) — warm shift\n"; + core::rgbf_t white(1.0f, 1.0f, 1.0f); + auto warm_white = bradford_adapt(white, WHITEPOINT_D65, WHITEPOINT_A); + std::cout << " White under D65: " << white << "\n"; + std::cout << " → under Tungsten A: " << warm_white << "\n"; + std::cout << " (Expect warm/yellowish shift)\n\n"; + + // 5. Von Kries comparison + std::cout << "5. Bradford vs Von Kries (D65 → A)\n"; + core::xyz_t test(0.3f, 0.5f, 0.2f); + auto brad = bradford_adapt(test, WHITEPOINT_D65, WHITEPOINT_A); + auto vk = von_kries_adapt(test, WHITEPOINT_D65, WHITEPOINT_A); + std::cout << " Original: " << test << "\n"; + std::cout << " Bradford(D65→A): " << brad << "\n"; + std::cout << " Von Kries(D65→A): " << vk << "\n"; + std::cout << " (Bradford is more perceptually accurate)\n\n"; + + // 6. ANSI swatches + std::cout << "6. Visual comparison\n"; + auto white_d65_rgb = conversion::color_cast(core::rgbf_t{1.0f, 1.0f, 1.0f}); + std::cout << " D65 white "; + colorcpp::io::ansi::print_swatch(std::cout, white_d65_rgb); + std::cout << "\n"; + + auto tungsten_rgb = conversion::color_cast( + bradford_adapt(core::rgbf_t{1.0f, 1.0f, 1.0f}, WHITEPOINT_D65, WHITEPOINT_A)); + std::cout << " Tungsten A "; + colorcpp::io::ansi::print_swatch(std::cout, tungsten_rgb); + std::cout << "\n\n"; + + std::cout << "Done.\n"; + return 0; +} diff --git a/examples/compare_example.cpp b/examples/compare_example.cpp index c7f7783..cbeec83 100644 --- a/examples/compare_example.cpp +++ b/examples/compare_example.cpp @@ -99,8 +99,8 @@ int main() { << compare::perceptual_equal_de2000(ui_rgb, converted_rgb, 0.05f) << '\n'; rgbaf_t alpha_a{0.5f, 0.25f, 0.75f, 1.0f}; rgbaf_t alpha_b{0.5f, 0.25f, 0.75f, 0.1f}; - std::cout << " perceptual_equal(rgba alpha differs, 0.001): " << compare::perceptual_equal(alpha_a, alpha_b, 0.001f) - << '\n'; + std::cout << " perceptual_equal(rgba alpha differs, 0.001): " + << compare::perceptual_equal(alpha_a, alpha_b, 0.001f) << '\n'; std::cout << " exact_equal(rgba alpha differs): " << compare::exact_equal(alpha_a, alpha_b) << '\n'; std::cout << " note: perceptual compare converts through CIELAB, so alpha is currently ignored\n"; diff --git a/examples/prophoto_rgb_example.cpp b/examples/prophoto_rgb_example.cpp new file mode 100644 index 0000000..c407319 --- /dev/null +++ b/examples/prophoto_rgb_example.cpp @@ -0,0 +1,70 @@ +/** + * @file prophoto_rgb_example.cpp + * @brief Demonstrates ProPhoto RGB (ROMM RGB) wide-gamut color space usage. + * + * Build: cmake -DCOLORCPP_BUILD_EXAMPLES=ON && make prophoto_rgb_example + */ + +#include +#include + +using namespace colorcpp; + +int main() { + std::cout << "=== ProPhoto RGB (ROMM RGB) Wide-Gamut Color Space ===\n\n"; + + // 1. Construct ProPhoto RGB colors + std::cout << "1. Construction\n"; + core::prophoto_rgbf_t red(0.8f, 0.1f, 0.1f); + core::prophoto_rgbf_t green(0.1f, 0.8f, 0.1f); + core::prophoto_rgbf_t blue(0.1f, 0.1f, 0.8f); + std::cout << " Red: " << red << "\n"; + std::cout << " Green: " << green << "\n"; + std::cout << " Blue: " << blue << "\n\n"; + + // 2. sRGB → ProPhoto → sRGB (gamut expansion) + std::cout << "2. sRGB ↔ ProPhoto RGB round-trip\n"; + core::rgbf_t srgb_red(1.0f, 0.0f, 0.0f); + auto pprgb = conversion::color_cast(srgb_red); + auto back = conversion::color_cast(pprgb); + std::cout << " sRGB red: " << srgb_red << "\n"; + std::cout << " → ProPhoto RGB: " << pprgb << "\n"; + std::cout << " → back to sRGB: " << back << "\n\n"; + + // 3. D50 white point / chromatic adaptation + std::cout << "3. Chromatic adaptation (D50 ↔ D65)\n"; + core::xyz_t white_d65 = algorithms::chromatic_adaptation::WHITEPOINT_D65; + core::xyz_t white_d50 = algorithms::chromatic_adaptation::WHITEPOINT_D50; + std::cout << " D65 white: " << white_d65 << "\n"; + std::cout << " D50 white: " << white_d50 << "\n"; + auto d65_to_d50 = algorithms::chromatic_adaptation::bradford_adapt(white_d65, white_d65, white_d50); + std::cout << " Bradford(D65→D50): " << d65_to_d50 << "\n\n"; + + // 4. Gamut check + std::cout << "4. Gamut membership\n"; + core::rgbf_t in_gamut(0.5f, 0.5f, 0.5f); + std::cout << " Gray 0.5 in ProPhoto? " << std::boolalpha << algorithms::gamut::is_in_prophoto_gamut(in_gamut) + << "\n"; + // ProPhoto is huge — most sRGB colors are well inside it + std::cout << " sRGB red in ProPhoto? " << algorithms::gamut::is_in_prophoto_gamut(srgb_red) << "\n\n"; + + // 5. CSS parsing + std::cout << "5. CSS color(prophoto-rgb ...) parsing\n"; + auto css = colorcpp::io::css::parse_css_color("color(prophoto-rgb 0.5 0.3 0.7 / 0.9)"); + if (css) { + std::cout << " Parsed: " << *css << "\n"; + std::cout << " CSS string: " + << colorcpp::io::css::to_css_color_string(conversion::color_cast(*css)) << "\n"; + } + std::cout << "\n"; + + // 6. Gamma encoding + std::cout << "6. Gamma encoding (Gamma 1.8 with linear segment)\n"; + auto linear_pp = conversion::color_cast(core::prophoto_rgbf_t{0.5f, 0.5f, 0.5f}); + std::cout << " ProPhoto (gamma): " << core::prophoto_rgbf_t{0.5f, 0.5f, 0.5f} << "\n"; + std::cout << " Linear ProPhoto: " << linear_pp << "\n"; + std::cout << " (Gamma 1.8 is gentler than sRGB's ~2.2)\n\n"; + + std::cout << "Done.\n"; + return 0; +} diff --git a/examples/rec2020_example.cpp b/examples/rec2020_example.cpp new file mode 100644 index 0000000..9500d45 --- /dev/null +++ b/examples/rec2020_example.cpp @@ -0,0 +1,75 @@ +/** + * @file rec2020_example.cpp + * @brief Demonstrates Rec.2020 (BT.2020) wide-gamut color space usage. + * + * Build: cmake -DCOLORCPP_BUILD_EXAMPLES=ON && make rec2020_example + */ + +#include +#include + +using namespace colorcpp; + +int main() { + std::cout << "=== Rec.2020 (BT.2020) Wide-Gamut Color Space ===\n\n"; + + // 1. Construct Rec.2020 colors + std::cout << "1. Construction\n"; + core::rec2020_rgbf_t red(0.8f, 0.1f, 0.1f); + core::rec2020_rgbf_t green(0.1f, 0.8f, 0.1f); + core::rec2020_rgbf_t blue(0.1f, 0.1f, 0.8f); + std::cout << " Red: " << red << "\n"; + std::cout << " Green: " << green << "\n"; + std::cout << " Blue: " << blue << "\n\n"; + + // 2. Gamut hierarchy: sRGB ⊂ Display P3 ⊂ Adobe RGB ⊂ Rec.2020 ⊂ ProPhoto + std::cout << "2. Gamut hierarchy comparison\n"; + core::rgbf_t test_color(0.0f, 1.0f, 0.0f); // sRGB pure green + + auto dp3 = conversion::color_cast(test_color); + auto argb = conversion::color_cast(test_color); + auto r2020 = conversion::color_cast(test_color); + auto pprgb = conversion::color_cast(test_color); + + std::cout << " sRGB green:\n"; + std::cout << " -> Display P3: " << dp3 << "\n"; + std::cout << " -> Adobe RGB: " << argb << "\n"; + std::cout << " -> Rec.2020: " << r2020 << "\n"; + std::cout << " -> ProPhoto: " << pprgb << "\n"; + std::cout << " (Notice how the same sRGB green maps to different values\n"; + std::cout << " in each space due to different primaries)\n\n"; + + // 3. Round-trip fidelity + std::cout << "3. Rec.2020 round-trip fidelity\n"; + core::rec2020_rgbf_t orig(0.5f, 0.3f, 0.7f); + auto via_srgb = conversion::color_cast(conversion::color_cast(orig)); + std::cout << " Original: " << orig << "\n"; + std::cout << " Via sRGB: " << via_srgb << "\n\n"; + + // 4. Transfer function + std::cout << "4. BT.709-style piecewise transfer function\n"; + core::rec2020_rgbf_t mid(0.5f, 0.5f, 0.5f); + auto linear = conversion::color_cast(mid); + std::cout << " Rec.2020 (gamma): " << mid << "\n"; + std::cout << " Linear Rec.2020: " << linear << "\n"; + std::cout << " (alpha=1.0993, beta=0.0181 -- slightly different from sRGB)\n\n"; + + // 5. CSS parsing + std::cout << "5. CSS color(rec2020 ...) parsing\n"; + using namespace colorcpp::io::css; + auto css = parse_css_color("color(rec2020 0.64 0.33 0.21)"); + if (css) { + std::cout << " Parsed: " << *css << "\n"; + std::cout << " CSS string (sRGB fallback): " << to_css_color_string(conversion::color_cast(*css)) + << "\n"; + } + std::cout << "\n"; + + // 6. Rec.2020 is widely used in UHD/HDR + std::cout << "6. Rec.2020 fact\n"; + std::cout << " Rec.2020 covers ~75% of CIE 1931 visible gamut.\n"; + std::cout << " It is the standard color space for 4K/8K UHDTV.\n\n"; + + std::cout << "Done.\n"; + return 0; +} diff --git a/include/colorcpp/algorithms/algorithms.hpp b/include/colorcpp/algorithms/algorithms.hpp index 6bca446..920b4bb 100644 --- a/include/colorcpp/algorithms/algorithms.hpp +++ b/include/colorcpp/algorithms/algorithms.hpp @@ -1,7 +1,7 @@ /** * @file algorithms.hpp - * @brief Aggregates color algorithms: accessibility, color temperature, delta E, gamut mapping, gradient, harmony, - * vision simulation. + * @brief Aggregates color algorithms: accessibility, chromatic adaptation, color temperature, delta E, gamut mapping, + * gradient, harmony, vision simulation. * * @par Color difference * - @ref delta_e::delta_e_76 / @ref delta_e::delta_e_94 / @ref delta_e::delta_e_2000: **CIELAB D65** metrics. @@ -12,6 +12,7 @@ #pragma once #include +#include #include #include #include diff --git a/include/colorcpp/algorithms/chromatic_adaptation.hpp b/include/colorcpp/algorithms/chromatic_adaptation.hpp new file mode 100644 index 0000000..607bd7e --- /dev/null +++ b/include/colorcpp/algorithms/chromatic_adaptation.hpp @@ -0,0 +1,184 @@ +/** + * @file chromatic_adaptation.hpp + * @brief Chromatic adaptation transforms (Bradford, Von Kries) and standard white point constants. + * + * Chromatic adaptation converts colors from one white point (illuminant) to another. + * This is essential when working with color spaces that use different reference whites + * (e.g., ProPhoto RGB uses D50, most others use D65). + * + * Bradford is the recommended method for most use cases (used in ICC profiles). + * Von Kries is simpler but less perceptually accurate. + * + * @see http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html + */ + +#pragma once + +#include +#include +#include +namespace colorcpp::operations::conversion { +// Forward declaration — full definition in conversion.hpp +template +constexpr To color_cast(const From& src); +} // namespace colorcpp::operations::conversion + +namespace colorcpp::algorithms::chromatic_adaptation { + +// ============================================================================= +// Standard White Points (CIE 1931 XYZ tristimulus, Y = 1.0) +// ============================================================================= + +/// D65 — CIE Standard Illuminant (noon daylight, correlated color temperature ~6504 K) +inline constexpr core::xyz_t WHITEPOINT_D65{0.95047f, 1.00000f, 1.08883f}; + +/// D50 — CIE Standard Illuminant (horizon daylight, CCT ~5003 K) +inline constexpr core::xyz_t WHITEPOINT_D50{0.96422f, 1.00000f, 0.82521f}; + +/// D55 — CIE Standard Illuminant (mid-morning/mid-afternoon daylight, CCT ~5503 K) +inline constexpr core::xyz_t WHITEPOINT_D55{0.95682f, 1.00000f, 0.92149f}; + +/// A — CIE Standard Illuminant (tungsten filament, CCT ~2856 K) +inline constexpr core::xyz_t WHITEPOINT_A{1.09850f, 1.00000f, 0.35585f}; + +/// C — CIE Standard Illuminant (filtered tungsten / average daylight, CCT ~6774 K) +inline constexpr core::xyz_t WHITEPOINT_C{0.98074f, 1.00000f, 1.18232f}; + +// ============================================================================= +// Von Kries Chromatic Adaptation (diagonal scaling in LMS space) +// ============================================================================= + +namespace details { + +// Von Kries LMS matrix (Hunt-Pointer-Estevez, normalized to D65) +inline constexpr float M_VK[9] = { + 0.40024f, 0.70760f, -0.08081f, -0.22630f, 1.16532f, 0.04570f, 0.00000f, 0.00000f, 0.91822f, +}; + +// Inverse Von Kries LMS matrix +inline constexpr float M_VK_inv[9] = { + 1.8599364f, -1.1293816f, 0.2198974f, 0.3611914f, 0.6388125f, -0.0000064f, 0.0000000f, 0.0000000f, 1.0890636f, +}; + +} // namespace details + +/** + * @brief Von Kries chromatic adaptation transform. + * + * Converts a color from @p src_white to @p dst_white using diagonal scaling + * in Hunt-Pointer-Estevez LMS cone response space. Simpler and faster than + * Bradford, but less perceptually uniform for large white point shifts. + * + * @tparam ColorT Any color type supported by conversion::color_cast to/from xyz_t. + * @param color Input color under src_white illuminant. + * @param src_white XYZ tristimulus of the source white point. + * @param dst_white XYZ tristimulus of the destination white point. + * @return Color adapted to dst_white illuminant. + */ +template +ColorT von_kries_adapt(const ColorT& color, const core::xyz_t& src_white, const core::xyz_t& dst_white) { + using namespace operations::conversion; + + auto src = color_cast(color); + + // Compute LMS of source and destination whites + float lms_src[3], lms_dst[3]; + for (int i = 0; i < 3; ++i) { + lms_src[i] = details::M_VK[i * 3 + 0] * (&src_white.x())[0] + details::M_VK[i * 3 + 1] * (&src_white.y())[0] + + details::M_VK[i * 3 + 2] * (&src_white.z())[0]; + lms_dst[i] = details::M_VK[i * 3 + 0] * (&dst_white.x())[0] + details::M_VK[i * 3 + 1] * (&dst_white.y())[0] + + details::M_VK[i * 3 + 2] * (&dst_white.z())[0]; + } + + // Convert source color to LMS + float lms[3]; + for (int i = 0; i < 3; ++i) { + lms[i] = + details::M_VK[i * 3 + 0] * src.x() + details::M_VK[i * 3 + 1] * src.y() + details::M_VK[i * 3 + 2] * src.z(); + } + + // Apply diagonal scaling + for (int i = 0; i < 3; ++i) { + if (lms_src[i] != 0.0f) { + lms[i] *= lms_dst[i] / lms_src[i]; + } + } + + // Convert back to XYZ + float x = details::M_VK_inv[0] * lms[0] + details::M_VK_inv[1] * lms[1] + details::M_VK_inv[2] * lms[2]; + float y = details::M_VK_inv[3] * lms[0] + details::M_VK_inv[4] * lms[1] + details::M_VK_inv[5] * lms[2]; + float z = details::M_VK_inv[6] * lms[0] + details::M_VK_inv[7] * lms[1] + details::M_VK_inv[8] * lms[2]; + + return color_cast(core::xyz_t{x, y, z}); +} + +// ============================================================================= +// Bradford Chromatic Adaptation (recommended for most use cases) +// ============================================================================= + +namespace details { + +// Bradford LMS matrix (from Bruce Lindbloom) +inline constexpr float M_BFD[9] = { + 0.8951000f, 0.2664000f, -0.1614000f, -0.7502000f, 1.7135000f, 0.0367000f, 0.0389000f, -0.0685000f, 1.0296000f, +}; + +// Inverse Bradford LMS matrix +inline constexpr float M_BFD_inv[9] = { + 0.9869929f, -0.1470543f, 0.1599627f, 0.4323053f, 0.5183603f, 0.0492912f, -0.0085287f, 0.0400428f, 0.9684867f, +}; + +} // namespace details + +/** + * @brief Bradford chromatic adaptation transform. + * + * Converts a color from @p src_white to @p dst_white using the Bradford + * LMS cone response matrix. This is the recommended method for most use + * cases — it is used in ICC profiles and provides perceptually accurate + * results across a wide range of white point shifts. + * + * @tparam ColorT Any color type supported by conversion::color_cast to/from xyz_t. + * @param color Input color under src_white illuminant. + * @param src_white XYZ tristimulus of the source white point. + * @param dst_white XYZ tristimulus of the destination white point. + * @return Color adapted to dst_white illuminant. + */ +template +ColorT bradford_adapt(const ColorT& color, const core::xyz_t& src_white, const core::xyz_t& dst_white) { + using namespace operations::conversion; + + auto src = color_cast(color); + + // Compute LMS of source and destination whites + float lms_src[3], lms_dst[3]; + for (int i = 0; i < 3; ++i) { + lms_src[i] = details::M_BFD[i * 3 + 0] * (&src_white.x())[0] + details::M_BFD[i * 3 + 1] * (&src_white.y())[0] + + details::M_BFD[i * 3 + 2] * (&src_white.z())[0]; + lms_dst[i] = details::M_BFD[i * 3 + 0] * (&dst_white.x())[0] + details::M_BFD[i * 3 + 1] * (&dst_white.y())[0] + + details::M_BFD[i * 3 + 2] * (&dst_white.z())[0]; + } + + // Convert source color to LMS + float lms[3]; + for (int i = 0; i < 3; ++i) { + lms[i] = + details::M_BFD[i * 3 + 0] * src.x() + details::M_BFD[i * 3 + 1] * src.y() + details::M_BFD[i * 3 + 2] * src.z(); + } + + // Apply diagonal scaling + for (int i = 0; i < 3; ++i) { + if (lms_src[i] != 0.0f) { + lms[i] *= lms_dst[i] / lms_src[i]; + } + } + + // Convert back to XYZ + float x = details::M_BFD_inv[0] * lms[0] + details::M_BFD_inv[1] * lms[1] + details::M_BFD_inv[2] * lms[2]; + float y = details::M_BFD_inv[3] * lms[0] + details::M_BFD_inv[4] * lms[1] + details::M_BFD_inv[5] * lms[2]; + float z = details::M_BFD_inv[6] * lms[0] + details::M_BFD_inv[7] * lms[1] + details::M_BFD_inv[8] * lms[2]; + + return color_cast(core::xyz_t{x, y, z}); +} + +} // namespace colorcpp::algorithms::chromatic_adaptation diff --git a/include/colorcpp/algorithms/delta_e/helpers.hpp b/include/colorcpp/algorithms/delta_e/helpers.hpp index c8aa9f3..ef6930b 100644 --- a/include/colorcpp/algorithms/delta_e/helpers.hpp +++ b/include/colorcpp/algorithms/delta_e/helpers.hpp @@ -60,7 +60,8 @@ bool is_visually_same_cmc(const ColorA& a, const ColorB& b, float threshold = 1. * @return true if colors are visually the same by DIN99 metric. */ template -bool is_visually_same_din99(const ColorA& a, const ColorB& b, float threshold = 1.0f, float k_L = 1.0f, float k_E = 1.0f) { +bool is_visually_same_din99(const ColorA& a, const ColorB& b, float threshold = 1.0f, float k_L = 1.0f, + float k_E = 1.0f) { return delta_e_din99(a, b, k_L, k_E) < threshold; } diff --git a/include/colorcpp/algorithms/gamut/details.hpp b/include/colorcpp/algorithms/gamut/details.hpp index 08df1f3..e517373 100644 --- a/include/colorcpp/algorithms/gamut/details.hpp +++ b/include/colorcpp/algorithms/gamut/details.hpp @@ -87,4 +87,24 @@ inline void linrgb_to_adobergb(float lr, float lg, float lb, float& r, float& g, b = std::pow(std::clamp(lb, 0.0f, 1.0f), inv_gamma); } +// ProPhoto RGB → linear RGB (D50 white point via conversion) +inline void prophoto_to_linrgb(float r, float g, float b, float& lr, float& lg, float& lb) noexcept { + // ProPhoto uses gamma 1.8 with linear segment below 1/512 + auto gamma_decode = [](float v) noexcept { return (v < (1.0f / 32.0f)) ? (v / 16.0f) : std::pow(v, 1.8f); }; + lr = gamma_decode(r); + lg = gamma_decode(g); + lb = gamma_decode(b); +} + +// linear RGB → ProPhoto RGB +inline void linrgb_to_prophoto(float lr, float lg, float lb, float& r, float& g, float& b) noexcept { + auto gamma_encode = [](float v) noexcept { + v = std::clamp(v, 0.0f, 1.0f); + return (v < (1.0f / 512.0f)) ? (v * 16.0f) : std::pow(v, 1.0f / 1.8f); + }; + r = gamma_encode(lr); + g = gamma_encode(lg); + b = gamma_encode(lb); +} + } // namespace colorcpp::algorithms::gamut::details diff --git a/include/colorcpp/algorithms/gamut/multi_colorspace.hpp b/include/colorcpp/algorithms/gamut/multi_colorspace.hpp index 1a2ba6c..5b93e05 100644 --- a/include/colorcpp/algorithms/gamut/multi_colorspace.hpp +++ b/include/colorcpp/algorithms/gamut/multi_colorspace.hpp @@ -16,9 +16,10 @@ namespace colorcpp::algorithms::gamut { * @brief Gamut types supported for checking and clipping. */ enum class gamut_type { - srgb, ///< Standard RGB (sRGB) - display_p3, ///< Display P3 (wide gamut) - adobe_rgb, ///< Adobe RGB (wide gamut) + srgb, ///< Standard RGB (sRGB) + display_p3, ///< Display P3 (wide gamut) + adobe_rgb, ///< Adobe RGB (wide gamut) + prophoto_rgb, ///< ProPhoto RGB (widest gamut) }; namespace details { @@ -43,6 +44,14 @@ inline bool linrgb_in_adobergb_gamut(float r, float g, float b, float eps = 1e-4 argb_b <= 1.0f + eps; } +// Check if linear RGB is in ProPhoto RGB gamut +inline bool linrgb_in_prophoto_gamut(float r, float g, float b, float eps = 1e-4f) noexcept { + float pprgb_r, pprgb_g, pprgb_b; + linrgb_to_prophoto(r, g, b, pprgb_r, pprgb_g, pprgb_b); + return pprgb_r >= -eps && pprgb_r <= 1.0f + eps && pprgb_g >= -eps && pprgb_g <= 1.0f + eps && pprgb_b >= -eps && + pprgb_b <= 1.0f + eps; +} + // Generic gamut check based on gamut type inline bool linrgb_in_gamut(float r, float g, float b, gamut_type type, float eps = 1e-4f) noexcept { switch (type) { @@ -52,6 +61,8 @@ inline bool linrgb_in_gamut(float r, float g, float b, gamut_type type, float ep return linrgb_in_displayp3_gamut(r, g, b, eps); case gamut_type::adobe_rgb: return linrgb_in_adobergb_gamut(r, g, b, eps); + case gamut_type::prophoto_rgb: + return linrgb_in_prophoto_gamut(r, g, b, eps); default: return details::linrgb_in_gamut(r, g, b, eps); } @@ -93,6 +104,14 @@ bool is_in_adobergb_gamut(const Color& c) { return is_in_gamut(c, gamut_type::adobe_rgb); } +/** + * @brief Check if a color is in ProPhoto RGB gamut. + */ +template +bool is_in_prophoto_gamut(const Color& c) { + return is_in_gamut(c, gamut_type::prophoto_rgb); +} + /** * @brief Clip a color to the specified gamut using simple channel clamping. * @tparam Color Input color type. diff --git a/include/colorcpp/algorithms/harmony.hpp b/include/colorcpp/algorithms/harmony.hpp index fb34f20..2dc76f0 100644 --- a/include/colorcpp/algorithms/harmony.hpp +++ b/include/colorcpp/algorithms/harmony.hpp @@ -10,10 +10,10 @@ #pragma once -#include -#include #include #include +#include +#include #include #include diff --git a/include/colorcpp/algorithms/palette/advanced_scales.hpp b/include/colorcpp/algorithms/palette/advanced_scales.hpp index 44aa433..871837e 100644 --- a/include/colorcpp/algorithms/palette/advanced_scales.hpp +++ b/include/colorcpp/algorithms/palette/advanced_scales.hpp @@ -7,9 +7,9 @@ #include #include +#include #include #include -#include #include namespace colorcpp::algorithms::palette { @@ -125,7 +125,7 @@ inline float apply_easing(float t, easing_type easing) { */ template core::palette_set easing_scale(const Color& start, const Color& end, size_t count, easing_type easing, - lerp_method method = lerp_method::oklab) { + lerp_method method = lerp_method::oklab) { core::palette_set p; if (count == 0) return p; if (count == 1) { @@ -164,8 +164,9 @@ core::palette_set easing_scale(const Color& start, const Color& end, size * @return Palette with spline-interpolated colors. */ template -core::palette_set spline_scale(const std::vector& control_points, size_t count, - operations::interpolate::cr_mode mode = operations::interpolate::cr_mode::centripetal) { +core::palette_set spline_scale( + const std::vector& control_points, size_t count, + operations::interpolate::cr_mode mode = operations::interpolate::cr_mode::centripetal) { core::palette_set p; if (control_points.empty() || count == 0) return p; @@ -214,26 +215,28 @@ core::palette_set spline_scale(const std::vector& control_points, auto c3 = color_cast(control_points[i3]); // Apply parameterization - float t_L = - operations::interpolate::details::cr_parameterize(c0.template get_index<0>(), c1.template get_index<0>(), - c2.template get_index<0>(), c3.template get_index<0>(), t, mode); - float t_A = - operations::interpolate::details::cr_parameterize(c0.template get_index<1>(), c1.template get_index<1>(), - c2.template get_index<1>(), c3.template get_index<1>(), t, mode); - float t_B = - operations::interpolate::details::cr_parameterize(c0.template get_index<2>(), c1.template get_index<2>(), - c2.template get_index<2>(), c3.template get_index<2>(), t, mode); - - core::oklab_t mid{ - std::clamp(operations::interpolate::details::catmull_rom(c0.template get_index<0>(), c1.template get_index<0>(), - c2.template get_index<0>(), c3.template get_index<0>(), t_L), - 0.0f, 1.0f), - std::clamp(operations::interpolate::details::catmull_rom(c0.template get_index<1>(), c1.template get_index<1>(), - c2.template get_index<1>(), c3.template get_index<1>(), t_A), - -0.5f, 0.5f), - std::clamp(operations::interpolate::details::catmull_rom(c0.template get_index<2>(), c1.template get_index<2>(), - c2.template get_index<2>(), c3.template get_index<2>(), t_B), - -0.5f, 0.5f)}; + float t_L = operations::interpolate::details::cr_parameterize( + c0.template get_index<0>(), c1.template get_index<0>(), c2.template get_index<0>(), + c3.template get_index<0>(), t, mode); + float t_A = operations::interpolate::details::cr_parameterize( + c0.template get_index<1>(), c1.template get_index<1>(), c2.template get_index<1>(), + c3.template get_index<1>(), t, mode); + float t_B = operations::interpolate::details::cr_parameterize( + c0.template get_index<2>(), c1.template get_index<2>(), c2.template get_index<2>(), + c3.template get_index<2>(), t, mode); + + core::oklab_t mid{std::clamp(operations::interpolate::details::catmull_rom( + c0.template get_index<0>(), c1.template get_index<0>(), + c2.template get_index<0>(), c3.template get_index<0>(), t_L), + 0.0f, 1.0f), + std::clamp(operations::interpolate::details::catmull_rom( + c0.template get_index<1>(), c1.template get_index<1>(), + c2.template get_index<1>(), c3.template get_index<1>(), t_A), + -0.5f, 0.5f), + std::clamp(operations::interpolate::details::catmull_rom( + c0.template get_index<2>(), c1.template get_index<2>(), + c2.template get_index<2>(), c3.template get_index<2>(), t_B), + -0.5f, 0.5f)}; // Interpolate alpha auto r1 = color_cast(control_points[i1]); @@ -260,7 +263,8 @@ core::palette_set spline_scale(const std::vector& control_points, * @return Palette with smooth multi-color gradient. */ template -core::palette_set multi_scale(const std::vector& colors, size_t count, lerp_method method = lerp_method::oklab) { +core::palette_set multi_scale(const std::vector& colors, size_t count, + lerp_method method = lerp_method::oklab) { core::palette_set p; if (colors.empty() || count == 0) return p; diff --git a/include/colorcpp/core/adobe_rgb.hpp b/include/colorcpp/core/adobe_rgb.hpp index dffdea3..55c16ed 100644 --- a/include/colorcpp/core/adobe_rgb.hpp +++ b/include/colorcpp/core/adobe_rgb.hpp @@ -168,4 +168,4 @@ std::ostream& operator<<(std::ostream& os, const colorcpp::core::basic_adobe_rgb template std::istream& operator>>(std::istream& is, colorcpp::core::basic_adobe_rgb& c) { return colorcpp::core::io::operator>>(is, static_cast&>(c)); -} \ No newline at end of file +} diff --git a/include/colorcpp/core/core.hpp b/include/colorcpp/core/core.hpp index 33eb878..4a79908 100644 --- a/include/colorcpp/core/core.hpp +++ b/include/colorcpp/core/core.hpp @@ -14,13 +14,14 @@ #include #include #include -#include #include #include #include #include #include #include +#include +#include #include #include diff --git a/include/colorcpp/core/prophoto_rgb.hpp b/include/colorcpp/core/prophoto_rgb.hpp index f570a44..51e929a 100644 --- a/include/colorcpp/core/prophoto_rgb.hpp +++ b/include/colorcpp/core/prophoto_rgb.hpp @@ -176,4 +176,4 @@ std::ostream& operator<<(std::ostream& os, const colorcpp::core::basic_prophoto_ template std::istream& operator>>(std::istream& is, colorcpp::core::basic_prophoto_rgb& c) { return colorcpp::core::io::operator>>(is, static_cast&>(c)); -} \ No newline at end of file +} diff --git a/include/colorcpp/core/rec2020.hpp b/include/colorcpp/core/rec2020.hpp new file mode 100644 index 0000000..4a90da1 --- /dev/null +++ b/include/colorcpp/core/rec2020.hpp @@ -0,0 +1,171 @@ +/** + * @file rec2020.hpp + * @brief Rec.2020 (BT.2020) color space (D65 white point, BT.709-style piecewise transfer). + * + * ITU-R BT.2020 (Rec.2020) is the UHDTV standard, covering approximately 75% of + * the CIE 1931 visible gamut. It uses the same D65 white point as sRGB but with + * significantly wider primaries. + * + * Transfer function: BT.709-style piecewise (α=1.0993, β=0.0181). + * + * @see https://www.itu.int/rec/R-REC-BT.2020 + */ + +#pragma once + +#include +#include +#include + +namespace colorcpp::core::rec2020 { + +namespace channel { + +struct r_tag {}; +struct g_tag {}; +struct b_tag {}; +struct a_tag {}; + +using f32_red = traits::basic_channel; +using f32_green = traits::basic_channel; +using f32_blue = traits::basic_channel; +using f32_alpha = traits::basic_channel; + +} // namespace channel + +namespace model { + +struct rec2020 {}; +struct rec2020a {}; +struct linear_rec2020f {}; +struct linear_rec2020af {}; + +} // namespace model + +} // namespace colorcpp::core::rec2020 + +namespace colorcpp::traits { + +template <> +struct model_traits { + using channels_type = + std::tuple; + static constexpr std::string_view prefix = "rec2020"; + static constexpr std::size_t channel_size = 3; +}; + +template <> +struct model_traits { + using channels_type = std::tuple; + static constexpr std::string_view prefix = "rec2020a"; + static constexpr std::size_t channel_size = 4; +}; + +template <> +struct model_traits { + using channels_type = + std::tuple; + static constexpr std::string_view prefix = "linear-rec2020"; + static constexpr std::size_t channel_size = 3; +}; + +template <> +struct model_traits { + using channels_type = std::tuple; + static constexpr std::string_view prefix = "linear-rec2020a"; + static constexpr std::size_t channel_size = 4; +}; + +} // namespace colorcpp::traits + +namespace colorcpp::core { + +/** + * @brief Rec.2020 / Rec.2020A with @c r(), @c g(), @c b(), and @c a() when alpha is present. + * @tparam Model rec2020::model::rec2020, rec2020a, linear_rec2020f, or linear_rec2020af. + */ +template +struct basic_rec2020 : basic_color { + using base = basic_color; + + using base::base; + using base::data; + + private: + template + constexpr auto& channel() { + constexpr std::size_t idx = traits::channel_index_v; + return data[idx]; + } + + template + constexpr const auto& channel() const { + constexpr std::size_t idx = traits::channel_index_v; + return data[idx]; + } + + public: + template >> + constexpr auto& r() { + return channel(); + } + + template >> + constexpr const auto& r() const { + return channel(); + } + + template >> + constexpr auto& g() { + return channel(); + } + + template >> + constexpr const auto& g() const { + return channel(); + } + + template >> + constexpr auto& b() { + return channel(); + } + + template >> + constexpr const auto& b() const { + return channel(); + } + + template >> + constexpr auto& a() { + return channel(); + } + + template >> + constexpr const auto& a() const { + return channel(); + } +}; + +/** @brief Rec.2020 with float channels. */ +using rec2020_rgbf_t = basic_rec2020; +/** @brief Rec.2020A with float channels. */ +using rec2020_rgbaf_t = basic_rec2020; +/** @brief Linear Rec.2020 with float channels. */ +using linear_rec2020_rgbf_t = basic_rec2020; +/** @brief Linear Rec.2020A with float channels. */ +using linear_rec2020_rgbaf_t = basic_rec2020; + +} // namespace colorcpp::core + +// I/O operators for basic_rec2020 +template +std::ostream& operator<<(std::ostream& os, const colorcpp::core::basic_rec2020& c) { + return colorcpp::core::io::operator<<(os, static_cast&>(c)); +} + +template +std::istream& operator>>(std::istream& is, colorcpp::core::basic_rec2020& c) { + return colorcpp::core::io::operator>>(is, static_cast&>(c)); +} diff --git a/include/colorcpp/io/css/color_function.hpp b/include/colorcpp/io/css/color_function.hpp index e2402cb..26729c5 100644 --- a/include/colorcpp/io/css/color_function.hpp +++ b/include/colorcpp/io/css/color_function.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -137,8 +138,11 @@ inline core::rgbaf_t prophoto_to_rgbaf(float r, float g, float b, float a) { } inline core::rgbaf_t rec2020_to_rgbaf(float r, float g, float b, float a) { - const auto xyz = linear_rec2020_to_xyz_d65(gamma_decode_rec2020(r), gamma_decode_rec2020(g), gamma_decode_rec2020(b)); - return xyz_d65_to_rgbaf(xyz.x, xyz.y, xyz.z, a); + auto r2020 = core::rec2020_rgbaf_t{r, g, b, a}; + auto out = operations::conversion::color_cast(r2020); + // Restore alpha — conversion via xyz_t hub drops it + out.a() = std::clamp(a, 0.0f, 1.0f); + return out; } inline bool parse_channel_unbounded(details::Cursor& d, float& out) { diff --git a/include/colorcpp/io/css/parse_detail.hpp b/include/colorcpp/io/css/parse_detail.hpp index b58c0eb..95502cc 100644 --- a/include/colorcpp/io/css/parse_detail.hpp +++ b/include/colorcpp/io/css/parse_detail.hpp @@ -482,12 +482,11 @@ inline std::optional resolve_color_mix_items(const parsed_co } } - const float fill_weight = - missing_count == 0 - ? 0.0f - : (explicit_sum <= 0.0f && missing_count == parsed.items.size() - ? 1.0f / static_cast(missing_count) - : std::max(0.0f, 1.0f - explicit_sum) / static_cast(missing_count)); + const float fill_weight = missing_count == 0 + ? 0.0f + : (explicit_sum <= 0.0f && missing_count == parsed.items.size() + ? 1.0f / static_cast(missing_count) + : std::max(0.0f, 1.0f - explicit_sum) / static_cast(missing_count)); float total = 0.0f; for (const auto& item : parsed.items) { diff --git a/include/colorcpp/io/css/relative_color.hpp b/include/colorcpp/io/css/relative_color.hpp index cddc1ea..4bef243 100644 --- a/include/colorcpp/io/css/relative_color.hpp +++ b/include/colorcpp/io/css/relative_color.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -1011,4 +1012,31 @@ inline std::optional evaluate( return evaluate(ast, context); } +// Forward declare parse_css_color primary template to enable specialization +template +inline std::optional parse_css_color(std::string_view str); + +// Rec.2020 specialization — parsed by generic color() infrastructure, +// but this overload enables parse_css_color(). +template <> +inline std::optional parse_css_color(std::string_view str) { + details::trim(str); + if (str.empty()) return std::nullopt; + + // Try direct typed parsing — handles color(rec2020 ...) with correct alpha + details::Cursor c{str, 0}; + if (auto typed = parse_color_function_as(c)) { + c.skip_ws(); + if (c.eof()) return typed; + } + + // Fallback: parse as sRGB and convert to Rec.2020 + auto parsed = parse_css_color_rgbaf(str); + if (!parsed) return std::nullopt; + auto out = operations::conversion::color_cast(*parsed); + // Restore alpha — conversion via xyz_t hub drops it + out.a() = std::clamp(parsed->a(), 0.0f, 1.0f); + return out; +} + } // namespace colorcpp::io::css diff --git a/include/colorcpp/io/literals/oklab.hpp b/include/colorcpp/io/literals/oklab.hpp index e37bb76..695a128 100644 --- a/include/colorcpp/io/literals/oklab.hpp +++ b/include/colorcpp/io/literals/oklab.hpp @@ -56,8 +56,7 @@ constexpr auto operator""_oklch() { static_assert(l <= 100, "colorcpp: _oklch L out of range (000–100)"); static_assert(c <= 100, "colorcpp: _oklch C out of range (000–100)"); static_assert(h <= 360, "colorcpp: _oklch H out of range (0–360)"); - return core::oklch_t{static_cast(l) / 100.0f, static_cast(c) * 0.4f / 100.0f, - static_cast(h)}; + return core::oklch_t{static_cast(l) / 100.0f, static_cast(c) * 0.4f / 100.0f, static_cast(h)}; } /** @} */ diff --git a/include/colorcpp/operations/blend.hpp b/include/colorcpp/operations/blend.hpp index fa076f0..9f7d380 100644 --- a/include/colorcpp/operations/blend.hpp +++ b/include/colorcpp/operations/blend.hpp @@ -19,7 +19,8 @@ #include #include -/** @brief Alpha-aware blending with selectable blend mode in `blend(dst, src, mode)` order; current implementation uses an encoded `rgbaf_t` working space rather than a verified linear-sRGB compositing path. */ +/** @brief Alpha-aware blending with selectable blend mode in `blend(dst, src, mode)` order; current implementation uses + * an encoded `rgbaf_t` working space rather than a verified linear-sRGB compositing path. */ namespace colorcpp::operations::blend { // All functionality is provided by the included sub-modules } diff --git a/include/colorcpp/operations/compare/equal.hpp b/include/colorcpp/operations/compare/equal.hpp index 083278c..907db39 100644 --- a/include/colorcpp/operations/compare/equal.hpp +++ b/include/colorcpp/operations/compare/equal.hpp @@ -5,12 +5,11 @@ #pragma once -#include - #include #include #include #include +#include namespace colorcpp::operations::compare { @@ -44,9 +43,9 @@ constexpr bool equal(const Color& a, const Color& b) noexcept { * @return True if colors are equal according to the policy. */ template -constexpr auto equal(const ColorA& a, const ColorB& b, Args&&... args) - noexcept(noexcept(Policy::compare(a, b, std::forward(args)...))) - -> decltype(Policy::compare(a, b, std::forward(args)...)) { +constexpr auto equal(const ColorA& a, const ColorB& b, + Args&&... args) noexcept(noexcept(Policy::compare(a, b, std::forward(args)...))) + -> decltype(Policy::compare(a, b, std::forward(args)...)) { return Policy::compare(a, b, std::forward(args)...); } diff --git a/include/colorcpp/operations/conversion/color_space_registry.hpp b/include/colorcpp/operations/conversion/color_space_registry.hpp index 3099efa..4e3a530 100644 --- a/include/colorcpp/operations/conversion/color_space_registry.hpp +++ b/include/colorcpp/operations/conversion/color_space_registry.hpp @@ -33,11 +33,12 @@ #include #include #include -#include #include #include #include #include +#include +#include #include #include #include @@ -217,4 +218,26 @@ struct color_traits { using hub_type = core::xyz_t; }; +// Rec.2020 uses Linear Rec.2020 as hub +template <> +struct color_traits { + using hub_type = core::linear_rec2020_rgbf_t; +}; + +template <> +struct color_traits { + using hub_type = core::linear_rec2020_rgbaf_t; +}; + +// Linear Rec.2020 uses XYZ as hub +template <> +struct color_traits { + using hub_type = core::xyz_t; +}; + +template <> +struct color_traits { + using hub_type = core::xyz_t; +}; + } // namespace colorcpp::operations::conversion diff --git a/include/colorcpp/operations/conversion/functions/adobe_rgb.hpp b/include/colorcpp/operations/conversion/functions/adobe_rgb.hpp index b56b251..0d1bf82 100644 --- a/include/colorcpp/operations/conversion/functions/adobe_rgb.hpp +++ b/include/colorcpp/operations/conversion/functions/adobe_rgb.hpp @@ -28,9 +28,7 @@ namespace colorcpp::operations::conversion::details { template constexpr To adobe_rgb_to_linear_adobe_rgb(const From& src) { // Pure Gamma 2.2 linearization (no sRGB-style linear segment) - auto linearize = [](float v) noexcept { - return std::pow(v, 2.2f); - }; + auto linearize = [](float v) noexcept { return std::pow(v, 2.2f); }; float r = linearize(to_unit(src.template get_index<0>())); float g = linearize(to_unit(src.template get_index<1>())); float b = linearize(to_unit(src.template get_index<2>())); @@ -88,9 +86,9 @@ constexpr To linear_adobe_rgb_to_xyz(const From& src) { if constexpr (To::channels >= 4) { float a = get_src_alpha(src); - return pack_to(from_unit(x), from_unit(y), from_unit(z), from_unit(a)); + return pack_to(from_value(x), from_value(y), from_value(z), from_value(a)); } else { - return pack_to(from_unit(x), from_unit(y), from_unit(z)); + return pack_to(from_value(x), from_value(y), from_value(z)); } } @@ -101,9 +99,10 @@ constexpr To linear_adobe_rgb_to_xyz(const From& src) { */ template constexpr To xyz_to_linear_adobe_rgb(const From& src) { - float x = to_unit(src.template get_index<0>()); - float y = to_unit(src.template get_index<1>()); - float z = to_unit(src.template get_index<2>()); + // Read XYZ directly (XYZ channels have [0,2] range, so to_unit would incorrectly rescale) + float x = static_cast(src.template get_index<0>()); + float y = static_cast(src.template get_index<1>()); + float z = static_cast(src.template get_index<2>()); float r = 2.0413690f * x - 0.5649654f * y - 0.3446945f * z; float g = -0.9692660f * x + 1.8760108f * y + 0.0415560f * z; @@ -129,9 +128,7 @@ constexpr To xyz_to_linear_adobe_rgb(const From& src) { template constexpr To adobe_rgb_to_srgb(const From& src) { // Step 1: Linearize (Gamma 2.2) - auto linearize = [](float v) noexcept { - return std::pow(v, 2.2f); - }; + auto linearize = [](float v) noexcept { return std::pow(v, 2.2f); }; float r_lin = linearize(to_unit(src.template get_index<0>())); float g_lin = linearize(to_unit(src.template get_index<1>())); float b_lin = linearize(to_unit(src.template get_index<2>())); diff --git a/include/colorcpp/operations/conversion/functions/index.hpp b/include/colorcpp/operations/conversion/functions/index.hpp index f6eb42a..9e5160a 100644 --- a/include/colorcpp/operations/conversion/functions/index.hpp +++ b/include/colorcpp/operations/conversion/functions/index.hpp @@ -11,12 +11,13 @@ #include #include #include -#include #include #include #include #include #include +#include +#include #include #include #include @@ -163,7 +164,7 @@ COLORCPP_REGISTER_CONVERSION_BIDIR_WEIGHTED(core::display_p3f_t, core::rgbf_t, COLORCPP_REGISTER_CONVERSION_BIDIR_WEIGHTED(core::display_p3af_t, core::rgbaf_t, details::display_p3_to_srgb, details::srgb_to_display_p3, - route_cost::shortcut_4_hop, route_cost::shortcut_4_hop) + route_cost::shortcut_2_hop, route_cost::shortcut_2_hop) // sRGB ↔ CIELAB (direct short link: 1 hop instead of 2 via Linear RGB) COLORCPP_REGISTER_CONVERSION_BIDIR_WEIGHTED(core::rgbf_t, core::cielab_t, details::srgb_to_lab, @@ -231,15 +232,14 @@ COLORCPP_REGISTER_CONVERSION_BIDIR(core::linear_adobe_rgbaf_t, core::xyz_t, details::xyz_to_linear_adobe_rgb) // Adobe RGB ↔ sRGB (direct short link: 1 hop instead of 4 via Linear ARGB → XYZ → Linear sRGB) -COLORCPP_REGISTER_CONVERSION_BIDIR_WEIGHTED(core::adobe_rgbf_t, core::rgbf_t, - details::adobe_rgb_to_srgb, - details::srgb_to_adobe_rgb, - route_cost::shortcut_4_hop, route_cost::shortcut_4_hop) +COLORCPP_REGISTER_CONVERSION_BIDIR_WEIGHTED(core::adobe_rgbf_t, core::rgbf_t, details::adobe_rgb_to_srgb, + details::srgb_to_adobe_rgb, route_cost::shortcut_4_hop, + route_cost::shortcut_4_hop) COLORCPP_REGISTER_CONVERSION_BIDIR_WEIGHTED(core::adobe_rgbaf_t, core::rgbaf_t, details::adobe_rgb_to_srgb, - details::srgb_to_adobe_rgb, - route_cost::shortcut_4_hop, route_cost::shortcut_4_hop) + details::srgb_to_adobe_rgb, route_cost::shortcut_4_hop, + route_cost::shortcut_4_hop) // ProPhoto RGB ↔ Linear ProPhoto RGB COLORCPP_REGISTER_CONVERSION_BIDIR(core::prophoto_rgbf_t, core::linear_prophoto_rgbf_t, @@ -259,4 +259,32 @@ COLORCPP_REGISTER_CONVERSION_BIDIR(core::linear_prophoto_rgbaf_t, core::xyz_t, details::linear_prophoto_rgb_to_xyz, details::xyz_to_linear_prophoto_rgb) +// Rec.2020 ↔ Linear Rec.2020 +COLORCPP_REGISTER_CONVERSION_BIDIR(core::rec2020_rgbf_t, core::linear_rec2020_rgbf_t, + details::rec2020_to_linear_rec2020, + details::linear_rec2020_to_rec2020) + +COLORCPP_REGISTER_CONVERSION_BIDIR(core::rec2020_rgbaf_t, core::linear_rec2020_rgbaf_t, + details::rec2020_to_linear_rec2020, + details::linear_rec2020_to_rec2020) + +// Linear Rec.2020 ↔ XYZ +COLORCPP_REGISTER_CONVERSION_BIDIR(core::linear_rec2020_rgbf_t, core::xyz_t, + details::linear_rec2020_to_xyz, + details::xyz_to_linear_rec2020) + +COLORCPP_REGISTER_CONVERSION_BIDIR(core::linear_rec2020_rgbaf_t, core::xyz_t, + details::linear_rec2020_to_xyz, + details::xyz_to_linear_rec2020) + +// Rec.2020 ↔ sRGB (direct short link) +COLORCPP_REGISTER_CONVERSION_BIDIR_WEIGHTED(core::rec2020_rgbf_t, core::rgbf_t, details::rec2020_to_srgb, + details::srgb_to_rec2020, route_cost::shortcut_4_hop, + route_cost::shortcut_4_hop) + +COLORCPP_REGISTER_CONVERSION_BIDIR_WEIGHTED(core::rec2020_rgbaf_t, core::rgbaf_t, + details::rec2020_to_srgb, + details::srgb_to_rec2020, route_cost::shortcut_4_hop, + route_cost::shortcut_4_hop) + } // namespace colorcpp::operations::conversion diff --git a/include/colorcpp/operations/conversion/functions/prophoto_rgb.hpp b/include/colorcpp/operations/conversion/functions/prophoto_rgb.hpp index d342609..cccd142 100644 --- a/include/colorcpp/operations/conversion/functions/prophoto_rgb.hpp +++ b/include/colorcpp/operations/conversion/functions/prophoto_rgb.hpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -106,13 +107,10 @@ constexpr To linear_prophoto_rgb_to_prophoto_rgb(const From& src) { /** * @brief Convert Linear ProPhoto RGB (D50) to CIE XYZ (D65). * - * This conversion combines two steps: + * This is a two-step conversion: * 1. Linear ProPhoto RGB → XYZ(D50) via ProPhoto primaries matrix * 2. XYZ(D50) → XYZ(D65) via Bradford chromatic adaptation * - * Combined matrix computed from: - * M_bfd(D50→D65) × M_prophoto_rgb_to_xyz(D50) - * * Reference: * http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html */ @@ -122,39 +120,50 @@ constexpr To linear_prophoto_rgb_to_xyz(const From& src) { float g = to_unit(src.template get_index<1>()); float b = to_unit(src.template get_index<2>()); - // Combined matrix: ProPhoto (D50) → XYZ(D65) via Bradford adaptation - float x = 0.755586f * r + 0.112772f * g + 0.108835f * b; - float y = 0.268238f * r + 0.715018f * g + 0.015744f * b; - float z = 0.003912f * r - 0.012919f * g + 1.097797f * b; + // Step 1: Linear ProPhoto RGB → XYZ(D50) via pure primaries matrix + // M_prophoto_to_xyz(D50) from Bruce Lindbloom + float x_d50 = 0.7977605f * r + 0.1351858f * g + 0.0313493f * b; + float y_d50 = 0.2880711f * r + 0.7118432f * g + 0.0000857f * b; + float z_d50 = 0.0000000f * r + 0.0000000f * g + 0.8251046f * b; + + // Step 2: Bradford chromatic adaptation D50 → D65 + core::xyz_t xyz_d50{x_d50, y_d50, z_d50}; + auto xyz_d65 = algorithms::chromatic_adaptation::bradford_adapt( + xyz_d50, algorithms::chromatic_adaptation::WHITEPOINT_D50, algorithms::chromatic_adaptation::WHITEPOINT_D65); if constexpr (To::channels >= 4) { float a = get_src_alpha(src); - return pack_to(from_unit(x), from_unit(y), from_unit(z), from_unit(a)); + return pack_to(from_value(xyz_d65.x()), from_value(xyz_d65.y()), from_value(xyz_d65.z()), + from_value(a)); } else { - return pack_to(from_unit(x), from_unit(y), from_unit(z)); + return pack_to(from_value(xyz_d65.x()), from_value(xyz_d65.y()), from_value(xyz_d65.z())); } } /** * @brief Convert CIE XYZ (D65) to Linear ProPhoto RGB (D50). * - * This conversion combines two steps: + * This is a two-step conversion: * 1. XYZ(D65) → XYZ(D50) via inverse Bradford chromatic adaptation * 2. XYZ(D50) → Linear ProPhoto RGB via inverse ProPhoto primaries matrix - * - * Combined matrix computed from: - * M_prophoto_xyz_to_rgb(D50) × M_bfd_inv(D65→D50) */ template constexpr To xyz_to_linear_prophoto_rgb(const From& src) { - float x = to_unit(src.template get_index<0>()); - float y = to_unit(src.template get_index<1>()); - float z = to_unit(src.template get_index<2>()); - - // Combined matrix: XYZ(D65) → ProPhoto linear (D50) via inverse Bradford - float r = 1.403092f * x - 0.223133f * y - 0.101553f * z; - float g = -0.526200f * x + 1.481540f * y + 0.017034f * z; - float b = -0.011191f * x + 0.018231f * y + 0.911478f * z; + // Read XYZ directly — XYZ channels use natural [0,2] range, not unit [0,1] + float x = static_cast(src.template get_index<0>()); + float y = static_cast(src.template get_index<1>()); + float z = static_cast(src.template get_index<2>()); + + // Step 1: Bradford chromatic adaptation D65 → D50 + core::xyz_t xyz_d65{x, y, z}; + auto xyz_d50 = algorithms::chromatic_adaptation::bradford_adapt( + xyz_d65, algorithms::chromatic_adaptation::WHITEPOINT_D65, algorithms::chromatic_adaptation::WHITEPOINT_D50); + + // Step 2: XYZ(D50) → Linear ProPhoto RGB via inverse primaries matrix + // M_xyz_to_prophoto(D50) from Bruce Lindbloom + float r = 1.3459433f * xyz_d50.x() - 0.2556075f * xyz_d50.y() - 0.0511118f * xyz_d50.z(); + float g = -0.5445989f * xyz_d50.x() + 1.5081673f * xyz_d50.y() + 0.0205351f * xyz_d50.z(); + float b = 0.0000000f * xyz_d50.x() + 0.0000000f * xyz_d50.y() + 1.2119587f * xyz_d50.z(); if constexpr (To::channels >= 4) { float a = get_src_alpha(src); diff --git a/include/colorcpp/operations/conversion/functions/rec2020.hpp b/include/colorcpp/operations/conversion/functions/rec2020.hpp new file mode 100644 index 0000000..a661854 --- /dev/null +++ b/include/colorcpp/operations/conversion/functions/rec2020.hpp @@ -0,0 +1,211 @@ +/** + * @file rec2020.hpp + * @brief Rec.2020 (BT.2020) conversion functions. + * + * Rec.2020 uses BT.709-style piecewise transfer function. + * White point: D65 (same as sRGB/XYZ hub, no chromatic adaptation needed). + * + * Transfer function parameters: + * α = 1.09929682680944 + * β = 0.018053968510807 + * Linear segment threshold (encoded) = β × 4.5 ≈ 0.081242858 + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace colorcpp::operations::conversion::details { + +/** + * @brief Rec.2020 transfer function constants (BT.709-style piecewise). + */ +namespace rec2020_tf { + +constexpr float alpha = 1.09929682680944f; +constexpr float beta = 0.018053968510807f; +constexpr float beta_times_4_5 = beta * 4.5f; // ≈ 0.081242858 +constexpr float exponent = 0.45f; // 1/2.222... +constexpr float inv_exponent = 1.0f / 0.45f; // ≈ 2.222... + +/** + * @brief Linearize a Rec.2020 encoded channel value. + */ +constexpr float to_linear(float v) noexcept { + if (v < beta_times_4_5) + return v / 4.5f; + else + return std::pow((v + alpha - 1.0f) / alpha, inv_exponent); +} + +/** + * @brief Encode a linear channel value to Rec.2020 gamma. + */ +constexpr float from_linear(float v) noexcept { + if (v < beta) + return v * 4.5f; + else + return alpha * std::pow(v, exponent) - (alpha - 1.0f); +} + +} // namespace rec2020_tf + +/** + * @brief Convert Rec.2020 gamma-encoded to Linear Rec.2020. + */ +template +constexpr To rec2020_to_linear_rec2020(const From& src) { + float r = rec2020_tf::to_linear(to_unit(src.template get_index<0>())); + float g = rec2020_tf::to_linear(to_unit(src.template get_index<1>())); + float b = rec2020_tf::to_linear(to_unit(src.template get_index<2>())); + float a = get_src_alpha(src); + + if constexpr (To::channels >= 4) + return pack_to(from_unit(r), from_unit(g), from_unit(b), from_unit(a)); + else + return pack_to(from_unit(r), from_unit(g), from_unit(b)); +} + +/** + * @brief Convert Linear Rec.2020 to gamma-encoded Rec.2020. + */ +template +constexpr To linear_rec2020_to_rec2020(const From& src) { + auto encode = [](float v) noexcept { + v = std::clamp(v, 0.0f, 1.0f); + return rec2020_tf::from_linear(v); + }; + float r = encode(to_unit(src.template get_index<0>())); + float g = encode(to_unit(src.template get_index<1>())); + float b = encode(to_unit(src.template get_index<2>())); + float a = get_src_alpha(src); + + if constexpr (To::channels >= 4) + return pack_to(from_unit(r), from_unit(g), from_unit(b), from_unit(a)); + else + return pack_to(from_unit(r), from_unit(g), from_unit(b)); +} + +/** + * @brief Convert Linear Rec.2020 to CIE XYZ (D65). + * + * Standard BT.2020 primaries matrix (ITU-R BT.2020-2, D65 white point). + */ +template +constexpr To linear_rec2020_to_xyz(const From& src) { + float r = to_unit(src.template get_index<0>()); + float g = to_unit(src.template get_index<1>()); + float b = to_unit(src.template get_index<2>()); + + float x = 0.6369580483012914f * r + 0.14461690358620838f * g + 0.16888097516417213f * b; + float y = 0.2627002120112671f * r + 0.6779980715188708f * g + 0.05930171646986196f * b; + float z = 0.0000000000000000f * r + 0.028072693049087428f * g + 1.0609850577107909f * b; + + // Use from_value for XYZ output (XYZ channels have [0,2] range, not [0,1]) + if constexpr (To::channels >= 4) { + float a = get_src_alpha(src); + return pack_to(from_value(x), from_value(y), from_value(z), from_value(a)); + } else { + return pack_to(from_value(x), from_value(y), from_value(z)); + } +} + +/** + * @brief Convert CIE XYZ (D65) to Linear Rec.2020. + * + * Inverse BT.2020 matrix (ITU-R BT.2020-2). + */ +template +constexpr To xyz_to_linear_rec2020(const From& src) { + // Read XYZ directly (XYZ channels have [0,2] range, so to_unit would incorrectly rescale) + float x = static_cast(src.template get_index<0>()); + float y = static_cast(src.template get_index<1>()); + float z = static_cast(src.template get_index<2>()); + + float r = 1.7166511879712679f * x - 0.35567078377639240f * y - 0.25336628137365992f * z; + float g = -0.6666843518324889f * x + 1.6164812366349388f * y + 0.015768545813911114f * z; + float b = 0.01763985744531078f * x - 0.04277061325780865f * y + 0.942103121235474f * z; + + if constexpr (To::channels >= 4) { + float a = get_src_alpha(src); + return pack_to(from_unit(r), from_unit(g), from_unit(b), from_unit(a)); + } else { + return pack_to(from_unit(r), from_unit(g), from_unit(b)); + } +} + +/** + * @brief Convert Rec.2020 directly to sRGB (4-hop shortcut). + * + * Optimized direct conversion: Rec.2020 linearize -> matrix -> sRGB gamma encode. + * Avoids intermediate XYZ object allocation. + */ +template +constexpr To rec2020_to_srgb(const From& src) { + // Step 1: Linearize Rec.2020 gamma + float r_lin = rec2020_tf::to_linear(to_unit(src.template get_index<0>())); + float g_lin = rec2020_tf::to_linear(to_unit(src.template get_index<1>())); + float b_lin = rec2020_tf::to_linear(to_unit(src.template get_index<2>())); + float a = get_src_alpha(src); + + // Step 2: Matrix: Linear Rec.2020 -> Linear sRGB (computed as M_xyz_to_srgb * M_rec2020_to_xyz) + float r_srgb_lin = 1.6605f * r_lin - 0.5876f * g_lin - 0.0728f * b_lin; + float g_srgb_lin = -0.1246f * r_lin + 1.1329f * g_lin - 0.0083f * b_lin; + float b_srgb_lin = -0.0182f * r_lin - 0.1006f * g_lin + 1.1187f * b_lin; + + // Step 3: Gamma encode for sRGB + auto gamma_encode = [](float v) noexcept { + v = std::clamp(v, 0.0f, 1.0f); + return (v <= 0.0031308f) ? (v * 12.92f) : (1.055f * std::pow(v, 1.0f / 2.4f) - 0.055f); + }; + float r = gamma_encode(r_srgb_lin); + float g = gamma_encode(g_srgb_lin); + float b = gamma_encode(b_srgb_lin); + + if constexpr (To::channels >= 4) + return pack_to(from_unit(r), from_unit(g), from_unit(b), from_unit(a)); + else + return pack_to(from_unit(r), from_unit(g), from_unit(b)); +} + +/** + * @brief Convert sRGB directly to Rec.2020 (4-hop shortcut). + * + * Optimized direct conversion: sRGB linearize -> inverse matrix -> Rec.2020 gamma encode. + */ +template +constexpr To srgb_to_rec2020(const From& src) { + // Step 1: Linearize sRGB + auto linearize = [](float v) noexcept { + return (v <= 0.04045f) ? (v / 12.92f) : std::pow((v + 0.055f) / 1.055f, 2.4f); + }; + float r_lin = linearize(to_unit(src.template get_index<0>())); + float g_lin = linearize(to_unit(src.template get_index<1>())); + float b_lin = linearize(to_unit(src.template get_index<2>())); + float a = get_src_alpha(src); + + // Step 2: Matrix: Linear sRGB -> Linear Rec.2020 (inverse of above) + float r_r2020_lin = 0.6274f * r_lin + 0.3293f * g_lin + 0.0433f * b_lin; + float g_r2020_lin = 0.0691f * r_lin + 0.9195f * g_lin + 0.0114f * b_lin; + float b_r2020_lin = 0.0164f * r_lin + 0.0880f * g_lin + 0.8956f * b_lin; + + // Step 3: Gamma encode Rec.2020 + auto gamma_encode = [](float v) noexcept { + v = std::clamp(v, 0.0f, 1.0f); + return rec2020_tf::from_linear(v); + }; + float r = gamma_encode(r_r2020_lin); + float g = gamma_encode(g_r2020_lin); + float b = gamma_encode(b_r2020_lin); + + if constexpr (To::channels >= 4) + return pack_to(from_unit(r), from_unit(g), from_unit(b), from_unit(a)); + else + return pack_to(from_unit(r), from_unit(g), from_unit(b)); +} + +} // namespace colorcpp::operations::conversion::details diff --git a/include/colorcpp/operations/conversion/graph.hpp b/include/colorcpp/operations/conversion/graph.hpp index fe8a5ea..02c3c48 100644 --- a/include/colorcpp/operations/conversion/graph.hpp +++ b/include/colorcpp/operations/conversion/graph.hpp @@ -197,7 +197,8 @@ using builtin_color_nodes = core::cmyk8_t, core::cmyk_float_t, core::display_p3f_t, core::display_p3af_t, core::linear_display_p3f_t, core::linear_display_p3af_t, core::adobe_rgbf_t, core::adobe_rgbaf_t, core::linear_adobe_rgbf_t, core::linear_adobe_rgbaf_t, core::prophoto_rgbf_t, core::prophoto_rgbaf_t, core::linear_prophoto_rgbf_t, - core::linear_prophoto_rgbaf_t>; + core::linear_prophoto_rgbaf_t, core::rec2020_rgbf_t, core::rec2020_rgbaf_t, core::linear_rec2020_rgbf_t, + core::linear_rec2020_rgbaf_t>; /** * @brief Global color graph node set including user extensions. @@ -266,7 +267,7 @@ struct search_state { std::array prev{}; }; -template +template struct shortest_path_for_nodes { private: static constexpr std::size_t node_count = Nodes::size; @@ -348,7 +349,7 @@ struct shortest_path_for_nodes { * @tparam Target Target color type * @tparam MaxNodes Safety bound on the known node count */ -template +template struct shortest_path : shortest_path_for_nodes {}; template diff --git a/include/colorcpp/operations/interpolate.hpp b/include/colorcpp/operations/interpolate.hpp index 471a3ab..16da470 100644 --- a/include/colorcpp/operations/interpolate.hpp +++ b/include/colorcpp/operations/interpolate.hpp @@ -26,7 +26,8 @@ #include #include -/** @brief Space-aware interpolation families: RGB-style, hue-aware cylindrical, perceptual, and path/spline helpers with multi-stop gradients, easing, and alpha-control helpers. */ +/** @brief Space-aware interpolation families: RGB-style, hue-aware cylindrical, perceptual, and path/spline helpers + * with multi-stop gradients, easing, and alpha-control helpers. */ namespace colorcpp::operations::interpolate { // All functionality is provided by the included sub-modules } diff --git a/include/colorcpp/operations/interpolate/smooth_path.hpp b/include/colorcpp/operations/interpolate/smooth_path.hpp index 076c485..7e5562f 100644 --- a/include/colorcpp/operations/interpolate/smooth_path.hpp +++ b/include/colorcpp/operations/interpolate/smooth_path.hpp @@ -6,17 +6,16 @@ #pragma once #include +#include +#include +#include +#include #include #include #include #include #include -#include -#include -#include -#include - namespace colorcpp::operations::interpolate { /** @brief Interpolator used by @ref lerp_path across adjacent control-point segments. */ @@ -88,8 +87,7 @@ auto lerp_path_impl(const Container& colors, float t, path_algorithm algorithm, */ template auto lerp_path(const Container& colors, float t, path_algorithm algorithm = path_algorithm::monotonic_spline, - cr_mode catmull_mode = cr_mode::centripetal, float tension = 0.0f) - -> details::path_color_t { + cr_mode catmull_mode = cr_mode::centripetal, float tension = 0.0f) -> details::path_color_t { return details::lerp_path_impl(colors, t, algorithm, catmull_mode, tension); } diff --git a/include/colorcpp/operations/palette.hpp b/include/colorcpp/operations/palette.hpp index e0c5d2e..325f35d 100644 --- a/include/colorcpp/operations/palette.hpp +++ b/include/colorcpp/operations/palette.hpp @@ -5,7 +5,8 @@ * This header includes all palette sub-modules: * - core/palette_set.hpp: Ordered list of colors with wrap indexing * - algorithms/palette/scales.hpp: Gradient scale generators (linear, visual, perceptual) - * - algorithms/palette/advanced_scales.hpp: Advanced gradient generators with easing functions, spline curves, and multi-color transitions + * - algorithms/palette/advanced_scales.hpp: Advanced gradient generators with easing functions, spline curves, and + * multi-color transitions * - operations/palette/schemes.hpp: Formal harmony scheme generators built on algorithms::harmony rules * - algorithms/palette/rainbow.hpp: Rainbow palette generator * - algorithms/palette/families.hpp: Warm, cool, and neutral palette families @@ -15,13 +16,13 @@ #pragma once -#include -#include #include -#include #include -#include +#include +#include +#include #include +#include #include /** @brief structural palette APIs plus heuristic theme and material helpers built on interpolate and harmony rules. */ diff --git a/include/colorcpp/operations/palette/theme.hpp b/include/colorcpp/operations/palette/theme.hpp index a44296d..58ef788 100644 --- a/include/colorcpp/operations/palette/theme.hpp +++ b/include/colorcpp/operations/palette/theme.hpp @@ -6,10 +6,10 @@ #pragma once +#include #include #include #include -#include namespace colorcpp::operations::palette { @@ -30,12 +30,8 @@ class theme { public: /** @brief Interpret @p seed as 0xRRGGBB and construct an opaque seed color with alpha set to 0xFF. */ static theme from_seed(uint32_t seed) { - return from_seed(core::rgba8_t{ - static_cast((seed >> 16) & 0xFF), - static_cast((seed >> 8) & 0xFF), - static_cast(seed & 0xFF), - 0xFF - }); + return from_seed(core::rgba8_t{static_cast((seed >> 16) & 0xFF), static_cast((seed >> 8) & 0xFF), + static_cast(seed & 0xFF), 0xFF}); } static theme from_seed(core::rgba8_t seed) { return theme(seed); } diff --git a/include/colorcpp/operations/random/luminance.hpp b/include/colorcpp/operations/random/luminance.hpp index 2874f5f..cb21ba6 100644 --- a/include/colorcpp/operations/random/luminance.hpp +++ b/include/colorcpp/operations/random/luminance.hpp @@ -40,10 +40,10 @@ class luminance_generator : public basic_random_generator { public: /** @brief Generation options. */ struct options { - float l_min = 0.0f; ///< OkLab lightness lower bound [0, 1]. - float l_max = 1.0f; ///< OkLab lightness upper bound [0, 1]. - float chroma_max = 0.4f; ///< OkLCH chroma upper bound [0, 0.4]. - bool gamut_map = true; ///< Clip to sRGB gamut via lightness-preserving mapper. + float l_min = 0.0f; ///< OkLab lightness lower bound [0, 1]. + float l_max = 1.0f; ///< OkLab lightness upper bound [0, 1]. + float chroma_max = 0.4f; ///< OkLCH chroma upper bound [0, 0.4]. + bool gamut_map = true; ///< Clip to sRGB gamut via lightness-preserving mapper. }; explicit luminance_generator(const Engine& e, const options& o = {}) : base(e), opts_(o) { validate(opts_); } diff --git a/tests/algorithms/test_chromatic_adaptation.cpp b/tests/algorithms/test_chromatic_adaptation.cpp new file mode 100644 index 0000000..5d01cd1 --- /dev/null +++ b/tests/algorithms/test_chromatic_adaptation.cpp @@ -0,0 +1,99 @@ +/** + * @file test_chromatic_adaptation.cpp + * @brief Tests for chromatic adaptation (Bradford and Von Kries). + */ + +#include + +#include +#include +#include +#include + +namespace colorcpp::algorithms::test { + +using namespace core; +using namespace chromatic_adaptation; +using namespace operations::conversion; + +// --- White point constants --- + +TEST(WhitePointTest, D65Values) { + EXPECT_NEAR(WHITEPOINT_D65.x(), 0.95047f, 1e-5f); + EXPECT_NEAR(WHITEPOINT_D65.y(), 1.00000f, 1e-5f); + EXPECT_NEAR(WHITEPOINT_D65.z(), 1.08883f, 1e-5f); +} + +TEST(WhitePointTest, D50Values) { + EXPECT_NEAR(WHITEPOINT_D50.x(), 0.96422f, 1e-5f); + EXPECT_NEAR(WHITEPOINT_D50.y(), 1.00000f, 1e-5f); + EXPECT_NEAR(WHITEPOINT_D50.z(), 0.82521f, 1e-5f); +} + +// --- Bradford adaptation --- + +TEST(BradfordTest, SameWhitePointIsIdentity) { + xyz_t color(0.5f, 0.4f, 0.3f); + auto result = bradford_adapt(color, WHITEPOINT_D65, WHITEPOINT_D65); + EXPECT_NEAR(result.x(), 0.5f, 1e-5f); + EXPECT_NEAR(result.y(), 0.4f, 1e-5f); + EXPECT_NEAR(result.z(), 0.3f, 1e-5f); +} + +TEST(BradfordTest, RoundTripD65D50D65) { + xyz_t color(0.5f, 0.4f, 0.3f); + auto d50 = bradford_adapt(color, WHITEPOINT_D65, WHITEPOINT_D50); + auto back = bradford_adapt(d50, WHITEPOINT_D50, WHITEPOINT_D65); + EXPECT_NEAR(back.x(), color.x(), 1e-4f); + EXPECT_NEAR(back.y(), color.y(), 1e-4f); + EXPECT_NEAR(back.z(), color.z(), 1e-4f); +} + +TEST(BradfordTest, D65ToD50KnownReference) { + // Bruce Lindbloom reference: D65 white (0.95047, 1.0, 1.08883) → D50 + auto result = bradford_adapt(WHITEPOINT_D65, WHITEPOINT_D65, WHITEPOINT_D50); + EXPECT_NEAR(result.x(), 0.96422f, 1e-4f); + EXPECT_NEAR(result.y(), 1.00000f, 1e-4f); + EXPECT_NEAR(result.z(), 0.82521f, 1e-4f); +} + +TEST(BradfordTest, NonNegativeOutput) { + xyz_t color(0.1f, 0.8f, 0.5f); + auto result = bradford_adapt(color, WHITEPOINT_D65, WHITEPOINT_A); + EXPECT_GE(result.x(), -1e-5f); + EXPECT_GE(result.y(), -1e-5f); + EXPECT_GE(result.z(), -1e-5f); +} + +TEST(BradfordTest, WorksWithRgbTypes) { + rgbf_t color(0.5f, 0.3f, 0.7f); + auto result = bradford_adapt(color, WHITEPOINT_D65, WHITEPOINT_D50); + // Result should be a valid rgbf_t (within [0,1]) + EXPECT_GE(result.r(), 0.0f); + EXPECT_LE(result.r(), 1.0f); + EXPECT_GE(result.g(), 0.0f); + EXPECT_LE(result.g(), 1.0f); + EXPECT_GE(result.b(), 0.0f); + EXPECT_LE(result.b(), 1.0f); +} + +// --- Von Kries adaptation --- + +TEST(VonKriesTest, SameWhitePointIsIdentity) { + xyz_t color(0.5f, 0.4f, 0.3f); + auto result = von_kries_adapt(color, WHITEPOINT_D65, WHITEPOINT_D65); + EXPECT_NEAR(result.x(), 0.5f, 1e-5f); + EXPECT_NEAR(result.y(), 0.4f, 1e-5f); + EXPECT_NEAR(result.z(), 0.3f, 1e-5f); +} + +TEST(VonKriesTest, RoundTripD65D50D65) { + xyz_t color(0.5f, 0.4f, 0.3f); + auto d50 = von_kries_adapt(color, WHITEPOINT_D65, WHITEPOINT_D50); + auto back = von_kries_adapt(d50, WHITEPOINT_D50, WHITEPOINT_D65); + EXPECT_NEAR(back.x(), color.x(), 1e-3f); + EXPECT_NEAR(back.y(), color.y(), 1e-3f); + EXPECT_NEAR(back.z(), color.z(), 1e-3f); +} + +} // namespace colorcpp::algorithms::test diff --git a/tests/algorithms/test_gamut.cpp b/tests/algorithms/test_gamut.cpp index a8b0e32..6477fa7 100644 --- a/tests/algorithms/test_gamut.cpp +++ b/tests/algorithms/test_gamut.cpp @@ -303,4 +303,33 @@ TEST(PreserveModeTest, GamutClipWithInfo) { } } +// --- ProPhoto RGB gamut --- + +TEST(MultiColorspaceGamutTest, ProPhotoInGamut) { + // Colors near sRGB primaries should all be within ProPhoto's massive gamut + EXPECT_TRUE(is_in_gamut(rgbf_t{1.0f, 0.0f, 0.0f}, gamut_type::prophoto_rgb)); + EXPECT_TRUE(is_in_gamut(rgbf_t{0.0f, 1.0f, 0.0f}, gamut_type::prophoto_rgb)); + EXPECT_TRUE(is_in_gamut(rgbf_t{0.0f, 0.0f, 1.0f}, gamut_type::prophoto_rgb)); + EXPECT_TRUE(is_in_gamut(rgbf_t{0.0f, 0.0f, 0.0f}, gamut_type::prophoto_rgb)); + EXPECT_TRUE(is_in_gamut(rgbf_t{1.0f, 1.0f, 1.0f}, gamut_type::prophoto_rgb)); +} + +TEST(MultiColorspaceGamutTest, ProPhotoConvenienceFunction) { + EXPECT_TRUE(is_in_prophoto_gamut(rgbf_t{0.5f, 0.5f, 0.5f})); +} + +TEST(MultiColorspaceGamutTest, GamutClipToProPhoto) { + // Clipping a valid sRGB color to ProPhoto should return it (ProPhoto is larger) + auto result = gamut_clip_to_gamut(rgbf_t{0.3f, 0.6f, 0.9f}, gamut_type::prophoto_rgb); + EXPECT_NEAR(result.template get_index<0>(), 0.3f, 1e-2f); + EXPECT_NEAR(result.template get_index<1>(), 0.6f, 1e-2f); + EXPECT_NEAR(result.template get_index<2>(), 0.9f, 1e-2f); +} + +TEST(MultiColorspaceGamutTest, ProPhotoClippedResultIsInGamut) { + // Clip an out-of-gamut color to ProPhoto, verify in-gamut + auto clipped = gamut_clip_to_gamut(core::oklab_t{0.5f, 0.4f, 0.0f}, gamut_type::prophoto_rgb); + EXPECT_TRUE(is_in_gamut(clipped, gamut_type::prophoto_rgb)); +} + } // namespace colorcpp::algorithms::test diff --git a/tests/core/test_adobe_rgb.cpp b/tests/core/test_adobe_rgb.cpp index 4a065a6..569e19e 100644 --- a/tests/core/test_adobe_rgb.cpp +++ b/tests/core/test_adobe_rgb.cpp @@ -247,4 +247,4 @@ TEST(AdobeRGBCSSTest, ColorMixProgressForm) { ASSERT_TRUE(c.has_value()); } -} // namespace colorcpp::core::test \ No newline at end of file +} // namespace colorcpp::core::test diff --git a/tests/core/test_display_p3.cpp b/tests/core/test_display_p3.cpp index 8d09a5c..39b8c37 100644 --- a/tests/core/test_display_p3.cpp +++ b/tests/core/test_display_p3.cpp @@ -239,8 +239,6 @@ TEST(DisplayP3Test, TypeTraits) { static_assert(traits::has_channel_tag_v); } -// Known color conversions - Temporarily disabled due to missing conversion implementation -/* TEST(DisplayP3ConversionTest, BlackConversion) { auto p3 = color_cast(rgbf_t{0.0f, 0.0f, 0.0f}); EXPECT_NEAR(p3.r(), 0.0f, 1e-4f); @@ -301,6 +299,5 @@ TEST(DisplayP3LinearTest, LinearRoundTrip) { EXPECT_NEAR(back.g(), orig.g(), 1e-4f); EXPECT_NEAR(back.b(), orig.b(), 1e-4f); } -*/ } // namespace colorcpp::core::test diff --git a/tests/core/test_prophoto_rgb.cpp b/tests/core/test_prophoto_rgb.cpp index a7bd83e..7ac719e 100644 --- a/tests/core/test_prophoto_rgb.cpp +++ b/tests/core/test_prophoto_rgb.cpp @@ -210,4 +210,4 @@ TEST(ProPhotoRGBCSSTest, ColorMixProgressForm) { ASSERT_TRUE(c.has_value()); } -} // namespace colorcpp::core::test \ No newline at end of file +} // namespace colorcpp::core::test diff --git a/tests/core/test_rec2020.cpp b/tests/core/test_rec2020.cpp new file mode 100644 index 0000000..1f4709e --- /dev/null +++ b/tests/core/test_rec2020.cpp @@ -0,0 +1,253 @@ +/** + * @file test_rec2020.cpp + * @brief Tests for Rec.2020 (BT.2020) color model and conversion. + * + * Rec.2020 uses BT.709-style piecewise transfer function and D65 white point. + */ + +#include + +#include +#include +#include +#include +#include + +namespace colorcpp::core::test { + +using namespace colorcpp::operations::conversion; +using colorcpp::io::css::parse_css_color; + +// --- Construction --- + +TEST(Rec2020Test, DefaultConstruction) { + rec2020_rgbf_t c; + EXPECT_FLOAT_EQ(c.r(), 0.0f); + EXPECT_FLOAT_EQ(c.g(), 0.0f); + EXPECT_FLOAT_EQ(c.b(), 0.0f); +} + +TEST(Rec2020Test, ParameterizedConstruction) { + rec2020_rgbf_t c(0.2f, 0.4f, 0.6f); + EXPECT_FLOAT_EQ(c.r(), 0.2f); + EXPECT_FLOAT_EQ(c.g(), 0.4f); + EXPECT_FLOAT_EQ(c.b(), 0.6f); +} + +TEST(Rec2020ATest, ParameterizedWithAlpha) { + rec2020_rgbaf_t c(0.2f, 0.4f, 0.6f, 0.8f); + EXPECT_FLOAT_EQ(c.r(), 0.2f); + EXPECT_FLOAT_EQ(c.g(), 0.4f); + EXPECT_FLOAT_EQ(c.b(), 0.6f); + EXPECT_FLOAT_EQ(c.a(), 0.8f); +} + +// --- Boundary values --- + +TEST(Rec2020Test, BoundaryValuesMin) { + rec2020_rgbf_t c(0.0f, 0.0f, 0.0f); + EXPECT_FLOAT_EQ(c.r(), 0.0f); + EXPECT_FLOAT_EQ(c.g(), 0.0f); + EXPECT_FLOAT_EQ(c.b(), 0.0f); +} + +TEST(Rec2020Test, BoundaryValuesMax) { + rec2020_rgbf_t c(1.0f, 1.0f, 1.0f); + EXPECT_FLOAT_EQ(c.r(), 1.0f); + EXPECT_FLOAT_EQ(c.g(), 1.0f); + EXPECT_FLOAT_EQ(c.b(), 1.0f); +} + +TEST(Rec2020Test, OutOfRangeThrows) { + EXPECT_THROW(rec2020_rgbf_t(-0.1f, 0.5f, 0.5f), std::out_of_range); + EXPECT_THROW(rec2020_rgbf_t(1.1f, 0.5f, 0.5f), std::out_of_range); + EXPECT_THROW(rec2020_rgbf_t(0.5f, -0.1f, 0.5f), std::out_of_range); + EXPECT_THROW(rec2020_rgbf_t(0.5f, 1.1f, 0.5f), std::out_of_range); + EXPECT_THROW(rec2020_rgbf_t(0.5f, 0.5f, -0.1f), std::out_of_range); + EXPECT_THROW(rec2020_rgbf_t(0.5f, 0.5f, 1.1f), std::out_of_range); +} + +// --- Member and indexed access --- + +TEST(Rec2020Test, MutableMemberAccess) { + rec2020_rgbf_t c(0.1f, 0.2f, 0.3f); + c.r() = 0.4f; + c.g() = 0.5f; + c.b() = 0.6f; + EXPECT_FLOAT_EQ(c.r(), 0.4f); + EXPECT_FLOAT_EQ(c.g(), 0.5f); + EXPECT_FLOAT_EQ(c.b(), 0.6f); +} + +TEST(Rec2020Test, IndexAccess) { + rec2020_rgbf_t c(0.2f, 0.4f, 0.6f); + EXPECT_FLOAT_EQ(c.get_index<0>(), 0.2f); + EXPECT_FLOAT_EQ(c.get_index<1>(), 0.4f); + EXPECT_FLOAT_EQ(c.get_index<2>(), 0.6f); +} + +TEST(Rec2020Test, ConstCorrectness) { + const rec2020_rgbf_t c(0.3f, 0.5f, 0.7f); + EXPECT_FLOAT_EQ(c.r(), 0.3f); + EXPECT_FLOAT_EQ(c.g(), 0.5f); + EXPECT_FLOAT_EQ(c.b(), 0.7f); +} + +// --- Copy and move semantics --- + +TEST(Rec2020Test, CopyConstruction) { + rec2020_rgbf_t original(0.2f, 0.4f, 0.6f); + rec2020_rgbf_t copy(original); + EXPECT_FLOAT_EQ(copy.r(), 0.2f); + EXPECT_FLOAT_EQ(copy.g(), 0.4f); + EXPECT_FLOAT_EQ(copy.b(), 0.6f); +} + +TEST(Rec2020Test, CopyAssignment) { + rec2020_rgbf_t original(0.2f, 0.4f, 0.6f); + rec2020_rgbf_t copy; + copy = original; + EXPECT_FLOAT_EQ(copy.r(), 0.2f); + EXPECT_FLOAT_EQ(copy.g(), 0.4f); + EXPECT_FLOAT_EQ(copy.b(), 0.6f); +} + +TEST(Rec2020Test, Equality) { + rec2020_rgbf_t a(0.2f, 0.4f, 0.6f); + rec2020_rgbf_t b(0.2f, 0.4f, 0.6f); + rec2020_rgbf_t c(0.3f, 0.4f, 0.6f); + EXPECT_EQ(a, b); + EXPECT_NE(a, c); +} + +// --- Linear variants --- + +TEST(LinearRec2020Test, Construction) { + linear_rec2020_rgbf_t c(0.1f, 0.5f, 0.9f); + EXPECT_FLOAT_EQ(c.r(), 0.1f); + EXPECT_FLOAT_EQ(c.g(), 0.5f); + EXPECT_FLOAT_EQ(c.b(), 0.9f); +} + +TEST(LinearRec2020ATest, ConstructionWithAlpha) { + linear_rec2020_rgbaf_t c(0.1f, 0.5f, 0.9f, 0.3f); + EXPECT_FLOAT_EQ(c.r(), 0.1f); + EXPECT_FLOAT_EQ(c.g(), 0.5f); + EXPECT_FLOAT_EQ(c.b(), 0.9f); + EXPECT_FLOAT_EQ(c.a(), 0.3f); +} + +// --- Conversion tests --- + +TEST(Rec2020ConversionTest, BlackConversion) { + auto r2020 = color_cast(rgbf_t{0.0f, 0.0f, 0.0f}); + EXPECT_NEAR(r2020.r(), 0.0f, 1e-4f); + EXPECT_NEAR(r2020.g(), 0.0f, 1e-4f); + EXPECT_NEAR(r2020.b(), 0.0f, 1e-4f); +} + +TEST(Rec2020ConversionTest, WhiteConversion) { + auto r2020 = color_cast(rgbf_t{1.0f, 1.0f, 1.0f}); + // sRGB and Rec.2020 both use D65 white point; round-trip through XYZ + // has ~1e-4 float precision on some channels + EXPECT_NEAR(r2020.r(), 1.0f, 5e-4f); + EXPECT_NEAR(r2020.g(), 1.0f, 5e-4f); + EXPECT_NEAR(r2020.b(), 1.0f, 5e-4f); +} + +// Alpha through graph routing (XYZ hub discards alpha, same limitation as Display P3A) +TEST(Rec2020ConversionTest, AlphaPreservedViaLinear) { + rec2020_rgbaf_t original(0.5f, 0.3f, 0.7f, 0.6f); + auto linear = color_cast(original); + EXPECT_NEAR(linear.a(), 0.6f, 1e-4f); +} + +TEST(Rec2020ConversionTest, FloatRoundTrip) { + rec2020_rgbf_t orig(0.6f, 0.2f, 0.8f); + auto back = color_cast(color_cast(orig)); + EXPECT_NEAR(back.r(), orig.r(), 1e-4f); + EXPECT_NEAR(back.g(), orig.g(), 1e-4f); + EXPECT_NEAR(back.b(), orig.b(), 1e-4f); +} + +TEST(Rec2020ConversionTest, AlphaLinearRoundTrip) { + rec2020_rgbaf_t orig(0.3f, 0.5f, 0.7f, 0.4f); + auto linear = color_cast(orig); + auto back = color_cast(linear); + EXPECT_NEAR(back.r(), orig.r(), 1e-4f); + EXPECT_NEAR(back.g(), orig.g(), 1e-4f); + EXPECT_NEAR(back.b(), orig.b(), 1e-4f); + EXPECT_NEAR(back.a(), orig.a(), 1e-4f); +} + +// Linear conversion +TEST(Rec2020LinearTest, GammaDirection) { + rec2020_rgbf_t gamma(0.5f, 0.5f, 0.5f); + auto linear = color_cast(gamma); + // Non-linear gamma -- linear values are lower for mid-tones + EXPECT_LT(linear.r(), 0.5f); + EXPECT_LT(linear.g(), 0.5f); + EXPECT_LT(linear.b(), 0.5f); +} + +TEST(Rec2020LinearTest, LinearRoundTrip) { + rec2020_rgbf_t orig(0.3f, 0.6f, 0.9f); + auto linear = color_cast(orig); + auto back = color_cast(linear); + EXPECT_NEAR(back.r(), orig.r(), 1e-4f); + EXPECT_NEAR(back.g(), orig.g(), 1e-4f); + EXPECT_NEAR(back.b(), orig.b(), 1e-4f); +} + +// Cross-space conversions +TEST(Rec2020ConversionTest, DisplayP3CrossConvert) { + rec2020_rgbf_t orig(0.5f, 0.3f, 0.7f); + auto dp3 = color_cast(orig); + auto back = color_cast(dp3); + EXPECT_NEAR(back.r(), orig.r(), 1e-2f); + EXPECT_NEAR(back.g(), orig.g(), 1e-2f); + EXPECT_NEAR(back.b(), orig.b(), 1e-2f); +} + +TEST(Rec2020ConversionTest, AdobeRGBCrossConvert) { + rec2020_rgbf_t orig(0.5f, 0.3f, 0.7f); + auto argb = color_cast(orig); + auto back = color_cast(argb); + EXPECT_NEAR(back.r(), orig.r(), 1e-2f); + EXPECT_NEAR(back.g(), orig.g(), 1e-2f); + EXPECT_NEAR(back.b(), orig.b(), 1e-2f); +} + +TEST(Rec2020ConversionTest, XYZCrossConvert) { + rec2020_rgbf_t orig(0.5f, 0.3f, 0.7f); + auto xyz = color_cast(orig); + auto back = color_cast(xyz); + EXPECT_NEAR(back.r(), orig.r(), 1e-4f); + EXPECT_NEAR(back.g(), orig.g(), 1e-4f); + EXPECT_NEAR(back.b(), orig.b(), 1e-4f); +} + +TEST(Rec2020ConversionTest, OKLabCrossConvert) { + rec2020_rgbf_t orig(0.5f, 0.3f, 0.7f); + auto oklab = color_cast(orig); + auto back = color_cast(oklab); + EXPECT_NEAR(back.r(), orig.r(), 1e-2f); + EXPECT_NEAR(back.g(), orig.g(), 1e-2f); + EXPECT_NEAR(back.b(), orig.b(), 1e-2f); +} + +// CSS parsing +TEST(Rec2020CSSTest, ParseColorFunction) { + auto c = parse_css_color("color(rec2020 0.5 0.3 0.7 / 0.8)"); + ASSERT_TRUE(c.has_value()); + EXPECT_NEAR(c->r(), 0.5f, 1e-4f); + EXPECT_NEAR(c->g(), 0.3f, 1e-4f); + EXPECT_NEAR(c->b(), 0.7f, 1e-4f); + EXPECT_NEAR(c->a(), 0.8f, 1e-4f); +} + +// ParseColorFunctionRoundTrip depends on to_css_color_string for Rec.2020, +// which requires Task 7 (CSS color function serialization). +// Test restored when to_css_color_string(rec2020_rgbaf_t) is available. + +} // namespace colorcpp::core::test diff --git a/tests/io/test_binary_io.cpp b/tests/io/test_binary_io.cpp index 40ace55..ce9199f 100644 --- a/tests/io/test_binary_io.cpp +++ b/tests/io/test_binary_io.cpp @@ -3,11 +3,11 @@ * @brief Unit tests for binary_io: LUT data structures and .cube format. */ +#include + #include #include #include -#include - #include #include diff --git a/tests/io/test_css.cpp b/tests/io/test_css.cpp index fea2254..fbdce21 100644 --- a/tests/io/test_css.cpp +++ b/tests/io/test_css.cpp @@ -594,9 +594,8 @@ TEST(Css, RelativeColorAstEvaluateSupportsVar) { auto concrete = parse_css_color_ast("red"); ASSERT_TRUE(concrete); EXPECT_TRUE(concrete->is_concrete()); - auto concrete_eval = evaluate(*concrete, [](std::string_view) -> std::optional { - return std::nullopt; - }); + auto concrete_eval = + evaluate(*concrete, [](std::string_view) -> std::optional { return std::nullopt; }); ASSERT_TRUE(concrete_eval); expect_rgba(*concrete_eval, 255, 0, 0, 255); } @@ -612,7 +611,8 @@ TEST(Css, RelativeColorParsingUsesContextVariableResolver) { ASSERT_TRUE(rgb_relative); expect_rgbaf_near(*rgb_relative, 0.9f, 0.2f, 0.48f, 0.8f, 0.01f); - auto color_relative = parse_css_color_rgbaf("color(from var(--theme-primary) srgb calc(r * 0.5) g b / alpha)", context); + auto color_relative = + parse_css_color_rgbaf("color(from var(--theme-primary) srgb calc(r * 0.5) g b / alpha)", context); ASSERT_TRUE(color_relative); expect_rgbaf_near(*color_relative, 0.45f, 0.2f, 0.4f, 1.0f, 0.01f); @@ -760,8 +760,8 @@ TEST(Css, ColorMixHueInterpolationKeywords) { auto shorter_expected = parse_css_color_rgbaf("oklch(0.7 0.04 60)"); ASSERT_TRUE(shorter); ASSERT_TRUE(shorter_expected); - expect_rgbaf_near(*shorter, shorter_expected->r(), shorter_expected->g(), shorter_expected->b(), shorter_expected->a(), - 0.01f); + expect_rgbaf_near(*shorter, shorter_expected->r(), shorter_expected->g(), shorter_expected->b(), + shorter_expected->a(), 0.01f); auto longer = parse_css_color_rgbaf("color-mix(in oklch longer hue, oklch(0.7 0.04 30), oklch(0.7 0.04 90))"); auto longer_expected = parse_css_color_rgbaf("oklch(0.7 0.04 240)"); diff --git a/tests/io/test_serialization.cpp b/tests/io/test_serialization.cpp index a0e0163..b352526 100644 --- a/tests/io/test_serialization.cpp +++ b/tests/io/test_serialization.cpp @@ -6,25 +6,29 @@ #include #include -#include -#include -#include -#include -#include -#include - #include #include #include #include #include #include +#include +#include +#include +#include +#include +#include using namespace colorcpp; using namespace colorcpp::io::serialization; struct fake_json { - enum class kind { null_value, number, array, object }; + enum class kind { + null_value, + number, + array, + object + }; kind type = kind::null_value; double number_value = 0.0; @@ -33,7 +37,11 @@ struct fake_json { }; struct fake_msgpack_packer { - enum class mode { none, array, map }; + enum class mode { + none, + array, + map + }; mode packed_mode = mode::none; std::vector numbers; @@ -71,7 +79,9 @@ struct colorcpp::io::serialization::json_adapter { arr.array_values.push_back(value); } - static void set(fake_json& obj, std::string_view key, const fake_json& val) { obj.object_values[std::string(key)] = val; } + static void set(fake_json& obj, std::string_view key, const fake_json& val) { + obj.object_values[std::string(key)] = val; + } static void set(fake_json& obj, std::string_view key, double v) { fake_json value; @@ -127,7 +137,9 @@ struct colorcpp::io::serialization::msgpack_packer { } } - static void pack_string(fake_msgpack_packer& p, std::string_view s) { p.key_values.emplace_back(std::string(s), 0.0); } + static void pack_string(fake_msgpack_packer& p, std::string_view s) { + p.key_values.emplace_back(std::string(s), 0.0); + } }; template <> diff --git a/tests/operations/test_conversion.cpp b/tests/operations/test_conversion.cpp index 23fb728..94a9e6c 100644 --- a/tests/operations/test_conversion.cpp +++ b/tests/operations/test_conversion.cpp @@ -148,9 +148,8 @@ TEST(ConversionTest, GraphRoutingPrefersLowerWeightedCanonicalPathToOklab) { static_assert(std::is_same_v, linear_rgbf_t>); const display_p3f_t sample{0.90f, 0.35f, 0.15f}; - const auto expected = - details::xyz_to_oklab(details::linear_display_p3_to_xyz( - details::display_p3_to_linear_display_p3(sample))); + const auto expected = details::xyz_to_oklab( + details::linear_display_p3_to_xyz(details::display_p3_to_linear_display_p3(sample))); const auto casted = color_cast(sample); EXPECT_NEAR(casted.l(), expected.l(), 1e-6f);