From 65408124d90a26eeeae8c9d116e57f709899c9bf Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Sun, 9 Aug 2026 15:44:11 +0200 Subject: [PATCH] feat(color): Add srgb_rec709_display as a built-in color space srgb_rec709_display is now a built-in color space alongside srgb_rec709_scene, and config color spaces like "sRGB - Display" are now classified as such. The built-in sRGB name remains an alias of srgb_rec709_scene. This means it can be relied on to be available, even when using an older OCIO config that does not contain it. And is on equal footing with srgb_rec709_scene in case it becomes the default in the future. Signed-off-by: Brecht Van Lommel --- src/libOpenImageIO/color_ocio.cpp | 108 ++++++++++++------ .../python-colorconfig/ref/out-ocio230.txt | 3 + .../python-colorconfig/ref/out-ocio230b.txt | 3 + .../python-colorconfig/ref/out-ocio232.txt | 3 + .../python-colorconfig/ref/out-ocio24.txt | 3 + .../python-colorconfig/ref/out-ocio25.txt | 3 + testsuite/python-colorconfig/ref/out.txt | 3 + .../src/test_colorconfig.py | 3 + 8 files changed, 94 insertions(+), 35 deletions(-) diff --git a/src/libOpenImageIO/color_ocio.cpp b/src/libOpenImageIO/color_ocio.cpp index a0935f06b4..841d94ddae 100644 --- a/src/libOpenImageIO/color_ocio.cpp +++ b/src/libOpenImageIO/color_ocio.cpp @@ -159,14 +159,17 @@ struct CSInfo { int index; // More than one can have the same index -- aliases enum Flags { none = 0, - is_linear_response = 1, // any cs with linear transfer function - is_scene_linear = 2, // equivalent to scene_linear - is_srgb = 4, // sRGB (primaries, and transfer function) - is_lin_srgb = 8, // sRGB/Rec709 primaries, linear response - is_ACEScg = 16, // ACEScg - is_Rec709 = 32, // Rec709 primaries and transfer function - is_data = 64, // Non-color-managed data - is_known = is_srgb | is_lin_srgb | is_ACEScg | is_Rec709 + is_linear_response = 1, // any cs with linear transfer function + is_scene_linear = 2, // equivalent to scene_linear + is_srgb_display = 4, // sRGB (primaries, and transfer function) display + is_srgb_scene = 8, // sRGB (primaries, and transfer function) scene + is_lin_srgb = 16, // sRGB/Rec709 primaries, linear response + is_ACEScg = 32, // ACEScg + is_Rec709 = 64, // Rec709 primaries and transfer function + is_data = 128, // Non-color-managed data + is_known = is_srgb_display | is_srgb_scene | is_lin_srgb | is_ACEScg + | is_Rec709 + }; int m_flags = 0; bool examined = false; @@ -208,7 +211,8 @@ class ColorConfig::Impl { std::vector colorspaces; std::string scene_linear_alias; // Alias for a scene-linear color space std::string lin_srgb_alias; - std::string srgb_alias; + std::string srgb_display_alias; + std::string srgb_scene_alias; std::string ACEScg_alias; std::string Rec709_alias; mutable spin_rw_mutex m_mutex; @@ -406,9 +410,9 @@ class ColorConfig::Impl { void debug_print_aliases() { - DBG("Aliases: scene_linear={} lin_srgb={} srgb={} ACEScg={} Rec709={}\n", - scene_linear_alias, lin_srgb_alias, srgb_alias, ACEScg_alias, - Rec709_alias); + DBG("Aliases: scene_linear={} lin_srgb={} srgb_display={} srgb_scene={} ACEScg={} Rec709={}\n", + scene_linear_alias, lin_srgb_alias, srgb_display_alias, + srgb_scene_alias, ACEScg_alias, Rec709_alias); } // For OCIO 2.3+, we can ask for the equivalent of some built-in @@ -426,7 +430,7 @@ class ColorConfig::Impl { // ColorConfig utility to take inventory of the color spaces available. -// It sets up knowledge of "linear", "srgb_rec709_scene", "Rec709", etc, +// It sets up knowledge of "linear", "srgb_rec709_display", "Rec709", etc, // even if the underlying OCIO configuration lacks them. void ColorConfig::Impl::inventory() @@ -474,8 +478,9 @@ ColorConfig::Impl::inventory() add("lin_rec709_scene", 0, linflags); add("lin_srgb", 0, linflags); add("lin_rec709", 0, linflags); - add("srgb_rec709_scene", 1, CSInfo::is_srgb); - add("sRGB", 1, CSInfo::is_srgb); + add("srgb_rec709_display", 1, CSInfo::is_srgb_display); + add("srgb_rec709_scene", 1, CSInfo::is_srgb_scene); + add("sRGB", 1, CSInfo::is_srgb_scene); add("Rec709", 2, CSInfo::is_Rec709); for (auto&& cs : colorspaces) @@ -589,14 +594,17 @@ ColorConfig::Impl::classify_by_name(CSInfo& cs) // General heuristics based on the names -- for a few canonical names, // believe them! Woe be unto the poor soul who names a color space "sRGB" // or "ACEScg" and it's really something entirely different. - if (Strutil::iequals(cs.name, "srgb_rec709_scene") - || Strutil::iequals(cs.name, "srgb_tx") - || Strutil::iequals(cs.name, "srgb_texture") - || Strutil::iequals(cs.name, "srgb texture") - || Strutil::iequals(cs.name, "srgb_rec709_scene") - || Strutil::iequals(cs.name, "sRGB - Texture") - || Strutil::iequals(cs.name, "sRGB")) { - cs.setflag(CSInfo::is_srgb, srgb_alias); + if (Strutil::iequals(cs.name, "srgb_rec709_display") + || Strutil::iequals(cs.name, "srgb_display") + || Strutil::iequals(cs.name, "sRGB - Display")) { + cs.setflag(CSInfo::is_srgb_display, srgb_display_alias); + } else if (Strutil::iequals(cs.name, "srgb_rec709_scene") + || Strutil::iequals(cs.name, "srgb_tx") + || Strutil::iequals(cs.name, "srgb_texture") + || Strutil::iequals(cs.name, "srgb texture") + || Strutil::iequals(cs.name, "sRGB - Texture") + || Strutil::iequals(cs.name, "sRGB")) { + cs.setflag(CSInfo::is_srgb_scene, srgb_scene_alias); } else if (Strutil::iequals(cs.name, "lin_rec709_scene") || Strutil::iequals(cs.name, "lin_rec709") || Strutil::iequals(cs.name, "Linear Rec.709 (sRGB)") @@ -622,7 +630,7 @@ ColorConfig::Impl::classify_by_name(CSInfo& cs) ACEScg_alias); } else if (cs.name == "srgbf" || cs.name == "srgbh" || cs.name == "srgb16" || cs.name == "srgb8") { - cs.setflag(CSInfo::is_srgb, srgb_alias); + cs.setflag(CSInfo::is_srgb_scene, srgb_scene_alias); } else if (cs.name == "srgblnf" || cs.name == "srgblnh" || cs.name == "srgbln16" || cs.name == "srgbln8") { cs.setflag(CSInfo::is_lin_srgb, lin_srgb_alias); @@ -633,7 +641,9 @@ ColorConfig::Impl::classify_by_name(CSInfo& cs) #endif // Set up some canonical names - if (cs.flags() & CSInfo::is_srgb) + if (cs.flags() & CSInfo::is_srgb_display) + cs.canonical = "srgb_rec709_display"; + else if (cs.flags() & CSInfo::is_srgb_scene) cs.canonical = "srgb_rec709_scene"; else if (cs.flags() & CSInfo::is_lin_srgb) cs.canonical = "lin_rec709_scene"; @@ -685,9 +695,12 @@ ColorConfig::Impl::classify_by_conversions(CSInfo& cs) // inversion costs, and they're not gonna be our favourite // canonical spaces anyway. // DBG("{} has LUT3\n", cs.name); + } else if (check_same_as_builtin_transform(cs.name.c_str(), + "srgb_display")) { + cs.setflag(CSInfo::is_srgb_display, srgb_display_alias); } else if (check_same_as_builtin_transform(cs.name.c_str(), "srgb_tx")) { - cs.setflag(CSInfo::is_srgb, srgb_alias); + cs.setflag(CSInfo::is_srgb_scene, srgb_scene_alias); } else if (check_same_as_builtin_transform(cs.name.c_str(), "lin_srgb")) { cs.setflag(CSInfo::is_lin_srgb | CSInfo::is_linear_response, @@ -707,7 +720,9 @@ ColorConfig::Impl::classify_by_conversions(CSInfo& cs) } // Set up some canonical names - if (cs.flags() & CSInfo::is_srgb) + if (cs.flags() & CSInfo::is_srgb_display) + cs.canonical = "srgb_rec709_display"; + else if (cs.flags() & CSInfo::is_srgb_scene) cs.canonical = "srgb_rec709_scene"; else if (cs.flags() & CSInfo::is_lin_srgb) cs.canonical = "lin_rec709_scene"; @@ -738,6 +753,9 @@ ColorConfig::Impl::reclassify_heuristics(CSInfo& cs) // If there is a known srgb space, and transforming our test // colors from "this cs" to srgb gives us what we expect for a // lin_srgb->srgb, then guess what? -- this is lin_srgb! + const std::string& srgb_alias = srgb_scene_alias.size() + ? srgb_scene_alias + : srgb_display_alias; if (srgb_alias.size() && test_conversion_yields(cs.name.c_str(), srgb_alias.c_str(), test_colors, lin_srgb_to_srgb_results)) { @@ -757,14 +775,23 @@ ColorConfig::Impl::identify_builtin_equivalents() if (disable_builtin_configs) return; Timer timer; + if (auto n = IdentifyBuiltinColorSpace("srgb_display")) { + if (CSInfo* cs = find(n)) { + cs->setflag(CSInfo::is_srgb_display, srgb_display_alias); + DBG("Identified {} = builtin '{}'\n", "srgb_rec709_display", + cs->name); + } + } else { + DBG("No config space identified as srgb_display\n"); + } if (auto n = IdentifyBuiltinColorSpace("srgb_tx")) { if (CSInfo* cs = find(n)) { - cs->setflag(CSInfo::is_srgb, srgb_alias); + cs->setflag(CSInfo::is_srgb_scene, srgb_scene_alias); DBG("Identified {} = builtin '{}'\n", "srgb_rec709_scene", cs->name); } } else { - DBG("No config space identified as srgb\n"); + DBG("No config space identified as srgb_scene\n"); } DBG("identify_builtin_equivalents srgb took {:0.2f}s\n", timer.lap()); if (auto n = IdentifyBuiltinColorSpace("lin_srgb")) { @@ -921,8 +948,10 @@ ColorConfig::Impl::init(string_view filename) for (auto&& cs : colorspaces) { // examine(&cs); DBG("Color space '{}':\n", cs.name); - if (cs.flags() & CSInfo::is_srgb) - DBG("'{}' is srgb\n", cs.name); + if (cs.flags() & CSInfo::is_srgb_display) + DBG("'{}' is srgb_display\n", cs.name); + if (cs.flags() & CSInfo::is_srgb_scene) + DBG("'{}' is srgb_scene\n", cs.name); if (cs.flags() & CSInfo::is_lin_srgb) DBG("'{}' is lin_srgb\n", cs.name); if (cs.flags() & CSInfo::is_ACEScg) @@ -1529,8 +1558,11 @@ ColorConfig::Impl::resolve(string_view name) const spin_rw_write_lock lock(m_mutex); if ((Strutil::iequals(name, "sRGB") || Strutil::iequals(name, "srgb_rec709_scene")) - && !srgb_alias.empty()) - return srgb_alias; + && !srgb_scene_alias.empty()) + return srgb_scene_alias; + if (Strutil::iequals(name, "srgb_rec709_display") + && !srgb_display_alias.empty()) + return srgb_display_alias; if ((Strutil::iequals(name, "lin_srgb") || Strutil::iequals(name, "lin_rec709") || Strutil::iequals(name, "lin_rec709_scene") @@ -1574,7 +1606,8 @@ ColorConfig::equivalent(string_view color_space1, // If the color spaces' flags (when masking only the bits that refer to // specific known color spaces) match, consider them equivalent. - const int mask = CSInfo::is_srgb | CSInfo::is_lin_srgb | CSInfo::is_ACEScg + const int mask = CSInfo::is_srgb_display | CSInfo::is_srgb_scene + | CSInfo::is_lin_srgb | CSInfo::is_ACEScg | CSInfo::is_Rec709; const CSInfo* csi1 = getImpl()->find(color_space1); const CSInfo* csi2 = getImpl()->find(color_space2); @@ -2321,6 +2354,11 @@ constexpr ColorInteropID color_interop_ids[] = { CICPMatrix::Unspecified }, { "pq_xyzd65_display", CICPPrimaries::XYZD65, CICPTransfer::PQ, CICPMatrix::Unspecified }, + + // OpenColorIO interop IDs, last so that the official ones above take + // priority when converting a CICP to an interop ID. + { "ocio:lin_ciexyzd65_display", CICPPrimaries::XYZD65, CICPTransfer::Linear, + CICPMatrix::Unspecified }, }; } // namespace @@ -2340,7 +2378,7 @@ ColorConfig::get_color_interop_id(string_view colorspace) const } catch (...) { interop_id = nullptr; } - if (interop_id) { + if (interop_id && interop_id[0]) { return interop_id; } } diff --git a/testsuite/python-colorconfig/ref/out-ocio230.txt b/testsuite/python-colorconfig/ref/out-ocio230.txt index ca19551d3d..641e26db87 100644 --- a/testsuite/python-colorconfig/ref/out-ocio230.txt +++ b/testsuite/python-colorconfig/ref/out-ocio230.txt @@ -31,6 +31,9 @@ equivalent('lnf', 'scene_linear'): False get_color_interop_id('ACEScg') = lin_ap1_scene get_color_interop_id('lin_srgb') = lin_rec709_scene get_color_interop_id([1, 13, 1, 1]) = srgb_rec709_scene +resolve('srgb_rec709_display'): srgb_rec709_display +equivalent('srgb_rec709_display', 'srgb_rec709_scene'): False +get_color_interop_id('sRGB - Display') = '' get_cicp('pq_rec2020_display') = [9, 16, 9, 1] get_cicp('unknown_interop_id') = None isColorSpaceLinear('scene_linear') = True diff --git a/testsuite/python-colorconfig/ref/out-ocio230b.txt b/testsuite/python-colorconfig/ref/out-ocio230b.txt index 542aa2a3bb..39f5b5b1bc 100644 --- a/testsuite/python-colorconfig/ref/out-ocio230b.txt +++ b/testsuite/python-colorconfig/ref/out-ocio230b.txt @@ -31,6 +31,9 @@ equivalent('lnf', 'scene_linear'): False get_color_interop_id('ACEScg') = lin_ap1_scene get_color_interop_id('lin_srgb') = lin_rec709_scene get_color_interop_id([1, 13, 1, 1]) = srgb_rec709_scene +resolve('srgb_rec709_display'): srgb_rec709_display +equivalent('srgb_rec709_display', 'srgb_rec709_scene'): False +get_color_interop_id('sRGB - Display') = '' get_cicp('pq_rec2020_display') = [9, 16, 9, 1] get_cicp('unknown_interop_id') = None isColorSpaceLinear('scene_linear') = True diff --git a/testsuite/python-colorconfig/ref/out-ocio232.txt b/testsuite/python-colorconfig/ref/out-ocio232.txt index 9c063cdd9a..53e33026cb 100644 --- a/testsuite/python-colorconfig/ref/out-ocio232.txt +++ b/testsuite/python-colorconfig/ref/out-ocio232.txt @@ -31,6 +31,9 @@ equivalent('lnf', 'scene_linear'): False get_color_interop_id('ACEScg') = lin_ap1_scene get_color_interop_id('lin_srgb') = lin_rec709_scene get_color_interop_id([1, 13, 1, 1]) = srgb_rec709_scene +resolve('srgb_rec709_display'): srgb_rec709_display +equivalent('srgb_rec709_display', 'srgb_rec709_scene'): False +get_color_interop_id('sRGB - Display') = '' get_cicp('pq_rec2020_display') = [9, 16, 9, 1] get_cicp('unknown_interop_id') = None isColorSpaceLinear('scene_linear') = True diff --git a/testsuite/python-colorconfig/ref/out-ocio24.txt b/testsuite/python-colorconfig/ref/out-ocio24.txt index a8dab6c064..b31b6df5ee 100644 --- a/testsuite/python-colorconfig/ref/out-ocio24.txt +++ b/testsuite/python-colorconfig/ref/out-ocio24.txt @@ -31,6 +31,9 @@ equivalent('lnf', 'scene_linear'): False get_color_interop_id('ACEScg') = lin_ap1_scene get_color_interop_id('lin_srgb') = lin_rec709_scene get_color_interop_id([1, 13, 1, 1]) = srgb_rec709_scene +resolve('srgb_rec709_display'): sRGB - Display +equivalent('srgb_rec709_display', 'srgb_rec709_scene'): False +get_color_interop_id('sRGB - Display') = 'srgb_rec709_display' get_cicp('pq_rec2020_display') = [9, 16, 9, 1] get_cicp('unknown_interop_id') = None isColorSpaceLinear('scene_linear') = True diff --git a/testsuite/python-colorconfig/ref/out-ocio25.txt b/testsuite/python-colorconfig/ref/out-ocio25.txt index 83e831b137..c08dc72dd4 100644 --- a/testsuite/python-colorconfig/ref/out-ocio25.txt +++ b/testsuite/python-colorconfig/ref/out-ocio25.txt @@ -31,6 +31,9 @@ equivalent('lnf', 'scene_linear'): False get_color_interop_id('ACEScg') = lin_ap1_scene get_color_interop_id('lin_srgb') = lin_rec709_scene get_color_interop_id([1, 13, 1, 1]) = srgb_rec709_scene +resolve('srgb_rec709_display'): sRGB - Display +equivalent('srgb_rec709_display', 'srgb_rec709_scene'): False +get_color_interop_id('sRGB - Display') = 'srgb_rec709_display' get_cicp('pq_rec2020_display') = [9, 16, 9, 1] get_cicp('unknown_interop_id') = None isColorSpaceLinear('scene_linear') = True diff --git a/testsuite/python-colorconfig/ref/out.txt b/testsuite/python-colorconfig/ref/out.txt index 711f2d88e4..6a0840e46d 100644 --- a/testsuite/python-colorconfig/ref/out.txt +++ b/testsuite/python-colorconfig/ref/out.txt @@ -31,6 +31,9 @@ equivalent('lnf', 'scene_linear'): False get_color_interop_id('ACEScg') = lin_ap1_scene get_color_interop_id('lin_srgb') = lin_rec709_scene get_color_interop_id([1, 13, 1, 1]) = srgb_rec709_scene +resolve('srgb_rec709_display'): srgb_rec709_display +equivalent('srgb_rec709_display', 'srgb_rec709_scene'): False +get_color_interop_id('sRGB - Display') = '' get_cicp('pq_rec2020_display') = [9, 16, 9, 1] get_cicp('unknown_interop_id') = None isColorSpaceLinear('scene_linear') = True diff --git a/testsuite/python-colorconfig/src/test_colorconfig.py b/testsuite/python-colorconfig/src/test_colorconfig.py index cbbba63dc9..ad95f98b70 100755 --- a/testsuite/python-colorconfig/src/test_colorconfig.py +++ b/testsuite/python-colorconfig/src/test_colorconfig.py @@ -60,6 +60,9 @@ print ("get_color_interop_id('ACEScg') = ", config.get_color_interop_id("ACEScg")) print ("get_color_interop_id('lin_srgb') = ", config.get_color_interop_id("lin_srgb")) print ("get_color_interop_id([1, 13, 1, 1]) = ", config.get_color_interop_id([1, 13, 1, 1])) + print ("resolve('srgb_rec709_display'):", config.resolve("srgb_rec709_display")) + print ("equivalent('srgb_rec709_display', 'srgb_rec709_scene'):", config.equivalent("srgb_rec709_display", "srgb_rec709_scene")) + print (f"get_color_interop_id('sRGB - Display') = '{config.get_color_interop_id('sRGB - Display')}'") print ("get_cicp('pq_rec2020_display') = ", config.get_cicp("pq_rec2020_display")) print ("get_cicp('unknown_interop_id') = ", config.get_cicp("unknown_interop_id")) print ("isColorSpaceLinear('scene_linear') = ", config.isColorSpaceLinear('scene_linear'))