diff --git a/include/colorcpp/core/adobe_rgb.hpp b/include/colorcpp/core/adobe_rgb.hpp new file mode 100644 index 0000000..dffdea3 --- /dev/null +++ b/include/colorcpp/core/adobe_rgb.hpp @@ -0,0 +1,171 @@ +/** + * @file adobe_rgb.hpp + * @brief Adobe RGB (A98-RGB) color space (D65 white point, pure Gamma 2.2). + * + * Adobe RGB (1998) uses the same D65 white point as sRGB but wider primaries + * covering approximately 50% of the CIE 1931 visible gamut (vs ~35% for sRGB). + * It's a standard working space in photography and design. + * + * Transfer function: pure Gamma 2.2 (no linear segment like sRGB). + * + * @see https://www.adobe.com/digitalimag/pdfs/AdobeRGB1998.pdf + */ + +#pragma once + +#include +#include +#include + +namespace colorcpp::core::adobe_rgb { + +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 adobe_rgb {}; +struct adobe_rgba {}; +struct linear_adobe_rgbf {}; +struct linear_adobe_rgbaf {}; + +} // namespace model + +} // namespace colorcpp::core::adobe_rgb + +namespace colorcpp::traits { + +template <> +struct model_traits { + using channels_type = std::tuple; + static constexpr std::string_view prefix = "adobe-rgb"; + static constexpr std::size_t channel_size = 3; +}; + +template <> +struct model_traits { + using channels_type = std::tuple; + static constexpr std::string_view prefix = "adobe-rgba"; + static constexpr std::size_t channel_size = 4; +}; + +template <> +struct model_traits { + using channels_type = std::tuple; + static constexpr std::string_view prefix = "linear-adobe-rgb"; + static constexpr std::size_t channel_size = 3; +}; + +template <> +struct model_traits { + using channels_type = std::tuple; + static constexpr std::string_view prefix = "linear-adobe-rgba"; + static constexpr std::size_t channel_size = 4; +}; + +} // namespace colorcpp::traits + +namespace colorcpp::core { + +/** + * @brief Adobe RGB / Adobe RGBA with @c r(), @c g(), @c b(), and @c a() when alpha is present. + * @tparam Model adobe_rgb::model::adobe_rgb or adobe_rgba. + */ +template +struct basic_adobe_rgb : 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 Adobe RGB with float channels. */ +using adobe_rgbf_t = basic_adobe_rgb; +/** @brief Adobe RGBA with float channels. */ +using adobe_rgbaf_t = basic_adobe_rgb; +/** @brief Linear Adobe RGB with float channels. */ +using linear_adobe_rgbf_t = basic_adobe_rgb; +/** @brief Linear Adobe RGBA with float channels. */ +using linear_adobe_rgbaf_t = basic_adobe_rgb; + +} // namespace colorcpp::core + +// I/O operators for basic_adobe_rgb +template +std::ostream& operator<<(std::ostream& os, const colorcpp::core::basic_adobe_rgb& c) { + return colorcpp::core::io::operator<<(os, static_cast&>(c)); +} + +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 a4b965d..33eb878 100644 --- a/include/colorcpp/core/core.hpp +++ b/include/colorcpp/core/core.hpp @@ -8,11 +8,13 @@ #pragma once +#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 new file mode 100644 index 0000000..f570a44 --- /dev/null +++ b/include/colorcpp/core/prophoto_rgb.hpp @@ -0,0 +1,179 @@ +/** + * @file prophoto_rgb.hpp + * @brief ProPhoto RGB (ROMM RGB) color space (D50 white point, Gamma 1.8). + * + * ProPhoto RGB is a wide-gamut RGB color space developed by Kodak, using D50 as + * its reference white point. It covers approximately 90% of the CIE L*a*b* gamut, + * making it one of the largest RGB working spaces. It is the internal working space + * of Adobe Lightroom and is specified by ICC as ROMM RGB (Reference Output Medium + * Metric RGB). + * + * Transfer function: Gamma 1.8 with a linear segment below 1/512. + * White point: D50 (0.3457, 0.3585) + * + * NOTE: Because the existing conversion graph uses XYZ(D65) as its central hub, + * conversions between linear ProPhoto RGB and XYZ incorporate a Bradford chromatic + * adaptation step (D50 <-> D65). + * + * @see https://www.color.org/ROMMRGB.pdf + * @see http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html + */ + +#pragma once + +#include +#include +#include + +namespace colorcpp::core::prophoto_rgb { + +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 prophoto_rgb {}; +struct prophoto_rgba {}; +struct linear_prophoto_rgbf {}; +struct linear_prophoto_rgbaf {}; + +} // namespace model + +} // namespace colorcpp::core::prophoto_rgb + +namespace colorcpp::traits { + +template <> +struct model_traits { + using channels_type = std::tuple; + static constexpr std::string_view prefix = "prophoto-rgb"; + static constexpr std::size_t channel_size = 3; +}; + +template <> +struct model_traits { + using channels_type = std::tuple; + static constexpr std::string_view prefix = "prophoto-rgba"; + static constexpr std::size_t channel_size = 4; +}; + +template <> +struct model_traits { + using channels_type = std::tuple; + static constexpr std::string_view prefix = "linear-prophoto-rgb"; + static constexpr std::size_t channel_size = 3; +}; + +template <> +struct model_traits { + using channels_type = std::tuple; + static constexpr std::string_view prefix = "linear-prophoto-rgba"; + static constexpr std::size_t channel_size = 4; +}; + +} // namespace colorcpp::traits + +namespace colorcpp::core { + +/** + * @brief ProPhoto RGB / ProPhoto RGBA with @c r(), @c g(), @c b(), and @c a() when alpha is present. + * @tparam Model prophoto_rgb::model::prophoto_rgb or prophoto_rgba. + */ +template +struct basic_prophoto_rgb : 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 ProPhoto RGB with float channels. */ +using prophoto_rgbf_t = basic_prophoto_rgb; +/** @brief ProPhoto RGBA with float channels. */ +using prophoto_rgbaf_t = basic_prophoto_rgb; +/** @brief Linear ProPhoto RGB with float channels. */ +using linear_prophoto_rgbf_t = basic_prophoto_rgb; +/** @brief Linear ProPhoto RGBA with float channels. */ +using linear_prophoto_rgbaf_t = basic_prophoto_rgb; + +} // namespace colorcpp::core + +// I/O operators for basic_prophoto_rgb +template +std::ostream& operator<<(std::ostream& os, const colorcpp::core::basic_prophoto_rgb& c) { + return colorcpp::core::io::operator<<(os, static_cast&>(c)); +} + +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/io/css/color_function.hpp b/include/colorcpp/io/css/color_function.hpp index 2c2cb10..e2402cb 100644 --- a/include/colorcpp/io/css/color_function.hpp +++ b/include/colorcpp/io/css/color_function.hpp @@ -6,8 +6,10 @@ #pragma once #include +#include #include #include +#include #include #include #include @@ -329,18 +331,12 @@ inline std::optional parsed_color_function_to_color(const parsed_color_fu return color_cast_from_css_source( core::xyz_t{clamp_xyz_channel(xyz.x), clamp_xyz_channel(xyz.y), clamp_xyz_channel(xyz.z)}, a); } - case Space::a98_rgb: { - const auto xyz = linear_a98_to_xyz_d65(gamma_decode_a98(r), gamma_decode_a98(g), gamma_decode_a98(b)); + case Space::a98_rgb: return color_cast_from_css_source( - core::xyz_t{clamp_xyz_channel(xyz.x), clamp_xyz_channel(xyz.y), clamp_xyz_channel(xyz.z)}, a); - } - case Space::prophoto_rgb: { - const auto xyz_d50 = - linear_prophoto_to_xyz_d50(gamma_decode_prophoto(r), gamma_decode_prophoto(g), gamma_decode_prophoto(b)); - const auto xyz = xyz_d50_to_d65(xyz_d50.x, xyz_d50.y, xyz_d50.z); + core::adobe_rgbf_t{clamp_unit_channel(r), clamp_unit_channel(g), clamp_unit_channel(b)}, a); + case Space::prophoto_rgb: return color_cast_from_css_source( - core::xyz_t{clamp_xyz_channel(xyz.x), clamp_xyz_channel(xyz.y), clamp_xyz_channel(xyz.z)}, a); - } + core::prophoto_rgbf_t{clamp_unit_channel(r), clamp_unit_channel(g), clamp_unit_channel(b)}, a); case Space::rec2020: { const auto xyz = linear_rec2020_to_xyz_d65(gamma_decode_rec2020(r), gamma_decode_rec2020(g), gamma_decode_rec2020(b)); diff --git a/include/colorcpp/io/css/parse_detail.hpp b/include/colorcpp/io/css/parse_detail.hpp index faaf040..b58c0eb 100644 --- a/include/colorcpp/io/css/parse_detail.hpp +++ b/include/colorcpp/io/css/parse_detail.hpp @@ -8,9 +8,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -62,6 +64,10 @@ enum class color_mix_space { xyz, display_p3, display_p3_linear, + a98_rgb, + a98_rgb_linear, + prophoto_rgb, + prophoto_rgb_linear, }; struct color_mix_interpolation_method { @@ -201,6 +207,14 @@ inline std::optional parse_color_mix_space(detai space = color_mix_space::display_p3_linear; else if (c.consume_ci("display-p3")) space = color_mix_space::display_p3; + else if (c.consume_ci("a98-rgb-linear")) + space = color_mix_space::a98_rgb_linear; + else if (c.consume_ci("a98-rgb")) + space = color_mix_space::a98_rgb; + else if (c.consume_ci("prophoto-rgb-linear")) + space = color_mix_space::prophoto_rgb_linear; + else if (c.consume_ci("prophoto-rgb")) + space = color_mix_space::prophoto_rgb; else if (c.consume_ci("lab")) space = color_mix_space::lab; else if (c.consume_ci("lch")) @@ -438,6 +452,14 @@ inline core::rgbaf_t mix_colors_in_space(const color_mix_interpolation_method& m return mix_in_rectangular_space(a, b, weights); case color_mix_space::display_p3_linear: return mix_in_rectangular_space(a, b, weights); + case color_mix_space::a98_rgb: + return mix_in_rectangular_space(a, b, weights); + case color_mix_space::a98_rgb_linear: + return mix_in_rectangular_space(a, b, weights); + case color_mix_space::prophoto_rgb: + return mix_in_rectangular_space(a, b, weights); + case color_mix_space::prophoto_rgb_linear: + return mix_in_rectangular_space(a, b, weights); } return mix_in_rectangular_space(a, b, weights); diff --git a/include/colorcpp/operations/conversion/color_space_registry.hpp b/include/colorcpp/operations/conversion/color_space_registry.hpp index da2cd03..3099efa 100644 --- a/include/colorcpp/operations/conversion/color_space_registry.hpp +++ b/include/colorcpp/operations/conversion/color_space_registry.hpp @@ -10,15 +10,17 @@ * ========================== * * XYZ (root hub) - * / | \ - * / | \ - * Linear RGB OkLab CIELAB - * | | | - * sRGB OkLCH CIELCH - * / | \ - * HSL HSV HWB CMYK - * - * Display P3 → Linear RGB → XYZ + * / | \ \ + * / | \ \-----------\ + * Linear RGB OkLab CIELAB \ + * | | | \ + * sRGB OkLCH CIELCH \ + * / | \ \ + * HSL HSV HWB CMYK \ + * \ + * Display P3 → Linear Display P3 → XYZ \ + * Adobe RGB → Linear Adobe RGB → XYZ \ + * ProPhoto → Linear ProPhoto → XYZ (Bradford D50↔D65) * * These relationships provide metadata and compatibility support for the * registered conversion graph rather than defining the public dispatch contract. @@ -26,10 +28,12 @@ #pragma once +#include #include #include #include #include +#include #include #include #include @@ -169,4 +173,48 @@ struct color_traits { using hub_type = core::xyz_t; }; +// Adobe RGB uses Linear Adobe RGB as hub +template <> +struct color_traits { + using hub_type = core::linear_adobe_rgbf_t; +}; + +template <> +struct color_traits { + using hub_type = core::linear_adobe_rgbaf_t; +}; + +// Linear Adobe RGB uses XYZ as hub +template <> +struct color_traits { + using hub_type = core::xyz_t; +}; + +template <> +struct color_traits { + using hub_type = core::xyz_t; +}; + +// ProPhoto RGB uses Linear ProPhoto RGB as hub +template <> +struct color_traits { + using hub_type = core::linear_prophoto_rgbf_t; +}; + +template <> +struct color_traits { + using hub_type = core::linear_prophoto_rgbaf_t; +}; + +// Linear ProPhoto RGB 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 new file mode 100644 index 0000000..b56b251 --- /dev/null +++ b/include/colorcpp/operations/conversion/functions/adobe_rgb.hpp @@ -0,0 +1,200 @@ +/** + * @file adobe_rgb.hpp + * @brief Adobe RGB (A98-RGB) conversion functions. + * + * Adobe RGB uses pure Gamma 2.2 transfer function (no linear segment like sRGB). + * White point: D65 (same as sRGB/XYZ hub, no chromatic adaptation needed). + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace colorcpp::operations::conversion::details { + +/** + * @brief Convert Adobe RGB gamma-encoded color to Linear Adobe RGB. + * + * Applies pure Gamma 2.2 decompression (inverse EOTF). + * Adobe RGB uses the same D65 white point as sRGB. + * + * @tparam To Target Linear Adobe RGB type + * @tparam From Source Adobe RGB type + */ +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); + }; + 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>())); + 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 Adobe RGB to gamma-encoded Adobe RGB. + * + * Applies pure Gamma 2.2 compression (EOTF). + * + * @tparam To Target Adobe RGB type + * @tparam From Source Linear Adobe RGB type + */ +template +constexpr To linear_adobe_rgb_to_adobe_rgb(const From& src) { + // Pure Gamma 2.2 encoding (inverse of linearization) + auto gamma_encode = [](float v) noexcept { + v = std::clamp(v, 0.0f, 1.0f); + return std::pow(v, 1.0f / 2.2f); + }; + float r = gamma_encode(to_unit(src.template get_index<0>())); + float g = gamma_encode(to_unit(src.template get_index<1>())); + float b = gamma_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 Adobe RGB to CIE XYZ (D65). + * + * Reference matrix from Bruce Lindbloom: + * http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html + * + * Adobe RGB (1998) primaries with D65 white point. + */ +template +constexpr To linear_adobe_rgb_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.5766690f * r + 0.1855580f * g + 0.1882290f * b; + float y = 0.2973450f * r + 0.6273640f * g + 0.0752910f * b; + float z = 0.0270310f * r + 0.0706890f * g + 0.9913380f * b; + + 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)); + } else { + return pack_to(from_unit(x), from_unit(y), from_unit(z)); + } +} + +/** + * @brief Convert CIE XYZ (D65) to Linear Adobe RGB. + * + * Reference matrix from Bruce Lindbloom (inverse of above). + */ +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>()); + + float r = 2.0413690f * x - 0.5649654f * y - 0.3446945f * z; + float g = -0.9692660f * x + 1.8760108f * y + 0.0415560f * z; + float b = 0.0134474f * x - 0.1183897f * y + 1.0154096f * 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 Adobe RGB directly to sRGB. + * + * Optimized direct conversion: Linearize (Gamma 2.2) → matrix → sRGB gamma encode. + * Avoids intermediate XYZ object creation. + * + * @tparam To Target sRGB type + * @tparam From Source Adobe RGB type + */ +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); + }; + 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: Adobe RGB linear → sRGB linear + // Computed as: M_xyz_to_srgb_linear × M_adobe_to_xyz + float r_srgb_lin = 1.398f * r_lin - 0.399f * g_lin + 0.001f * b_lin; + float g_srgb_lin = 0.000f * r_lin + 1.000f * g_lin + 0.000f * b_lin; + float b_srgb_lin = 0.000f * r_lin - 0.043f * g_lin + 1.043f * 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 Adobe RGB. + * + * Optimized direct conversion: sRGB linearize → inverse matrix → Gamma 2.2 encode. + * + * @tparam To Target Adobe RGB type + * @tparam From Source sRGB type + */ +template +constexpr To srgb_to_adobe_rgb(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: sRGB linear → Adobe RGB linear (inverse of above) + float r_adbe_lin = 0.715f * r_lin + 0.285f * g_lin + 0.000f * b_lin; + float g_adbe_lin = 0.000f * r_lin + 1.000f * g_lin + 0.000f * b_lin; + float b_adbe_lin = 0.000f * r_lin + 0.041f * g_lin + 0.959f * b_lin; + + // Step 3: Gamma encode (Adobe RGB Gamma 2.2) + auto gamma_encode = [](float v) noexcept { + v = std::clamp(v, 0.0f, 1.0f); + return std::pow(v, 1.0f / 2.2f); + }; + float r = gamma_encode(r_adbe_lin); + float g = gamma_encode(g_adbe_lin); + float b = gamma_encode(b_adbe_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/functions/index.hpp b/include/colorcpp/operations/conversion/functions/index.hpp index 17a48ca..f6eb42a 100644 --- a/include/colorcpp/operations/conversion/functions/index.hpp +++ b/include/colorcpp/operations/conversion/functions/index.hpp @@ -6,10 +6,12 @@ #pragma once // Include all conversion function files +#include #include #include #include #include +#include #include #include #include @@ -210,4 +212,51 @@ COLORCPP_REGISTER_CONVERSION_BIDIR_WEIGHTED(core::hwba_float_t, core::linear_rgb details::linear_rgb_to_hwb, route_cost::shortcut_2_hop, route_cost::shortcut_2_hop) +// Adobe RGB ↔ Linear Adobe RGB +COLORCPP_REGISTER_CONVERSION_BIDIR(core::adobe_rgbf_t, core::linear_adobe_rgbf_t, + details::adobe_rgb_to_linear_adobe_rgb, + details::linear_adobe_rgb_to_adobe_rgb) + +COLORCPP_REGISTER_CONVERSION_BIDIR(core::adobe_rgbaf_t, core::linear_adobe_rgbaf_t, + details::adobe_rgb_to_linear_adobe_rgb, + details::linear_adobe_rgb_to_adobe_rgb) + +// Linear Adobe RGB ↔ XYZ +COLORCPP_REGISTER_CONVERSION_BIDIR(core::linear_adobe_rgbf_t, core::xyz_t, + details::linear_adobe_rgb_to_xyz, + details::xyz_to_linear_adobe_rgb) + +COLORCPP_REGISTER_CONVERSION_BIDIR(core::linear_adobe_rgbaf_t, core::xyz_t, + details::linear_adobe_rgb_to_xyz, + 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_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) + +// ProPhoto RGB ↔ Linear ProPhoto RGB +COLORCPP_REGISTER_CONVERSION_BIDIR(core::prophoto_rgbf_t, core::linear_prophoto_rgbf_t, + details::prophoto_rgb_to_linear_prophoto_rgb, + details::linear_prophoto_rgb_to_prophoto_rgb) + +COLORCPP_REGISTER_CONVERSION_BIDIR(core::prophoto_rgbaf_t, core::linear_prophoto_rgbaf_t, + details::prophoto_rgb_to_linear_prophoto_rgb, + details::linear_prophoto_rgb_to_prophoto_rgb) + +// Linear ProPhoto RGB ↔ XYZ +COLORCPP_REGISTER_CONVERSION_BIDIR(core::linear_prophoto_rgbf_t, core::xyz_t, + details::linear_prophoto_rgb_to_xyz, + details::xyz_to_linear_prophoto_rgb) + +COLORCPP_REGISTER_CONVERSION_BIDIR(core::linear_prophoto_rgbaf_t, core::xyz_t, + details::linear_prophoto_rgb_to_xyz, + details::xyz_to_linear_prophoto_rgb) + } // namespace colorcpp::operations::conversion diff --git a/include/colorcpp/operations/conversion/functions/prophoto_rgb.hpp b/include/colorcpp/operations/conversion/functions/prophoto_rgb.hpp new file mode 100644 index 0000000..d342609 --- /dev/null +++ b/include/colorcpp/operations/conversion/functions/prophoto_rgb.hpp @@ -0,0 +1,167 @@ +/** + * @file prophoto_rgb.hpp + * @brief ProPhoto RGB (ROMM RGB) conversion functions. + * + * ProPhoto RGB uses Gamma 1.8 with a linear segment below 1/512. + * White point: D50 (differs from XYZ(D65) hub, requiring Bradford adaptation). + * + * Conversion path: + * ProPhoto gamma → Linear ProPhoto (D50) → [Bradford D50→D65] → XYZ(D65) + * XYZ(D65) → [Bradford D65→D50] → Linear ProPhoto (D50) → ProPhoto gamma + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace colorcpp::operations::conversion::details { + +/** + * @brief ProPhoto RGB transfer function constants. + */ +namespace prophoto_tf { + +// Linear segment cutoff on the linear side +constexpr float linear_cutoff = 1.0f / 512.0f; // ≈ 0.001953125 + +// Linear segment cutoff on the encoded side (inverse) +constexpr float encoded_cutoff = 1.0f / 32.0f; // ≈ 0.03125 + +constexpr float gamma = 1.8f; + +/** + * @brief Linearize a ProPhoto RGB encoded channel value. + * + * ProPhoto transfer: below 1/512 use linear segment, otherwise power 1.8. + */ +constexpr float to_linear(float v) noexcept { + if (v < encoded_cutoff) + return v / 16.0f; + else + return std::pow(v, gamma); +} + +/** + * @brief Encode a linear ProPhoto RGB channel value. + */ +constexpr float from_linear(float v) noexcept { + if (v < linear_cutoff) + return v * 16.0f; + else + return std::pow(v, 1.0f / gamma); +} + +} // namespace prophoto_tf + +/** + * @brief Convert ProPhoto RGB gamma-encoded color to Linear ProPhoto RGB. + * + * Applies ProPhoto transfer (Gamma 1.8 with linear shadow segment). + * + * @tparam To Target Linear ProPhoto RGB type + * @tparam From Source ProPhoto RGB type + */ +template +constexpr To prophoto_rgb_to_linear_prophoto_rgb(const From& src) { + float r = prophoto_tf::to_linear(to_unit(src.template get_index<0>())); + float g = prophoto_tf::to_linear(to_unit(src.template get_index<1>())); + float b = prophoto_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 ProPhoto RGB to gamma-encoded ProPhoto RGB. + * + * Applies inverse ProPhoto transfer (power 1/1.8 with linear shadow segment). + * + * @tparam To Target ProPhoto RGB type + * @tparam From Source Linear ProPhoto RGB type + */ +template +constexpr To linear_prophoto_rgb_to_prophoto_rgb(const From& src) { + auto encode = [](float v) noexcept { + v = std::clamp(v, 0.0f, 1.0f); + return prophoto_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 ProPhoto RGB (D50) to CIE XYZ (D65). + * + * This conversion combines two steps: + * 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 + */ +template +constexpr To linear_prophoto_rgb_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>()); + + // 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; + + 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)); + } else { + return pack_to(from_unit(x), from_unit(y), from_unit(z)); + } +} + +/** + * @brief Convert CIE XYZ (D65) to Linear ProPhoto RGB (D50). + * + * This conversion combines two steps: + * 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; + + 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)); + } +} + +} // namespace colorcpp::operations::conversion::details diff --git a/include/colorcpp/operations/conversion/graph.hpp b/include/colorcpp/operations/conversion/graph.hpp index a12b450..fe8a5ea 100644 --- a/include/colorcpp/operations/conversion/graph.hpp +++ b/include/colorcpp/operations/conversion/graph.hpp @@ -9,6 +9,7 @@ #pragma once #include +#include #include #include #include @@ -17,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -193,7 +195,9 @@ using builtin_color_nodes = core::xyz_t, core::cielab_t, core::cielch_t, core::oklab_t, core::oklch_t, core::hsl_float_t, core::hsla_float_t, core::hsv_float_t, core::hsva_float_t, core::hwb_float_t, core::hwba_float_t, 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::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>; /** * @brief Global color graph node set including user extensions. diff --git a/tests/core/test_adobe_rgb.cpp b/tests/core/test_adobe_rgb.cpp new file mode 100644 index 0000000..4a065a6 --- /dev/null +++ b/tests/core/test_adobe_rgb.cpp @@ -0,0 +1,250 @@ +/** + * @file test_adobe_rgb.cpp + * @brief Tests for Adobe RGB (A98-RGB) color model and conversion. + * + * Adobe RGB uses pure Gamma 2.2 transfer function and D65 white point. + */ + +#include + +#include +#include +#include +#include + +namespace colorcpp::core::test { + +using namespace colorcpp::operations::conversion; + +// --- Construction --- + +TEST(AdobeRGBTest, DefaultConstruction) { + adobe_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(AdobeRGBTest, ParameterizedConstruction) { + adobe_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(AdobeRGBATest, ParameterizedWithAlpha) { + adobe_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(AdobeRGBTest, BoundaryValuesMin) { + adobe_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(AdobeRGBTest, BoundaryValuesMax) { + adobe_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(AdobeRGBTest, OutOfRangeThrows) { + EXPECT_THROW(adobe_rgbf_t(-0.1f, 0.5f, 0.5f), std::out_of_range); + EXPECT_THROW(adobe_rgbf_t(1.1f, 0.5f, 0.5f), std::out_of_range); + EXPECT_THROW(adobe_rgbf_t(0.5f, -0.1f, 0.5f), std::out_of_range); + EXPECT_THROW(adobe_rgbf_t(0.5f, 1.1f, 0.5f), std::out_of_range); + EXPECT_THROW(adobe_rgbf_t(0.5f, 0.5f, -0.1f), std::out_of_range); + EXPECT_THROW(adobe_rgbf_t(0.5f, 0.5f, 1.1f), std::out_of_range); +} + +// --- Member and indexed access --- + +TEST(AdobeRGBTest, MutableMemberAccess) { + adobe_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(AdobeRGBTest, IndexAccess) { + adobe_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(AdobeRGBTest, ConstCorrectness) { + const adobe_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(AdobeRGBTest, CopyConstructors) { + adobe_rgbf_t original(0.2f, 0.4f, 0.6f); + adobe_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(AdobeRGBTest, CopyAssignment) { + adobe_rgbf_t original(0.2f, 0.4f, 0.6f); + adobe_rgbf_t copy(0.0f, 0.0f, 0.0f); + copy = original; + EXPECT_FLOAT_EQ(copy.r(), 0.2f); + EXPECT_FLOAT_EQ(copy.g(), 0.4f); + EXPECT_FLOAT_EQ(copy.b(), 0.6f); +} + +TEST(AdobeRGBTest, EqualityComparison) { + adobe_rgbf_t c1(0.2f, 0.4f, 0.6f); + adobe_rgbf_t c2(0.2f, 0.4f, 0.6f); + adobe_rgbf_t c3(0.3f, 0.4f, 0.6f); + EXPECT_TRUE(c1 == c2); + EXPECT_FALSE(c1 == c3); + EXPECT_TRUE(c1 != c3); +} + +// --- Type traits --- + +TEST(AdobeRGBTest, TypeTraits) { + using channels = typename traits::model_traits::channels_type; + static_assert(std::tuple_size_v == 3); + + using a_channels = typename traits::model_traits::channels_type; + static_assert(std::tuple_size_v == 4); + + static_assert(!traits::has_channel_tag_v); + static_assert(traits::has_channel_tag_v); +} + +// --- sRGB <-> Adobe RGB conversions --- + +TEST(AdobeRGBConversionTest, BlackRemainsBlack) { + auto a = color_cast(rgbf_t{0.0f, 0.0f, 0.0f}); + EXPECT_NEAR(a.r(), 0.0f, 1e-4f); + EXPECT_NEAR(a.g(), 0.0f, 1e-4f); + EXPECT_NEAR(a.b(), 0.0f, 1e-4f); +} + +TEST(AdobeRGBConversionTest, FromSRGBViaCanonicalPath) { + auto a = color_cast(rgbf_t{0.2f, 0.5f, 0.8f}); + // Verify conversion produces expected non-zero results + EXPECT_GT(a.r(), 0.0f); + EXPECT_GT(a.g(), 0.0f); + EXPECT_GT(a.b(), 0.0f); + EXPECT_LT(a.r(), 1.0f); + EXPECT_LT(a.g(), 1.0f); + EXPECT_LT(a.b(), 1.0f); +} + +// Alpha through graph routing uses direct edge via linear_adobe_rgbaf_t -> rgbaf_t +// (XYZ hub discards alpha, same limitation as Display P3A) +TEST(AdobeRGBConversionTest, AlphaPreservedViaLinear) { + adobe_rgbaf_t orig(0.5f, 0.3f, 0.7f, 0.6f); + auto linear = color_cast(orig); + EXPECT_NEAR(linear.a(), 0.6f, 1e-4f); +} + +// --- Linear Adobe RGB conversions --- + +TEST(AdobeRGBLinearTest, GammaCorrected) { + adobe_rgbf_t gamma(0.5f, 0.5f, 0.5f); + auto linear = color_cast(gamma); + EXPECT_LT(linear.r(), 0.5f); +} + +TEST(AdobeRGBLinearTest, LinearRoundTrip) { + adobe_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(AdobeRGBConversionTest, ToDisplayP3AndBack) { + adobe_rgbf_t orig(0.4f, 0.2f, 0.7f); + auto dp3 = color_cast(orig); + auto back = color_cast(dp3); + 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(AdobeRGBConversionTest, ToOklabAndBack) { + adobe_rgbf_t orig(0.3f, 0.6f, 0.2f); + auto ok = color_cast(orig); + auto back = color_cast(ok); + 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(AdobeRGBConversionTest, ToXYZAndBack) { + adobe_rgbf_t orig(0.25f, 0.45f, 0.85f); + 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); +} + +// --- CSS parsing support --- + +TEST(AdobeRGBCSSTest, ColorFunctionAsRgba8) { + auto c = colorcpp::io::css::parse_css_color_rgba8("color(a98-rgb 0.5 0.3 0.7)"); + ASSERT_TRUE(c.has_value()); + EXPECT_GT(c->get_index<0>(), 0); + EXPECT_LT(c->get_index<2>(), 255); +} + +TEST(AdobeRGBCSSTest, ColorFunctionWithAlpha) { + auto c = colorcpp::io::css::parse_css_color_rgba8("color(a98-rgb 0.2 0.4 0.6 / 0.5)"); + ASSERT_TRUE(c.has_value()); + EXPECT_EQ(c->get_index<3>(), 128); +} + +TEST(AdobeRGBCSSTest, ColorFunctionAsTyped) { + colorcpp::io::css::details::Cursor cursor{"color(a98-rgb 0.3 0.5 0.7)", 0}; + auto c = colorcpp::io::css::parse_color_function_as(cursor); + ASSERT_TRUE(c.has_value()); + EXPECT_NEAR(c->r(), 0.3f, 1e-4f); + EXPECT_NEAR(c->g(), 0.5f, 1e-4f); + EXPECT_NEAR(c->b(), 0.7f, 1e-4f); +} + +TEST(AdobeRGBCSSTest, ColorMixInA98RGB) { + auto c = colorcpp::io::css::parse_css_color_rgbaf("color-mix(in a98-rgb, red, blue)"); + ASSERT_TRUE(c.has_value()); + EXPECT_GT(c->get_index<0>(), 0.0f); + EXPECT_LT(c->get_index<2>(), 1.0f); +} + +TEST(AdobeRGBCSSTest, ColorMixInA98RGBLinear) { + auto c = colorcpp::io::css::parse_css_color_rgbaf("color-mix(in a98-rgb-linear, red, blue)"); + ASSERT_TRUE(c.has_value()); +} + +TEST(AdobeRGBCSSTest, ColorMixProgressForm) { + auto c = colorcpp::io::css::parse_css_color_rgbaf("color-mix(25% in a98-rgb, red, blue)"); + ASSERT_TRUE(c.has_value()); +} + +} // namespace colorcpp::core::test \ No newline at end of file diff --git a/tests/core/test_prophoto_rgb.cpp b/tests/core/test_prophoto_rgb.cpp new file mode 100644 index 0000000..a7bd83e --- /dev/null +++ b/tests/core/test_prophoto_rgb.cpp @@ -0,0 +1,213 @@ +/** + * @file test_prophoto_rgb.cpp + * @brief Tests for ProPhoto RGB (ROMM RGB) color model and conversion. + * + * ProPhoto RGB uses Gamma 1.8 with linear segment, D50 white point, + * and Bradford chromatic adaptation for D50↔D65 conversion. + */ + +#include + +#include +#include +#include +#include + +namespace colorcpp::core::test { + +using namespace colorcpp::operations::conversion; + +// --- Construction --- + +TEST(ProPhotoRGBTest, DefaultConstruction) { + prophoto_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(ProPhotoRGBTest, ParameterizedConstruction) { + prophoto_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(ProPhotoRGBATest, ParameterizedWithAlpha) { + prophoto_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(ProPhotoRGBTest, BoundaryValuesMin) { + prophoto_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(ProPhotoRGBTest, BoundaryValuesMax) { + prophoto_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(ProPhotoRGBTest, OutOfRangeThrows) { + EXPECT_THROW(prophoto_rgbf_t(-0.1f, 0.5f, 0.5f), std::out_of_range); + EXPECT_THROW(prophoto_rgbf_t(1.1f, 0.5f, 0.5f), std::out_of_range); +} + +TEST(ProPhotoRGBTest, MutableMemberAccess) { + prophoto_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(ProPhotoRGBTest, IndexAccess) { + prophoto_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); +} + +// --- Type traits --- + +TEST(ProPhotoRGBTest, TypeTraits) { + using channels = typename traits::model_traits::channels_type; + static_assert(std::tuple_size_v == 3); + + using a_channels = typename traits::model_traits::channels_type; + static_assert(std::tuple_size_v == 4); +} + +// --- sRGB <-> ProPhoto RGB conversions --- + +TEST(ProPhotoRGBConversionTest, BlackRemainsBlack) { + auto c = color_cast(rgbf_t{0.0f, 0.0f, 0.0f}); + EXPECT_NEAR(c.r(), 0.0f, 1e-4f); + EXPECT_NEAR(c.g(), 0.0f, 1e-4f); + EXPECT_NEAR(c.b(), 0.0f, 1e-4f); +} + +TEST(ProPhotoRGBConversionTest, FromSRGBViaCanonicalPath) { + auto c = color_cast(rgbf_t{0.2f, 0.5f, 0.8f}); + EXPECT_GT(c.r(), 0.0f); + EXPECT_GT(c.g(), 0.0f); + EXPECT_GT(c.b(), 0.0f); + EXPECT_LT(c.r(), 1.0f); + EXPECT_LT(c.g(), 1.0f); + EXPECT_LT(c.b(), 1.0f); +} + +// Alpha through graph routing (XYZ hub discards alpha, same limitation as Display P3A) +TEST(ProPhotoRGBConversionTest, AlphaPreservedViaLinear) { + prophoto_rgbaf_t orig(0.5f, 0.3f, 0.7f, 0.6f); + auto linear = color_cast(orig); + EXPECT_NEAR(linear.a(), 0.6f, 1e-4f); +} + +// --- Linear ProPhoto RGB conversions --- + +TEST(ProPhotoRGBLinearTest, GammaCorrected) { + prophoto_rgbf_t gamma(0.5f, 0.5f, 0.5f); + auto linear = color_cast(gamma); + EXPECT_LT(linear.r(), 0.5f); +} + +TEST(ProPhotoRGBLinearTest, LinearRoundTrip) { + prophoto_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 (looser tolerance via graph routing) --- + +TEST(ProPhotoRGBConversionTest, ToDisplayP3AndBack) { + prophoto_rgbf_t orig(0.4f, 0.2f, 0.7f); + auto dp3 = color_cast(orig); + auto back = color_cast(dp3); + // Multi-hop graph routing through Bradford adaptation + EXPECT_GT(back.r(), 0.0f); + EXPECT_GT(back.g(), 0.0f); + EXPECT_GT(back.b(), 0.0f); + EXPECT_LT(back.r(), 1.0f); + EXPECT_LT(back.g(), 1.0f); + EXPECT_LT(back.b(), 1.0f); +} + +TEST(ProPhotoRGBConversionTest, ToOklabAndBack) { + prophoto_rgbf_t orig(0.3f, 0.6f, 0.2f); + auto ok = color_cast(orig); + auto back = color_cast(ok); + EXPECT_GT(back.r(), 0.0f); + EXPECT_GT(back.g(), 0.0f); + EXPECT_GT(back.b(), 0.0f); +} + +TEST(ProPhotoRGBConversionTest, ToXYZAndBack) { + prophoto_rgbf_t orig(0.25f, 0.45f, 0.85f); + auto xyz = color_cast(orig); + auto back = color_cast(xyz); + // ProPhoto → XYZ → ProPhoto goes through Bradford D50↔D65 adaptation + EXPECT_GT(back.r(), 0.0f); + EXPECT_GT(back.g(), 0.0f); + EXPECT_GT(back.b(), 0.0f); + EXPECT_LT(back.r(), 1.0f); + EXPECT_LT(back.g(), 1.0f); + EXPECT_LT(back.b(), 1.0f); +} + +// --- CSS parsing support --- + +TEST(ProPhotoRGBCSSTest, ColorFunctionAsRgba8) { + auto c = colorcpp::io::css::parse_css_color_rgba8("color(prophoto-rgb 0.5 0.3 0.7)"); + ASSERT_TRUE(c.has_value()); + EXPECT_GT(c->get_index<0>(), 0); + EXPECT_LT(c->get_index<2>(), 255); +} + +TEST(ProPhotoRGBCSSTest, ColorFunctionWithAlpha) { + auto c = colorcpp::io::css::parse_css_color_rgba8("color(prophoto-rgb 0.2 0.4 0.6 / 0.5)"); + ASSERT_TRUE(c.has_value()); + EXPECT_EQ(c->get_index<3>(), 128); +} + +TEST(ProPhotoRGBCSSTest, ColorFunctionAsTyped) { + colorcpp::io::css::details::Cursor cursor{"color(prophoto-rgb 0.3 0.5 0.7)", 0}; + auto c = colorcpp::io::css::parse_color_function_as(cursor); + ASSERT_TRUE(c.has_value()); + EXPECT_NEAR(c->r(), 0.3f, 1e-4f); + EXPECT_NEAR(c->g(), 0.5f, 1e-4f); + EXPECT_NEAR(c->b(), 0.7f, 1e-4f); +} + +TEST(ProPhotoRGBCSSTest, ColorMixInProPhoto) { + auto c = colorcpp::io::css::parse_css_color_rgbaf("color-mix(in prophoto-rgb, red, blue)"); + ASSERT_TRUE(c.has_value()); + EXPECT_GT(c->get_index<0>(), 0.0f); + EXPECT_LT(c->get_index<2>(), 1.0f); +} + +TEST(ProPhotoRGBCSSTest, ColorMixInProPhotoLinear) { + auto c = colorcpp::io::css::parse_css_color_rgbaf("color-mix(in prophoto-rgb-linear, red, blue)"); + ASSERT_TRUE(c.has_value()); +} + +TEST(ProPhotoRGBCSSTest, ColorMixProgressForm) { + auto c = colorcpp::io::css::parse_css_color_rgbaf("color-mix(25% in prophoto-rgb, red, blue)"); + ASSERT_TRUE(c.has_value()); +} + +} // namespace colorcpp::core::test \ No newline at end of file