From ea0fbfdf2db3819d6cfc2713736e251f5c8f3282 Mon Sep 17 00:00:00 2001 From: "Merlot.Qi" Date: Tue, 16 Jun 2026 17:24:25 +0800 Subject: [PATCH 1/4] feat: implement Adobe RGB and ProPhoto RGB color spaces Add support for Adobe RGB (A98-RGB) with pure Gamma 2.2 transfer function and D65 white point, and ProPhoto RGB with linear transfer function. New header files define color models, channel traits, and conversion functions for both color spaces. --- include/colorcpp/core/adobe_rgb.hpp | 171 +++++++++++++++ include/colorcpp/core/prophoto_rgb.hpp | 179 ++++++++++++++++ .../conversion/functions/adobe_rgb.hpp | 200 ++++++++++++++++++ .../conversion/functions/prophoto_rgb.hpp | 167 +++++++++++++++ 4 files changed, 717 insertions(+) create mode 100644 include/colorcpp/core/adobe_rgb.hpp create mode 100644 include/colorcpp/core/prophoto_rgb.hpp create mode 100644 include/colorcpp/operations/conversion/functions/adobe_rgb.hpp create mode 100644 include/colorcpp/operations/conversion/functions/prophoto_rgb.hpp 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/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/operations/conversion/functions/adobe_rgb.hpp b/include/colorcpp/operations/conversion/functions/adobe_rgb.hpp new file mode 100644 index 0000000..fba381b --- /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 \ No newline at end of file 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..201e4bd --- /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 \ No newline at end of file From f4820640d6ce13361127c06d497c696a74cffb8a Mon Sep 17 00:00:00 2001 From: "Merlot.Qi" Date: Tue, 16 Jun 2026 17:30:11 +0800 Subject: [PATCH 2/4] feat: add Adobe RGB and ProPhoto RGB color space support - Register Adobe RGB, Linear Adobe RGB, ProPhoto RGB, and Linear ProPhoto RGB color traits in the conversion graph, linking them to XYZ via their linear variants. - Include the new color space headers in core and registration files. - Update the color space hierarchy diagram in color_space_registry.hpp to reflect the new spaces. - Fix missing trailing newline in adobe_rgb.hpp conversion functions file. --- include/colorcpp/core/core.hpp | 2 + .../conversion/color_space_registry.hpp | 66 ++++++++++++++++--- .../conversion/functions/adobe_rgb.hpp | 2 +- .../operations/conversion/functions/index.hpp | 49 ++++++++++++++ .../conversion/functions/prophoto_rgb.hpp | 2 +- 5 files changed, 110 insertions(+), 11 deletions(-) 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/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 index fba381b..b56b251 100644 --- a/include/colorcpp/operations/conversion/functions/adobe_rgb.hpp +++ b/include/colorcpp/operations/conversion/functions/adobe_rgb.hpp @@ -197,4 +197,4 @@ constexpr To srgb_to_adobe_rgb(const From& src) { return pack_to(from_unit(r), from_unit(g), from_unit(b)); } -} // namespace colorcpp::operations::conversion::details \ No newline at end of file +} // 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 index 201e4bd..d342609 100644 --- a/include/colorcpp/operations/conversion/functions/prophoto_rgb.hpp +++ b/include/colorcpp/operations/conversion/functions/prophoto_rgb.hpp @@ -164,4 +164,4 @@ constexpr To xyz_to_linear_prophoto_rgb(const From& src) { } } -} // namespace colorcpp::operations::conversion::details \ No newline at end of file +} // namespace colorcpp::operations::conversion::details From bf3d458ad7008bfcfbf3fb782667bfcd1b05b9d3 Mon Sep 17 00:00:00 2001 From: "Merlot.Qi" Date: Wed, 17 Jun 2026 13:55:04 +0800 Subject: [PATCH 3/4] feat(io,conversion): add direct Adobe RGB and ProPhoto RGB support Replace A98-RGB and ProPhoto RGB conversion via XYZ with direct color types (adobe_rgbf_t, prophoto_rgbf_t) in CSS color function parsing. Extend conversion graph with Adobe RGB and ProPhoto RGB color nodes and their linear variants for improved accuracy and efficiency. --- include/colorcpp/io/css/color_function.hpp | 16 ++++++---------- include/colorcpp/operations/conversion/graph.hpp | 6 +++++- 2 files changed, 11 insertions(+), 11 deletions(-) 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/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. From f2c9fdaa48fa61f6ecd79226525a83a382dcb00e Mon Sep 17 00:00:00 2001 From: "Merlot.Qi" Date: Wed, 17 Jun 2026 15:40:52 +0800 Subject: [PATCH 4/4] feat(tests): add tests for Adobe RGB and ProPhoto RGB color models and conversions --- include/colorcpp/io/css/parse_detail.hpp | 22 ++ tests/core/test_adobe_rgb.cpp | 250 +++++++++++++++++++++++ tests/core/test_prophoto_rgb.cpp | 213 +++++++++++++++++++ 3 files changed, 485 insertions(+) create mode 100644 tests/core/test_adobe_rgb.cpp create mode 100644 tests/core/test_prophoto_rgb.cpp 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/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