From 9fdcf0c6bfe305842148170065b911ffbe7712d7 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Tue, 25 Nov 2025 16:24:20 +0100 Subject: [PATCH 01/12] feat: Auto convert between oiio:ColorSpace and CICP attributes in I/O When reading image files with CICP metadata, automatically set the corresponding "oiio:ColorSpace". When writing files that support CICP and no other colorspace metadata can represent "oiio:ColorSpace", automatically write CICP metadata. Setting "oiio:ColorSpace" on read prefers scene referred over display referred color spaces, changing existing behavior as little as possible. The alternative would have been to interpret the presence of CICP metadata as an indication that the image is likely display referred, which might be reasonable too. Also add new ColorConfig set_colorspace_cicp and get_colorspace_cicp API functions to share logic between file formats. Signed-off-by: Brecht Van Lommel --- src/ffmpeg.imageio/ffmpeginput.cpp | 4 +- src/heif.imageio/heifinput.cpp | 5 +- src/heif.imageio/heifoutput.cpp | 9 +- src/include/OpenImageIO/color.h | 17 +++ src/libOpenImageIO/color_ocio.cpp | 134 ++++++++++++++++++ src/png.imageio/png_pvt.h | 41 ++++-- testsuite/ffmpeg/ref/out-ffmpeg6.1.txt | 2 + testsuite/ffmpeg/ref/out-ffmpeg8.0.txt | 2 + testsuite/heif/ref/out-libheif1.12-orient.txt | 12 +- testsuite/heif/ref/out-libheif1.4.txt | 12 +- testsuite/heif/ref/out-libheif1.5.txt | 12 +- .../heif/ref/out-libheif1.9-with-av1-alt2.txt | 12 +- .../heif/ref/out-libheif1.9-with-av1.txt | 12 +- testsuite/heif/run.py | 5 + 14 files changed, 253 insertions(+), 26 deletions(-) diff --git a/src/ffmpeg.imageio/ffmpeginput.cpp b/src/ffmpeg.imageio/ffmpeginput.cpp index 8eec06b08f..574fe10449 100644 --- a/src/ffmpeg.imageio/ffmpeginput.cpp +++ b/src/ffmpeg.imageio/ffmpeginput.cpp @@ -71,6 +71,7 @@ receive_frame(AVCodecContext* avctx, AVFrame* picture, AVPacket* avpkt) +#include #include #include #include @@ -548,7 +549,8 @@ FFmpegInput::open(const std::string& name, ImageSpec& spec) = { m_codec_context->color_primaries, m_codec_context->color_trc, m_codec_context->colorspace, m_codec_context->color_range == AVCOL_RANGE_MPEG ? 0 : 1 }; - m_spec.attribute("CICP", TypeDesc(TypeDesc::INT, 4), cicp); + ColorConfig::default_colorconfig().set_colorspace_cicp(m_spec, cicp); + m_nsubimages = m_frames; spec = m_spec; m_filename = name; diff --git a/src/heif.imageio/heifinput.cpp b/src/heif.imageio/heifinput.cpp index 4ba8e2fb61..c615913c6d 100644 --- a/src/heif.imageio/heifinput.cpp +++ b/src/heif.imageio/heifinput.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // https://github.com/AcademySoftwareFoundation/OpenImageIO +#include #include #include #include @@ -291,7 +292,9 @@ HeifInput::seek_subimage(int subimage, int miplevel) int(nclx->transfer_characteristics), int(nclx->matrix_coefficients), int(nclx->full_range_flag ? 1 : 0) }; - m_spec.attribute("CICP", TypeDesc(TypeDesc::INT, 4), cicp); + const ColorConfig& colorconfig( + ColorConfig::default_colorconfig()); + colorconfig.set_colorspace_cicp(m_spec, cicp); } heif_nclx_color_profile_free(nclx); } diff --git a/src/heif.imageio/heifoutput.cpp b/src/heif.imageio/heifoutput.cpp index 6ed1dbb439..e5e6c34b26 100644 --- a/src/heif.imageio/heifoutput.cpp +++ b/src/heif.imageio/heifoutput.cpp @@ -3,6 +3,7 @@ // https://github.com/AcademySoftwareFoundation/OpenImageIO +#include #include #include #include @@ -249,10 +250,10 @@ HeifOutput::close() std::unique_ptr nclx(heif_nclx_color_profile_alloc(), heif_nclx_color_profile_free); - const ParamValue* p = m_spec.find_attribute("CICP", - TypeDesc(TypeDesc::INT, 4)); - if (p) { - const int* cicp = static_cast(p->data()); + const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); + const bool auto_cicp = true; + int cicp[4]; + if (colorconfig.get_colorspace_cicp(m_spec, auto_cicp, cicp)) { nclx->color_primaries = heif_color_primaries(cicp[0]); nclx->transfer_characteristics = heif_transfer_characteristics( cicp[1]); diff --git a/src/include/OpenImageIO/color.h b/src/include/OpenImageIO/color.h index 46676efe76..ae63de69d3 100644 --- a/src/include/OpenImageIO/color.h +++ b/src/include/OpenImageIO/color.h @@ -422,6 +422,23 @@ class OIIO_API ColorConfig { /// @version 3.0 void set_colorspace_rec709_gamma(ImageSpec& spec, float gamma) const; + /// Set the "CICP" attribute in the spec. If a corresponding colorspace + /// is found, automatically set "oiio:ColorSpace" as well. It also removes + /// or alters several other attributes that may hint color space in ways + /// that might be contradictory or no longer true. + /// + /// @version 3.1 + void set_colorspace_cicp(ImageSpec& spec, const int cicp[4]) const; + + /// Get the CICP code corresponding from the "CICP" attribute. If there + /// is not such attribute and auto_cicp is true, atempt to determine a CICP + /// code from the "oiio:ColorSpace" attribute. + /// Returns false if no CICP code could be determined. + /// + /// @version 3.1 + bool get_colorspace_cicp(ImageSpec& spec, bool auto_colorspace, + int cicp[4]) const; + /// Return if OpenImageIO was built with OCIO support static bool supportsOpenColorIO(); diff --git a/src/libOpenImageIO/color_ocio.cpp b/src/libOpenImageIO/color_ocio.cpp index 26f45c8d2b..40bc59d154 100644 --- a/src/libOpenImageIO/color_ocio.cpp +++ b/src/libOpenImageIO/color_ocio.cpp @@ -2655,6 +2655,140 @@ ColorConfig::set_colorspace_rec709_gamma(ImageSpec& spec, float gamma) const } } +namespace { +// Primaries +static const int cicp_primaries_rec709 = 1; +static const int cicp_primaries_rec2020 = 9; +static const int cicp_primaries_xyzd65 = 10; +static const int cicp_primaries_p3d65 = 12; +// Transfer functions +static const int cicp_transfer_bt709 = 1; +static const int cicp_transfer_g22 = 4; +static const int cicp_transfer_linear = 8; +static const int cicp_transfer_srgb = 13; +static const int cicp_transfer_pq = 16; +static const int cicp_transfer_g26 = 17; +static const int cicp_transfer_hlg = 18; +// Matrix +static const int cicp_matrix_bt709 = 1; +static const int cicp_matrix_unspecified = 2; +static const int cicp_matrix_rec2020_ncl = 9; +static const int cicp_matrix_rec2020_cl = 10; +// Range +static const int cicp_range_full = 1; + +// Mapping between color interop ID and CICP, based on Color Interop Forum +// recommendations. +struct ColorSpaceCICP { + const char* interop_id; + int cicp[4]; +}; + +static const ColorSpaceCICP color_space_cicp[] = { + // Scene referred interop IDs first so they are the default in automatic + // conversion from CICP to interop ID. + { "srgb_rec709_scene", + { cicp_primaries_rec709, cicp_transfer_srgb, cicp_matrix_bt709, + cicp_range_full } }, + { "srgb_rec709_scene", + { cicp_primaries_rec709, cicp_transfer_srgb, cicp_matrix_bt709, + cicp_range_full } }, + { "srgb_p3d65_scene", + { cicp_primaries_p3d65, cicp_transfer_srgb, cicp_matrix_bt709, + cicp_range_full } }, + // These are not display color spaces at all, but can be represented by CICP. + { "lin_rec709_scene", + { cicp_primaries_rec709, cicp_transfer_linear, cicp_matrix_bt709, + cicp_range_full } }, + { "lin_p3d65_scene", + { cicp_primaries_p3d65, cicp_transfer_linear, cicp_matrix_bt709, + cicp_range_full } }, + { "lin_rec2020_scene", + { cicp_primaries_rec2020, cicp_transfer_linear, cicp_matrix_rec2020_cl, + cicp_range_full } }, + { "lin_ciexyzd65_scene", + { cicp_primaries_xyzd65, cicp_transfer_linear, cicp_matrix_unspecified, + cicp_range_full } }, + + // Display referred interop IDs. + { "srgb_rec709_display", + { cicp_primaries_rec709, cicp_transfer_srgb, cicp_matrix_bt709, + cicp_range_full } }, + { "g24_rec709_display", + { cicp_primaries_rec709, cicp_transfer_bt709, cicp_matrix_bt709, + cicp_range_full } }, + { "srgb_p3d65_display", + { cicp_primaries_p3d65, cicp_transfer_srgb, cicp_matrix_bt709, + cicp_range_full } }, + { "srgbe_p3d65_display", + { cicp_primaries_p3d65, cicp_transfer_srgb, cicp_matrix_bt709, + cicp_range_full } }, + { "pq_p3d65_display", + { cicp_primaries_p3d65, cicp_transfer_pq, cicp_matrix_rec2020_ncl, + cicp_range_full } }, + { "pq_rec2020_display", + { cicp_primaries_rec2020, cicp_transfer_pq, cicp_matrix_rec2020_ncl, + cicp_range_full } }, + { "hlg_rec2020_display", + { cicp_primaries_rec2020, cicp_transfer_hlg, cicp_matrix_rec2020_ncl, + cicp_range_full } }, + { "g22_rec709_display", + { cicp_primaries_rec709, cicp_transfer_g22, cicp_matrix_bt709, + cicp_range_full } }, + // No CICP code for Adobe RGB primaries. + // { "g22_adobergb_display" } + { "g26_p3d65_display", + { cicp_primaries_p3d65, cicp_transfer_g26, cicp_matrix_bt709, + cicp_range_full } }, + { "g26_xyzd65_display", + { cicp_primaries_xyzd65, cicp_transfer_g26, cicp_matrix_unspecified, + cicp_range_full } }, + { "pq_xyzd65_display", + { cicp_primaries_xyzd65, cicp_transfer_pq, cicp_matrix_unspecified, + cicp_range_full } }, +}; +} // namespace + +void +ColorConfig::set_colorspace_cicp(ImageSpec& spec, const int cicp[4]) const +{ + spec.attribute("CICP", TypeDesc(TypeDesc::INT, 4), cicp); + + for (const ColorSpaceCICP& space : color_space_cicp) { + if (space.cicp[0] == cicp[0] && space.cicp[1] == cicp[1]) { + set_colorspace(spec, space.interop_id); + return; + } + } +} + +bool +ColorConfig::get_colorspace_cicp(ImageSpec& spec, bool auto_cicp, + int cicp[4]) const +{ + const ParamValue* p = spec.find_attribute("CICP", + TypeDesc(TypeDesc::INT, 4)); + if (p) { + std::copy_n(static_cast(p->data()), 4, cicp); + return true; + } + + if (!auto_cicp) { + return false; + } + + string_view colorspace = spec.get_string_attribute("oiio:ColorSpace"); + if (!colorspace.empty()) { + for (const ColorSpaceCICP& space : color_space_cicp) { + if (equivalent(colorspace, space.interop_id)) { + std::copy_n(space.cicp, 4, cicp); + return true; + } + } + } + + return false; +} void set_colorspace(ImageSpec& spec, string_view colorspace) diff --git a/src/png.imageio/png_pvt.h b/src/png.imageio/png_pvt.h index a6a7c06609..e885310447 100644 --- a/src/png.imageio/png_pvt.h +++ b/src/png.imageio/png_pvt.h @@ -40,7 +40,6 @@ For further information see the following mailing list threads: OIIO_PLUGIN_NAMESPACE_BEGIN #define ICC_PROFILE_ATTR "ICCProfile" -#define CICP_ATTR "CICP" namespace PNG_pvt { @@ -330,8 +329,9 @@ read_info(png_structp& sp, png_infop& ip, int& bit_depth, int& color_type, { png_byte pri = 0, trc = 0, mtx = 0, vfr = 0; if (png_get_cICP(sp, ip, &pri, &trc, &mtx, &vfr)) { - int cicp[4] = { pri, trc, mtx, vfr }; - spec.attribute(CICP_ATTR, TypeDesc(TypeDesc::INT, 4), cicp); + const int cicp[4] = { pri, trc, mtx, vfr }; + const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); + colorconfig.set_colorspace_cicp(spec, cicp); } } #endif @@ -608,7 +608,8 @@ write_info(png_structp& sp, png_infop& ip, int& color_type, ImageSpec& spec, string_view colorspace = spec.get_string_attribute("oiio:ColorSpace", "srgb_rec709_scene"); const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); - srgb = false; + bool wrote_colorspace = false; + srgb = false; if (colorconfig.equivalent(colorspace, "srgb_rec709_scene")) { srgb = true; gamma = 1.0f; @@ -628,7 +629,8 @@ write_info(png_structp& sp, png_infop& ip, int& color_type, ImageSpec& spec, if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp) return "Could not set PNG gAMA chunk"; png_set_gAMA(sp, ip, 1.0); - srgb = false; + srgb = false; + wrote_colorspace = true; } else if (Strutil::istarts_with(colorspace, "Gamma")) { // Back compatible, this is DEPRECATED(3.1) Strutil::parse_word(colorspace); @@ -638,24 +640,28 @@ write_info(png_structp& sp, png_infop& ip, int& color_type, ImageSpec& spec, if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp) return "Could not set PNG gAMA chunk"; png_set_gAMA(sp, ip, 1.0f / gamma); - srgb = false; + srgb = false; + wrote_colorspace = true; } else if (colorconfig.equivalent(colorspace, "g22_rec709_scene")) { gamma = 2.2f; if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp) return "Could not set PNG gAMA chunk"; png_set_gAMA(sp, ip, 1.0f / gamma); - srgb = false; + srgb = false; + wrote_colorspace = true; } else if (colorconfig.equivalent(colorspace, "g18_rec709_scene")) { gamma = 1.8f; if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp) return "Could not set PNG gAMA chunk"; png_set_gAMA(sp, ip, 1.0f / gamma); - srgb = false; + srgb = false; + wrote_colorspace = true; } else if (colorconfig.equivalent(colorspace, "srgb_rec709_scene")) { if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp) return "Could not set PNG gAMA and cHRM chunk"; png_set_sRGB_gAMA_and_cHRM(sp, ip, PNG_sRGB_INTENT_ABSOLUTE); - srgb = true; + srgb = true; + wrote_colorspace = true; } // Write ICC profile, if we have anything @@ -667,8 +673,10 @@ write_info(png_structp& sp, png_infop& ip, int& color_type, ImageSpec& spec, return "Could not set PNG iCCP chunk"; unsigned char* icc_profile = (unsigned char*)icc_profile_parameter->data(); - if (icc_profile && length) + if (icc_profile && length) { png_set_iCCP(sp, ip, "Embedded Profile", 0, icc_profile, length); + wrote_colorspace = true; + } } if (false && !spec.find_attribute("DateTime")) { @@ -724,13 +732,16 @@ write_info(png_structp& sp, png_infop& ip, int& color_type, ImageSpec& spec, } #ifdef PNG_cICP_SUPPORTED - const ParamValue* p = spec.find_attribute(CICP_ATTR, - TypeDesc(TypeDesc::INT, 4)); - if (p) { - const int* int_vals = static_cast(p->data()); + // Only automatically determine CICP from oiio::ColorSpace if we didn't + // write colorspace metadata yet. + const bool auto_cicp = !wrote_colorspace; + int cicp[4]; + if (colorconfig.get_colorspace_cicp(spec, auto_cicp, cicp)) { + // Matrix must be RGB according to PNG spec v3 + cicp[2] = 0; png_byte vals[4]; for (int i = 0; i < 4; ++i) - vals[i] = static_cast(int_vals[i]); + vals[i] = static_cast(cicp[i]); if (setjmp(png_jmpbuf(sp))) // NOLINT(cert-err52-cpp) return "Could not set PNG cICP chunk"; // libpng will only write the chunk if the third byte is 0 diff --git a/testsuite/ffmpeg/ref/out-ffmpeg6.1.txt b/testsuite/ffmpeg/ref/out-ffmpeg6.1.txt index 0d037e3cc5..1f1258b343 100644 --- a/testsuite/ffmpeg/ref/out-ffmpeg6.1.txt +++ b/testsuite/ffmpeg/ref/out-ffmpeg6.1.txt @@ -96,6 +96,7 @@ ref/vp9_rec2100_pq.mkv : 384 x 216, 3 channel, uint10 FFmpeg movie FramesPerSecond: 24/1 (24) ffmpeg:codec_name: "Google VP9" oiio:BitsPerSample: 10 + oiio:ColorSpace: "pq_rec2020_display" oiio:Movie: 1 oiio:subimages: 2 subimage 1: 384 x 216, 3 channel, uint10 FFmpeg movie @@ -106,5 +107,6 @@ ref/vp9_rec2100_pq.mkv : 384 x 216, 3 channel, uint10 FFmpeg movie FramesPerSecond: 24/1 (24) ffmpeg:codec_name: "Google VP9" oiio:BitsPerSample: 10 + oiio:ColorSpace: "pq_rec2020_display" oiio:Movie: 1 oiio:subimages: 2 diff --git a/testsuite/ffmpeg/ref/out-ffmpeg8.0.txt b/testsuite/ffmpeg/ref/out-ffmpeg8.0.txt index a4b503edca..28084a22e3 100644 --- a/testsuite/ffmpeg/ref/out-ffmpeg8.0.txt +++ b/testsuite/ffmpeg/ref/out-ffmpeg8.0.txt @@ -96,6 +96,7 @@ ref/vp9_rec2100_pq.mkv : 384 x 216, 3 channel, uint10 FFmpeg movie FramesPerSecond: 24/1 (24) ffmpeg:codec_name: "Google VP9" oiio:BitsPerSample: 10 + oiio:ColorSpace: "pq_rec2020_display" oiio:Movie: 1 oiio:subimages: 2 subimage 1: 384 x 216, 3 channel, uint10 FFmpeg movie @@ -106,5 +107,6 @@ ref/vp9_rec2100_pq.mkv : 384 x 216, 3 channel, uint10 FFmpeg movie FramesPerSecond: 24/1 (24) ffmpeg:codec_name: "Google VP9" oiio:BitsPerSample: 10 + oiio:ColorSpace: "pq_rec2020_display" oiio:Movie: 1 oiio:subimages: 2 diff --git a/testsuite/heif/ref/out-libheif1.12-orient.txt b/testsuite/heif/ref/out-libheif1.12-orient.txt index 78b7d9fd6f..875e3ac54e 100644 --- a/testsuite/heif/ref/out-libheif1.12-orient.txt +++ b/testsuite/heif/ref/out-libheif1.12-orient.txt @@ -64,7 +64,17 @@ cicp_pq.avif : 16 x 16, 4 channel, uint10 heif Exif:FlashPixVersion: "0100" heif:UnassociatedAlpha: 1 oiio:BitsPerSample: 10 - oiio:ColorSpace: "srgb_rec709_scene" + oiio:ColorSpace: "pq_rec2020_display" +Reading colorspace_hlg.avif +colorspace_hlg.avif : 16 x 16, 4 channel, uint10 heif + SHA-1: 0F3CAB52D479BC23E9C981DBADDFEF1F792E5540 + channel list: R, G, B, A + CICP: 9, 18, 9, 1 + Exif:ExifVersion: "0230" + Exif:FlashPixVersion: "0100" + heif:UnassociatedAlpha: 1 + oiio:BitsPerSample: 10 + oiio:ColorSpace: "hlg_rec2020_display" Reading ../oiio-images/heif/greyhounds-looking-for-a-table.heic ../oiio-images/heif/greyhounds-looking-for-a-table.heic : 3024 x 4032, 3 channel, uint8 heif SHA-1: 8211F56BBABDC7615CCAF67CBF49741D1A292D2E diff --git a/testsuite/heif/ref/out-libheif1.4.txt b/testsuite/heif/ref/out-libheif1.4.txt index bd0ff14603..9d8304f14a 100644 --- a/testsuite/heif/ref/out-libheif1.4.txt +++ b/testsuite/heif/ref/out-libheif1.4.txt @@ -64,7 +64,17 @@ cicp_pq.avif : 16 x 16, 4 channel, uint10 heif Exif:FlashPixVersion: "0100" heif:UnassociatedAlpha: 1 oiio:BitsPerSample: 10 - oiio:ColorSpace: "srgb_rec709_scene" + oiio:ColorSpace: "pq_rec2020_display" +Reading colorspace_hlg.avif +colorspace_hlg.avif : 16 x 16, 4 channel, uint10 heif + SHA-1: 0F3CAB52D479BC23E9C981DBADDFEF1F792E5540 + channel list: R, G, B, A + CICP: 9, 18, 9, 1 + Exif:ExifVersion: "0230" + Exif:FlashPixVersion: "0100" + heif:UnassociatedAlpha: 1 + oiio:BitsPerSample: 10 + oiio:ColorSpace: "hlg_rec2020_display" Reading ../oiio-images/heif/greyhounds-looking-for-a-table.heic ../oiio-images/heif/greyhounds-looking-for-a-table.heic : 3024 x 4032, 3 channel, uint8 heif SHA-1: 8211F56BBABDC7615CCAF67CBF49741D1A292D2E diff --git a/testsuite/heif/ref/out-libheif1.5.txt b/testsuite/heif/ref/out-libheif1.5.txt index 39398f71cd..9dfd4ff23d 100644 --- a/testsuite/heif/ref/out-libheif1.5.txt +++ b/testsuite/heif/ref/out-libheif1.5.txt @@ -64,7 +64,17 @@ cicp_pq.avif : 16 x 16, 4 channel, uint10 heif Exif:FlashPixVersion: "0100" heif:UnassociatedAlpha: 1 oiio:BitsPerSample: 10 - oiio:ColorSpace: "srgb_rec709_scene" + oiio:ColorSpace: "pq_rec2020_display" +Reading colorspace_hlg.avif +colorspace_hlg.avif : 16 x 16, 4 channel, uint10 heif + SHA-1: 0F3CAB52D479BC23E9C981DBADDFEF1F792E5540 + channel list: R, G, B, A + CICP: 9, 18, 9, 1 + Exif:ExifVersion: "0230" + Exif:FlashPixVersion: "0100" + heif:UnassociatedAlpha: 1 + oiio:BitsPerSample: 10 + oiio:ColorSpace: "hlg_rec2020_display" Reading ../oiio-images/heif/greyhounds-looking-for-a-table.heic ../oiio-images/heif/greyhounds-looking-for-a-table.heic : 3024 x 4032, 3 channel, uint8 heif SHA-1: 8211F56BBABDC7615CCAF67CBF49741D1A292D2E diff --git a/testsuite/heif/ref/out-libheif1.9-with-av1-alt2.txt b/testsuite/heif/ref/out-libheif1.9-with-av1-alt2.txt index 6be6dc26e0..c938a6fe73 100644 --- a/testsuite/heif/ref/out-libheif1.9-with-av1-alt2.txt +++ b/testsuite/heif/ref/out-libheif1.9-with-av1-alt2.txt @@ -64,7 +64,17 @@ cicp_pq.avif : 16 x 16, 4 channel, uint10 heif Exif:FlashPixVersion: "0100" heif:UnassociatedAlpha: 1 oiio:BitsPerSample: 10 - oiio:ColorSpace: "srgb_rec709_scene" + oiio:ColorSpace: "pq_rec2020_display" +Reading colorspace_hlg.avif +colorspace_hlg.avif : 16 x 16, 4 channel, uint10 heif + SHA-1: 0F3CAB52D479BC23E9C981DBADDFEF1F792E5540 + channel list: R, G, B, A + CICP: 9, 18, 9, 1 + Exif:ExifVersion: "0230" + Exif:FlashPixVersion: "0100" + heif:UnassociatedAlpha: 1 + oiio:BitsPerSample: 10 + oiio:ColorSpace: "hlg_rec2020_display" Reading ../oiio-images/heif/greyhounds-looking-for-a-table.heic ../oiio-images/heif/greyhounds-looking-for-a-table.heic : 3024 x 4032, 3 channel, uint8 heif SHA-1: 8064B23A1A995B0D6525AFB5248EEC6C730BBB6C diff --git a/testsuite/heif/ref/out-libheif1.9-with-av1.txt b/testsuite/heif/ref/out-libheif1.9-with-av1.txt index e1f848ea83..f6d7ca55a5 100644 --- a/testsuite/heif/ref/out-libheif1.9-with-av1.txt +++ b/testsuite/heif/ref/out-libheif1.9-with-av1.txt @@ -64,7 +64,17 @@ cicp_pq.avif : 16 x 16, 4 channel, uint10 heif Exif:FlashPixVersion: "0100" heif:UnassociatedAlpha: 1 oiio:BitsPerSample: 10 - oiio:ColorSpace: "srgb_rec709_scene" + oiio:ColorSpace: "pq_rec2020_display" +Reading colorspace_hlg.avif +colorspace_hlg.avif : 16 x 16, 4 channel, uint10 heif + SHA-1: 0F3CAB52D479BC23E9C981DBADDFEF1F792E5540 + channel list: R, G, B, A + CICP: 9, 18, 9, 1 + Exif:ExifVersion: "0230" + Exif:FlashPixVersion: "0100" + heif:UnassociatedAlpha: 1 + oiio:BitsPerSample: 10 + oiio:ColorSpace: "hlg_rec2020_display" Reading ../oiio-images/heif/greyhounds-looking-for-a-table.heic ../oiio-images/heif/greyhounds-looking-for-a-table.heic : 3024 x 4032, 3 channel, uint8 heif SHA-1: 8211F56BBABDC7615CCAF67CBF49741D1A292D2E diff --git a/testsuite/heif/run.py b/testsuite/heif/run.py index 9dc5a06f35..0a3250e374 100755 --- a/testsuite/heif/run.py +++ b/testsuite/heif/run.py @@ -13,6 +13,11 @@ " -d uint10 --cicp \"9,16,9,1\" -o cicp_pq.avif" ) command += info_command ("cicp_pq.avif", safematch=True) + +command += oiiotool (os.path.join(imagedir, "test-10bit.avif") + + " -d uint10 --attrib oiio:ColorSpace hlg_rec2020_display -o colorspace_hlg.avif" ) +command += info_command ("colorspace_hlg.avif", safematch=True) + files = [ "greyhounds-looking-for-a-table.heic", "sewing-threads.heic" ] for f in files: command = command + info_command (os.path.join(OIIO_TESTSUITE_IMAGEDIR, f)) From dd9aa9c9e6a517266f8b4139e539555e08c8db29 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Thu, 27 Nov 2025 15:01:04 +0100 Subject: [PATCH 02/12] Fix warning about unused variable Signed-off-by: Brecht Van Lommel --- src/png.imageio/png_pvt.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/png.imageio/png_pvt.h b/src/png.imageio/png_pvt.h index e885310447..6a887e6667 100644 --- a/src/png.imageio/png_pvt.h +++ b/src/png.imageio/png_pvt.h @@ -608,8 +608,8 @@ write_info(png_structp& sp, png_infop& ip, int& color_type, ImageSpec& spec, string_view colorspace = spec.get_string_attribute("oiio:ColorSpace", "srgb_rec709_scene"); const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); - bool wrote_colorspace = false; - srgb = false; + OIIO_MAYBE_UNUSED bool wrote_colorspace = false; + srgb = false; if (colorconfig.equivalent(colorspace, "srgb_rec709_scene")) { srgb = true; gamma = 1.0f; From ec296537819c72cc9d82265d258f8b7e87ee63a7 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Thu, 27 Nov 2025 20:45:39 +0100 Subject: [PATCH 03/12] Use enum class and constexpr, fix spelling Signed-off-by: Brecht Van Lommel --- src/include/OpenImageIO/color.h | 4 +- src/libOpenImageIO/color_ocio.cpp | 149 ++++++++++++++---------------- 2 files changed, 73 insertions(+), 80 deletions(-) diff --git a/src/include/OpenImageIO/color.h b/src/include/OpenImageIO/color.h index ae63de69d3..3e8bf12e51 100644 --- a/src/include/OpenImageIO/color.h +++ b/src/include/OpenImageIO/color.h @@ -431,12 +431,12 @@ class OIIO_API ColorConfig { void set_colorspace_cicp(ImageSpec& spec, const int cicp[4]) const; /// Get the CICP code corresponding from the "CICP" attribute. If there - /// is not such attribute and auto_cicp is true, atempt to determine a CICP + /// is no such attribute and auto_cicp is true, attempt to determine a CICP /// code from the "oiio:ColorSpace" attribute. /// Returns false if no CICP code could be determined. /// /// @version 3.1 - bool get_colorspace_cicp(ImageSpec& spec, bool auto_colorspace, + bool get_colorspace_cicp(ImageSpec& spec, bool auto_cicp, int cicp[4]) const; /// Return if OpenImageIO was built with OCIO support diff --git a/src/libOpenImageIO/color_ocio.cpp b/src/libOpenImageIO/color_ocio.cpp index 40bc59d154..00b6636a77 100644 --- a/src/libOpenImageIO/color_ocio.cpp +++ b/src/libOpenImageIO/color_ocio.cpp @@ -2656,96 +2656,86 @@ ColorConfig::set_colorspace_rec709_gamma(ImageSpec& spec, float gamma) const } namespace { -// Primaries -static const int cicp_primaries_rec709 = 1; -static const int cicp_primaries_rec2020 = 9; -static const int cicp_primaries_xyzd65 = 10; -static const int cicp_primaries_p3d65 = 12; -// Transfer functions -static const int cicp_transfer_bt709 = 1; -static const int cicp_transfer_g22 = 4; -static const int cicp_transfer_linear = 8; -static const int cicp_transfer_srgb = 13; -static const int cicp_transfer_pq = 16; -static const int cicp_transfer_g26 = 17; -static const int cicp_transfer_hlg = 18; -// Matrix -static const int cicp_matrix_bt709 = 1; -static const int cicp_matrix_unspecified = 2; -static const int cicp_matrix_rec2020_ncl = 9; -static const int cicp_matrix_rec2020_cl = 10; -// Range -static const int cicp_range_full = 1; +enum class CICPPrimaries { + Rec709 = 1, + Rec2020 = 9, + XYZD65 = 10, + P3D65 = 12, +}; +enum class CICPTransfer { + BT709 = 1, + Gamma22 = 4, + Linear = 8, + sRGB = 13, + PQ = 16, + Gamma26 = 17, + HLG = 18, +}; +enum class CICPMatrix { + RGB = 0, + BT709 = 1, + Unspecified = 2, + Rec2020_NCL = 9, + Rec2020_CL = 10, +}; +enum class CICPRange { + Narrow = 0, + Full = 1, +}; // Mapping between color interop ID and CICP, based on Color Interop Forum // recommendations. struct ColorSpaceCICP { const char* interop_id; - int cicp[4]; + CICPPrimaries primaries; + CICPTransfer transfer; + CICPMatrix matrix; }; -static const ColorSpaceCICP color_space_cicp[] = { +constexpr ColorSpaceCICP color_space_cicp[] = { // Scene referred interop IDs first so they are the default in automatic // conversion from CICP to interop ID. - { "srgb_rec709_scene", - { cicp_primaries_rec709, cicp_transfer_srgb, cicp_matrix_bt709, - cicp_range_full } }, - { "srgb_rec709_scene", - { cicp_primaries_rec709, cicp_transfer_srgb, cicp_matrix_bt709, - cicp_range_full } }, - { "srgb_p3d65_scene", - { cicp_primaries_p3d65, cicp_transfer_srgb, cicp_matrix_bt709, - cicp_range_full } }, + { "srgb_rec709_scene", CICPPrimaries::Rec709, CICPTransfer::sRGB, + CICPMatrix::BT709 }, + { "srgb_rec709_scene", CICPPrimaries::Rec709, CICPTransfer::sRGB, + CICPMatrix::BT709 }, + { "srgb_p3d65_scene", CICPPrimaries::P3D65, CICPTransfer::sRGB, + CICPMatrix::BT709 }, // These are not display color spaces at all, but can be represented by CICP. - { "lin_rec709_scene", - { cicp_primaries_rec709, cicp_transfer_linear, cicp_matrix_bt709, - cicp_range_full } }, - { "lin_p3d65_scene", - { cicp_primaries_p3d65, cicp_transfer_linear, cicp_matrix_bt709, - cicp_range_full } }, - { "lin_rec2020_scene", - { cicp_primaries_rec2020, cicp_transfer_linear, cicp_matrix_rec2020_cl, - cicp_range_full } }, - { "lin_ciexyzd65_scene", - { cicp_primaries_xyzd65, cicp_transfer_linear, cicp_matrix_unspecified, - cicp_range_full } }, + { "lin_rec709_scene", CICPPrimaries::Rec709, CICPTransfer::Linear, + CICPMatrix::BT709 }, + { "lin_p3d65_scene", CICPPrimaries::P3D65, CICPTransfer::Linear, + CICPMatrix::BT709 }, + { "lin_rec2020_scene", CICPPrimaries::Rec2020, CICPTransfer::Linear, + CICPMatrix::Rec2020_CL }, + { "lin_ciexyzd65_scene", CICPPrimaries::XYZD65, CICPTransfer::Linear, + CICPMatrix::Unspecified }, // Display referred interop IDs. - { "srgb_rec709_display", - { cicp_primaries_rec709, cicp_transfer_srgb, cicp_matrix_bt709, - cicp_range_full } }, - { "g24_rec709_display", - { cicp_primaries_rec709, cicp_transfer_bt709, cicp_matrix_bt709, - cicp_range_full } }, - { "srgb_p3d65_display", - { cicp_primaries_p3d65, cicp_transfer_srgb, cicp_matrix_bt709, - cicp_range_full } }, - { "srgbe_p3d65_display", - { cicp_primaries_p3d65, cicp_transfer_srgb, cicp_matrix_bt709, - cicp_range_full } }, - { "pq_p3d65_display", - { cicp_primaries_p3d65, cicp_transfer_pq, cicp_matrix_rec2020_ncl, - cicp_range_full } }, - { "pq_rec2020_display", - { cicp_primaries_rec2020, cicp_transfer_pq, cicp_matrix_rec2020_ncl, - cicp_range_full } }, - { "hlg_rec2020_display", - { cicp_primaries_rec2020, cicp_transfer_hlg, cicp_matrix_rec2020_ncl, - cicp_range_full } }, - { "g22_rec709_display", - { cicp_primaries_rec709, cicp_transfer_g22, cicp_matrix_bt709, - cicp_range_full } }, + { "srgb_rec709_display", CICPPrimaries::Rec709, CICPTransfer::sRGB, + CICPMatrix::BT709 }, + { "g24_rec709_display", CICPPrimaries::Rec709, CICPTransfer::BT709, + CICPMatrix::BT709 }, + { "srgb_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::sRGB, + CICPMatrix::BT709 }, + { "srgbe_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::sRGB, + CICPMatrix::BT709 }, + { "pq_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::PQ, + CICPMatrix::Rec2020_NCL }, + { "pq_rec2020_display", CICPPrimaries::Rec2020, CICPTransfer::PQ, + CICPMatrix::Rec2020_NCL }, + { "hlg_rec2020_display", CICPPrimaries::Rec2020, CICPTransfer::HLG, + CICPMatrix::Rec2020_NCL }, + { "g22_rec709_display", CICPPrimaries::Rec709, CICPTransfer::Gamma22, + CICPMatrix::BT709 }, // No CICP code for Adobe RGB primaries. // { "g22_adobergb_display" } - { "g26_p3d65_display", - { cicp_primaries_p3d65, cicp_transfer_g26, cicp_matrix_bt709, - cicp_range_full } }, - { "g26_xyzd65_display", - { cicp_primaries_xyzd65, cicp_transfer_g26, cicp_matrix_unspecified, - cicp_range_full } }, - { "pq_xyzd65_display", - { cicp_primaries_xyzd65, cicp_transfer_pq, cicp_matrix_unspecified, - cicp_range_full } }, + { "g26_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::Gamma26, + CICPMatrix::BT709 }, + { "g26_xyzd65_display", CICPPrimaries::XYZD65, CICPTransfer::Gamma26, + CICPMatrix::Unspecified }, + { "pq_xyzd65_display", CICPPrimaries::XYZD65, CICPTransfer::PQ, + CICPMatrix::Unspecified }, }; } // namespace @@ -2755,7 +2745,7 @@ ColorConfig::set_colorspace_cicp(ImageSpec& spec, const int cicp[4]) const spec.attribute("CICP", TypeDesc(TypeDesc::INT, 4), cicp); for (const ColorSpaceCICP& space : color_space_cicp) { - if (space.cicp[0] == cicp[0] && space.cicp[1] == cicp[1]) { + if (int(space.primaries) == cicp[0] && int(space.transfer) == cicp[1]) { set_colorspace(spec, space.interop_id); return; } @@ -2781,7 +2771,10 @@ ColorConfig::get_colorspace_cicp(ImageSpec& spec, bool auto_cicp, if (!colorspace.empty()) { for (const ColorSpaceCICP& space : color_space_cicp) { if (equivalent(colorspace, space.interop_id)) { - std::copy_n(space.cicp, 4, cicp); + cicp[0] = int(space.primaries); + cicp[1] = int(space.transfer); + cicp[2] = int(space.matrix); + cicp[3] = int(CICPRange::Full); return true; } } From b9ea1c9a8ea1de86806bb4ecea4519ff02b29d80 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Thu, 27 Nov 2025 23:39:12 +0100 Subject: [PATCH 04/12] Change API functions to getCICP and getColorInteropID, keep more logic in file formats Signed-off-by: Brecht Van Lommel --- src/ffmpeg.imageio/ffmpeginput.cpp | 6 +- src/heif.imageio/heifinput.cpp | 5 +- src/heif.imageio/heifoutput.cpp | 9 +- src/include/OpenImageIO/color.h | 29 +- src/libOpenImageIO/color_ocio.cpp | 249 +++++++++--------- src/png.imageio/png_pvt.h | 17 +- src/python/py_colorconfig.cpp | 15 ++ .../python-colorconfig/ref/out-ocio23.txt | 3 + .../python-colorconfig/ref/out-ocio24.txt | 3 + .../python-colorconfig/ref/out-ocio25.txt | 3 + .../src/test_colorconfig.py | 3 + 11 files changed, 186 insertions(+), 156 deletions(-) diff --git a/src/ffmpeg.imageio/ffmpeginput.cpp b/src/ffmpeg.imageio/ffmpeginput.cpp index 574fe10449..ff198b2bbf 100644 --- a/src/ffmpeg.imageio/ffmpeginput.cpp +++ b/src/ffmpeg.imageio/ffmpeginput.cpp @@ -549,7 +549,11 @@ FFmpegInput::open(const std::string& name, ImageSpec& spec) = { m_codec_context->color_primaries, m_codec_context->color_trc, m_codec_context->colorspace, m_codec_context->color_range == AVCOL_RANGE_MPEG ? 0 : 1 }; - ColorConfig::default_colorconfig().set_colorspace_cicp(m_spec, cicp); + m_spec.attribute("CICP", TypeDesc(TypeDesc::INT, 4), cicp); + const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); + string_view interop_id = colorconfig.getColorInteropID(cicp); + if (!interop_id.empty()) + m_spec.attribute("oiio:ColorSpace", interop_id); m_nsubimages = m_frames; spec = m_spec; diff --git a/src/heif.imageio/heifinput.cpp b/src/heif.imageio/heifinput.cpp index c615913c6d..3d673e113c 100644 --- a/src/heif.imageio/heifinput.cpp +++ b/src/heif.imageio/heifinput.cpp @@ -292,9 +292,12 @@ HeifInput::seek_subimage(int subimage, int miplevel) int(nclx->transfer_characteristics), int(nclx->matrix_coefficients), int(nclx->full_range_flag ? 1 : 0) }; + m_spec.attribute("CICP", TypeDesc(TypeDesc::INT, 4), cicp); const ColorConfig& colorconfig( ColorConfig::default_colorconfig()); - colorconfig.set_colorspace_cicp(m_spec, cicp); + string_view interop_id = colorconfig.getColorInteropID(cicp); + if (!interop_id.empty()) + m_spec.attribute("oiio:ColorSpace", interop_id); } heif_nclx_color_profile_free(nclx); } diff --git a/src/heif.imageio/heifoutput.cpp b/src/heif.imageio/heifoutput.cpp index e5e6c34b26..02ff3e023a 100644 --- a/src/heif.imageio/heifoutput.cpp +++ b/src/heif.imageio/heifoutput.cpp @@ -251,9 +251,12 @@ HeifOutput::close() void (*)(heif_color_profile_nclx*)> nclx(heif_nclx_color_profile_alloc(), heif_nclx_color_profile_free); const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); - const bool auto_cicp = true; - int cicp[4]; - if (colorconfig.get_colorspace_cicp(m_spec, auto_cicp, cicp)) { + const ParamValue* p = m_spec.find_attribute("CICP", + TypeDesc(TypeDesc::INT, 4)); + string_view colorspace = m_spec.get_string_attribute("oiio:ColorSpace"); + const int* cicp = (p) ? static_cast(p->data()) + : colorconfig.getCICP(colorspace); + if (cicp) { nclx->color_primaries = heif_color_primaries(cicp[0]); nclx->transfer_characteristics = heif_transfer_characteristics( cicp[1]); diff --git a/src/include/OpenImageIO/color.h b/src/include/OpenImageIO/color.h index 3e8bf12e51..9c91130503 100644 --- a/src/include/OpenImageIO/color.h +++ b/src/include/OpenImageIO/color.h @@ -402,6 +402,18 @@ class OIIO_API ColorConfig { bool equivalent(string_view color_space, string_view other_color_space) const; + /// Find CICP code corresponding to the colorspace. + /// Returns pointer to int[4], and nullptr if not found. + /// + /// @version 3.1 + const int* getCICP(string_view colorspace) const; + + /// Find color interop ID corresponding to the CICP code. + /// Returns empty string if not found. + /// + /// @version 3.1 + string_view getColorInteropID(const int cicp[4]) const; + /// Return a filename or other identifier for the config we're using. std::string configname() const; @@ -422,23 +434,6 @@ class OIIO_API ColorConfig { /// @version 3.0 void set_colorspace_rec709_gamma(ImageSpec& spec, float gamma) const; - /// Set the "CICP" attribute in the spec. If a corresponding colorspace - /// is found, automatically set "oiio:ColorSpace" as well. It also removes - /// or alters several other attributes that may hint color space in ways - /// that might be contradictory or no longer true. - /// - /// @version 3.1 - void set_colorspace_cicp(ImageSpec& spec, const int cicp[4]) const; - - /// Get the CICP code corresponding from the "CICP" attribute. If there - /// is no such attribute and auto_cicp is true, attempt to determine a CICP - /// code from the "oiio:ColorSpace" attribute. - /// Returns false if no CICP code could be determined. - /// - /// @version 3.1 - bool get_colorspace_cicp(ImageSpec& spec, bool auto_cicp, - int cicp[4]) const; - /// Return if OpenImageIO was built with OCIO support static bool supportsOpenColorIO(); diff --git a/src/libOpenImageIO/color_ocio.cpp b/src/libOpenImageIO/color_ocio.cpp index 00b6636a77..e162cc164e 100644 --- a/src/libOpenImageIO/color_ocio.cpp +++ b/src/libOpenImageIO/color_ocio.cpp @@ -1997,6 +1997,127 @@ ColorConfig::parseColorSpaceFromString(string_view str) const } +////////////////////////////////////////////////////////////////////////// +// +// Color Interop ID + +namespace { +enum class CICPPrimaries : int { + Rec709 = 1, + Rec2020 = 9, + XYZD65 = 10, + P3D65 = 12, +}; + +enum class CICPTransfer : int { + BT709 = 1, + Gamma22 = 4, + Linear = 8, + sRGB = 13, + PQ = 16, + Gamma26 = 17, + HLG = 18, +}; + +enum class CICPMatrix : int { + RGB = 0, + BT709 = 1, + Unspecified = 2, + Rec2020_NCL = 9, + Rec2020_CL = 10, +}; + +enum class CICPRange : int { + Narrow = 0, + Full = 1, +}; + +struct ColorInteropID { + constexpr ColorInteropID(const char* interop_id, CICPPrimaries primaries, + CICPTransfer transfer, CICPMatrix matrix) + : interop_id(interop_id) + , cicp({ int(primaries), int(transfer), int(matrix), + int(CICPRange::Full) }) + { + } + + const char* interop_id; + std::array cicp; +}; + +// Mapping between color interop ID and CICP, based on Color Interop Forum +// recommendations. +constexpr ColorInteropID color_interop_ids[] = { + // Scene referred interop IDs first so they are the default in automatic + // conversion from CICP to interop ID. + { "srgb_rec709_scene", CICPPrimaries::Rec709, CICPTransfer::sRGB, + CICPMatrix::BT709 }, + { "srgb_p3d65_scene", CICPPrimaries::P3D65, CICPTransfer::sRGB, + CICPMatrix::BT709 }, + { "g22_rec709_scene", CICPPrimaries::Rec709, CICPTransfer::Gamma22, + CICPMatrix::BT709 }, + // These are not display color spaces at all, but can be represented by CICP. + { "lin_rec709_scene", CICPPrimaries::Rec709, CICPTransfer::Linear, + CICPMatrix::BT709 }, + { "lin_p3d65_scene", CICPPrimaries::P3D65, CICPTransfer::Linear, + CICPMatrix::BT709 }, + { "lin_rec2020_scene", CICPPrimaries::Rec2020, CICPTransfer::Linear, + CICPMatrix::Rec2020_CL }, + { "lin_ciexyzd65_scene", CICPPrimaries::XYZD65, CICPTransfer::Linear, + CICPMatrix::Unspecified }, + + // Display referred interop IDs. + { "srgb_rec709_display", CICPPrimaries::Rec709, CICPTransfer::sRGB, + CICPMatrix::BT709 }, + { "g24_rec709_display", CICPPrimaries::Rec709, CICPTransfer::BT709, + CICPMatrix::BT709 }, + { "srgb_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::sRGB, + CICPMatrix::BT709 }, + { "srgbe_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::sRGB, + CICPMatrix::BT709 }, + { "pq_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::PQ, + CICPMatrix::Rec2020_NCL }, + { "pq_rec2020_display", CICPPrimaries::Rec2020, CICPTransfer::PQ, + CICPMatrix::Rec2020_NCL }, + { "hlg_rec2020_display", CICPPrimaries::Rec2020, CICPTransfer::HLG, + CICPMatrix::Rec2020_NCL }, + { "g22_rec709_display", CICPPrimaries::Rec709, CICPTransfer::Gamma22, + CICPMatrix::BT709 }, + // No CICP code for Adobe RGB primaries. + // { "g22_adobergb_display" } + { "g26_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::Gamma26, + CICPMatrix::BT709 }, + { "g26_xyzd65_display", CICPPrimaries::XYZD65, CICPTransfer::Gamma26, + CICPMatrix::Unspecified }, + { "pq_xyzd65_display", CICPPrimaries::XYZD65, CICPTransfer::PQ, + CICPMatrix::Unspecified }, +}; +} // namespace + +string_view +ColorConfig::getColorInteropID(const int cicp[4]) const +{ + for (const ColorInteropID& interop : color_interop_ids) { + if (interop.cicp[0] == cicp[0] && interop.cicp[1] == cicp[1]) { + return interop.interop_id; + } + } + return ""; +} + +const int* +ColorConfig::getCICP(string_view colorspace) const +{ + if (!colorspace.empty()) { + for (const ColorInteropID& interop : color_interop_ids) { + if (equivalent(colorspace, interop.interop_id)) { + return interop.cicp.data(); + } + } + } + return nullptr; +} + ////////////////////////////////////////////////////////////////////////// // @@ -2655,134 +2776,6 @@ ColorConfig::set_colorspace_rec709_gamma(ImageSpec& spec, float gamma) const } } -namespace { -enum class CICPPrimaries { - Rec709 = 1, - Rec2020 = 9, - XYZD65 = 10, - P3D65 = 12, -}; -enum class CICPTransfer { - BT709 = 1, - Gamma22 = 4, - Linear = 8, - sRGB = 13, - PQ = 16, - Gamma26 = 17, - HLG = 18, -}; -enum class CICPMatrix { - RGB = 0, - BT709 = 1, - Unspecified = 2, - Rec2020_NCL = 9, - Rec2020_CL = 10, -}; -enum class CICPRange { - Narrow = 0, - Full = 1, -}; - -// Mapping between color interop ID and CICP, based on Color Interop Forum -// recommendations. -struct ColorSpaceCICP { - const char* interop_id; - CICPPrimaries primaries; - CICPTransfer transfer; - CICPMatrix matrix; -}; - -constexpr ColorSpaceCICP color_space_cicp[] = { - // Scene referred interop IDs first so they are the default in automatic - // conversion from CICP to interop ID. - { "srgb_rec709_scene", CICPPrimaries::Rec709, CICPTransfer::sRGB, - CICPMatrix::BT709 }, - { "srgb_rec709_scene", CICPPrimaries::Rec709, CICPTransfer::sRGB, - CICPMatrix::BT709 }, - { "srgb_p3d65_scene", CICPPrimaries::P3D65, CICPTransfer::sRGB, - CICPMatrix::BT709 }, - // These are not display color spaces at all, but can be represented by CICP. - { "lin_rec709_scene", CICPPrimaries::Rec709, CICPTransfer::Linear, - CICPMatrix::BT709 }, - { "lin_p3d65_scene", CICPPrimaries::P3D65, CICPTransfer::Linear, - CICPMatrix::BT709 }, - { "lin_rec2020_scene", CICPPrimaries::Rec2020, CICPTransfer::Linear, - CICPMatrix::Rec2020_CL }, - { "lin_ciexyzd65_scene", CICPPrimaries::XYZD65, CICPTransfer::Linear, - CICPMatrix::Unspecified }, - - // Display referred interop IDs. - { "srgb_rec709_display", CICPPrimaries::Rec709, CICPTransfer::sRGB, - CICPMatrix::BT709 }, - { "g24_rec709_display", CICPPrimaries::Rec709, CICPTransfer::BT709, - CICPMatrix::BT709 }, - { "srgb_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::sRGB, - CICPMatrix::BT709 }, - { "srgbe_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::sRGB, - CICPMatrix::BT709 }, - { "pq_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::PQ, - CICPMatrix::Rec2020_NCL }, - { "pq_rec2020_display", CICPPrimaries::Rec2020, CICPTransfer::PQ, - CICPMatrix::Rec2020_NCL }, - { "hlg_rec2020_display", CICPPrimaries::Rec2020, CICPTransfer::HLG, - CICPMatrix::Rec2020_NCL }, - { "g22_rec709_display", CICPPrimaries::Rec709, CICPTransfer::Gamma22, - CICPMatrix::BT709 }, - // No CICP code for Adobe RGB primaries. - // { "g22_adobergb_display" } - { "g26_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::Gamma26, - CICPMatrix::BT709 }, - { "g26_xyzd65_display", CICPPrimaries::XYZD65, CICPTransfer::Gamma26, - CICPMatrix::Unspecified }, - { "pq_xyzd65_display", CICPPrimaries::XYZD65, CICPTransfer::PQ, - CICPMatrix::Unspecified }, -}; -} // namespace - -void -ColorConfig::set_colorspace_cicp(ImageSpec& spec, const int cicp[4]) const -{ - spec.attribute("CICP", TypeDesc(TypeDesc::INT, 4), cicp); - - for (const ColorSpaceCICP& space : color_space_cicp) { - if (int(space.primaries) == cicp[0] && int(space.transfer) == cicp[1]) { - set_colorspace(spec, space.interop_id); - return; - } - } -} - -bool -ColorConfig::get_colorspace_cicp(ImageSpec& spec, bool auto_cicp, - int cicp[4]) const -{ - const ParamValue* p = spec.find_attribute("CICP", - TypeDesc(TypeDesc::INT, 4)); - if (p) { - std::copy_n(static_cast(p->data()), 4, cicp); - return true; - } - - if (!auto_cicp) { - return false; - } - - string_view colorspace = spec.get_string_attribute("oiio:ColorSpace"); - if (!colorspace.empty()) { - for (const ColorSpaceCICP& space : color_space_cicp) { - if (equivalent(colorspace, space.interop_id)) { - cicp[0] = int(space.primaries); - cicp[1] = int(space.transfer); - cicp[2] = int(space.matrix); - cicp[3] = int(CICPRange::Full); - return true; - } - } - } - - return false; -} - void set_colorspace(ImageSpec& spec, string_view colorspace) { diff --git a/src/png.imageio/png_pvt.h b/src/png.imageio/png_pvt.h index 6a887e6667..280a9fba00 100644 --- a/src/png.imageio/png_pvt.h +++ b/src/png.imageio/png_pvt.h @@ -40,6 +40,7 @@ For further information see the following mailing list threads: OIIO_PLUGIN_NAMESPACE_BEGIN #define ICC_PROFILE_ATTR "ICCProfile" +#define CICP_ATTR "CICP" namespace PNG_pvt { @@ -330,8 +331,11 @@ read_info(png_structp& sp, png_infop& ip, int& bit_depth, int& color_type, png_byte pri = 0, trc = 0, mtx = 0, vfr = 0; if (png_get_cICP(sp, ip, &pri, &trc, &mtx, &vfr)) { const int cicp[4] = { pri, trc, mtx, vfr }; + spec.attribute(CICP_ATTR, TypeDesc(TypeDesc::INT, 4), cicp); const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); - colorconfig.set_colorspace_cicp(spec, cicp); + string_view interop_id = colorconfig.getColorInteropID(cicp); + if (!interop_id.empty()) + spec.attribute("oiio:ColorSpace", interop_id); } } #endif @@ -734,11 +738,12 @@ write_info(png_structp& sp, png_infop& ip, int& color_type, ImageSpec& spec, #ifdef PNG_cICP_SUPPORTED // Only automatically determine CICP from oiio::ColorSpace if we didn't // write colorspace metadata yet. - const bool auto_cicp = !wrote_colorspace; - int cicp[4]; - if (colorconfig.get_colorspace_cicp(spec, auto_cicp, cicp)) { - // Matrix must be RGB according to PNG spec v3 - cicp[2] = 0; + const ParamValue* p = spec.find_attribute(CICP_ATTR, + TypeDesc(TypeDesc::INT, 4)); + const int* cicp = (p) ? static_cast(p->data()) + : (!wrote_colorspace) ? colorconfig.getCICP(colorspace) + : nullptr; + if (cicp) { png_byte vals[4]; for (int i = 0; i < 4; ++i) vals[i] = static_cast(cicp[i]); diff --git a/src/python/py_colorconfig.cpp b/src/python/py_colorconfig.cpp index 129d089502..9dfd5208ab 100644 --- a/src/python/py_colorconfig.cpp +++ b/src/python/py_colorconfig.cpp @@ -4,6 +4,7 @@ #include "py_oiio.h" #include +#include #include namespace PyOpenImageIO { @@ -160,6 +161,20 @@ declare_colorconfig(py::module& m) return self.equivalent(color_space, other_color_space); }, "color_space"_a, "other_color_space"_a) + .def("getColorInteropID", + [](const ColorConfig& self, const std::array cicp) { + return std::string(self.getColorInteropID(cicp.data())); + }) + .def("getCICP", + [](const ColorConfig& self, const std::string& colorspace) + -> std::optional> { + const int* cicp = self.getCICP(colorspace); + if (cicp) { + return std::array( + { cicp[0], cicp[1], cicp[2], cicp[3] }); + } + return std::nullopt; + }) .def("configname", &ColorConfig::configname) .def_static("default_colorconfig", []() -> const ColorConfig& { return ColorConfig::default_colorconfig(); diff --git a/testsuite/python-colorconfig/ref/out-ocio23.txt b/testsuite/python-colorconfig/ref/out-ocio23.txt index 883ef051d4..2dc2f0c9be 100644 --- a/testsuite/python-colorconfig/ref/out-ocio23.txt +++ b/testsuite/python-colorconfig/ref/out-ocio23.txt @@ -26,6 +26,9 @@ equivalent('linear', 'lin_srgb'): False equivalent('scene_linear', 'lin_srgb'): False equivalent('ACEScg', 'scene_linear'): True equivalent('lnf', 'scene_linear'): False +getColorInteropID([1, 13, 1, 1]) = srgb_rec709_scene +getCICP('pq_rec2020_display') = [9, 16, 9, 1] +getCICP('unknown_interop_id') = None Loaded test OCIO config: oiio_test_v0.9.2.ocio Parsed color space for filepath 'foo_lin_ap1.exr': ACEScg diff --git a/testsuite/python-colorconfig/ref/out-ocio24.txt b/testsuite/python-colorconfig/ref/out-ocio24.txt index 1bf39d0cb3..68ae85d9c8 100644 --- a/testsuite/python-colorconfig/ref/out-ocio24.txt +++ b/testsuite/python-colorconfig/ref/out-ocio24.txt @@ -26,6 +26,9 @@ equivalent('linear', 'lin_srgb'): False equivalent('scene_linear', 'lin_srgb'): False equivalent('ACEScg', 'scene_linear'): True equivalent('lnf', 'scene_linear'): False +getColorInteropID([1, 13, 1, 1]) = srgb_rec709_scene +getCICP('pq_rec2020_display') = [9, 16, 9, 1] +getCICP('unknown_interop_id') = None Loaded test OCIO config: oiio_test_v0.9.2.ocio Parsed color space for filepath 'foo_lin_ap1.exr': ACEScg diff --git a/testsuite/python-colorconfig/ref/out-ocio25.txt b/testsuite/python-colorconfig/ref/out-ocio25.txt index 07569ff915..557b2f29c9 100644 --- a/testsuite/python-colorconfig/ref/out-ocio25.txt +++ b/testsuite/python-colorconfig/ref/out-ocio25.txt @@ -26,6 +26,9 @@ equivalent('linear', 'lin_srgb'): False equivalent('scene_linear', 'lin_srgb'): False equivalent('ACEScg', 'scene_linear'): True equivalent('lnf', 'scene_linear'): False +getColorInteropID([1, 13, 1, 1]) = srgb_rec709_scene +getCICP('pq_rec2020_display') = [9, 16, 9, 1] +getCICP('unknown_interop_id') = None Loaded test OCIO config: oiio_test_v0.9.2.ocio Parsed color space for filepath 'foo_lin_ap1.exr': ACEScg diff --git a/testsuite/python-colorconfig/src/test_colorconfig.py b/testsuite/python-colorconfig/src/test_colorconfig.py index 597223e785..cfa32b893a 100755 --- a/testsuite/python-colorconfig/src/test_colorconfig.py +++ b/testsuite/python-colorconfig/src/test_colorconfig.py @@ -48,6 +48,9 @@ print ("equivalent('scene_linear', 'lin_srgb'):", config.equivalent("scene_linear", "lin_srgb")) print ("equivalent('ACEScg', 'scene_linear'):", config.equivalent("ACEScg", "scene_linear")) print ("equivalent('lnf', 'scene_linear'):", config.equivalent("lnf", "scene_linear")) + print ("getColorInteropID([1, 13, 1, 1]) = ", config.getColorInteropID([1, 13, 1, 1])) + print ("getCICP('pq_rec2020_display') = ", config.getCICP("pq_rec2020_display")) + print ("getCICP('unknown_interop_id') = ", config.getCICP("unknown_interop_id")) print ("") config = oiio.ColorConfig(str(TEST_CONFIG_PATH)) From 822b06f3fc05a1b1b9fa2bf661e56693b1499cb0 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Fri, 28 Nov 2025 01:10:47 +0100 Subject: [PATCH 05/12] Use cspan and snake case Signed-off-by: Brecht Van Lommel --- src/ffmpeg.imageio/ffmpeginput.cpp | 2 +- src/heif.imageio/heifinput.cpp | 2 +- src/heif.imageio/heifoutput.cpp | 6 +++--- src/include/OpenImageIO/color.h | 6 +++--- src/libOpenImageIO/color_ocio.cpp | 11 ++++++----- src/png.imageio/png_pvt.h | 10 +++++----- src/python/py_colorconfig.cpp | 10 +++++----- testsuite/python-colorconfig/ref/out-ocio23.txt | 6 +++--- testsuite/python-colorconfig/ref/out-ocio24.txt | 6 +++--- testsuite/python-colorconfig/ref/out-ocio25.txt | 6 +++--- testsuite/python-colorconfig/src/test_colorconfig.py | 6 +++--- 11 files changed, 36 insertions(+), 35 deletions(-) diff --git a/src/ffmpeg.imageio/ffmpeginput.cpp b/src/ffmpeg.imageio/ffmpeginput.cpp index ff198b2bbf..26227dd61c 100644 --- a/src/ffmpeg.imageio/ffmpeginput.cpp +++ b/src/ffmpeg.imageio/ffmpeginput.cpp @@ -551,7 +551,7 @@ FFmpegInput::open(const std::string& name, ImageSpec& spec) m_codec_context->color_range == AVCOL_RANGE_MPEG ? 0 : 1 }; m_spec.attribute("CICP", TypeDesc(TypeDesc::INT, 4), cicp); const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); - string_view interop_id = colorconfig.getColorInteropID(cicp); + string_view interop_id = colorconfig.get_color_interop_id(cicp); if (!interop_id.empty()) m_spec.attribute("oiio:ColorSpace", interop_id); diff --git a/src/heif.imageio/heifinput.cpp b/src/heif.imageio/heifinput.cpp index 3d673e113c..349bcdb1d4 100644 --- a/src/heif.imageio/heifinput.cpp +++ b/src/heif.imageio/heifinput.cpp @@ -295,7 +295,7 @@ HeifInput::seek_subimage(int subimage, int miplevel) m_spec.attribute("CICP", TypeDesc(TypeDesc::INT, 4), cicp); const ColorConfig& colorconfig( ColorConfig::default_colorconfig()); - string_view interop_id = colorconfig.getColorInteropID(cicp); + string_view interop_id = colorconfig.get_color_interop_id(cicp); if (!interop_id.empty()) m_spec.attribute("oiio:ColorSpace", interop_id); } diff --git a/src/heif.imageio/heifoutput.cpp b/src/heif.imageio/heifoutput.cpp index 02ff3e023a..9998ab5a5f 100644 --- a/src/heif.imageio/heifoutput.cpp +++ b/src/heif.imageio/heifoutput.cpp @@ -254,9 +254,9 @@ HeifOutput::close() const ParamValue* p = m_spec.find_attribute("CICP", TypeDesc(TypeDesc::INT, 4)); string_view colorspace = m_spec.get_string_attribute("oiio:ColorSpace"); - const int* cicp = (p) ? static_cast(p->data()) - : colorconfig.getCICP(colorspace); - if (cicp) { + cspan cicp = (p) ? p->as_cspan() + : colorconfig.get_cicp(colorspace); + if (!cicp.empty()) { nclx->color_primaries = heif_color_primaries(cicp[0]); nclx->transfer_characteristics = heif_transfer_characteristics( cicp[1]); diff --git a/src/include/OpenImageIO/color.h b/src/include/OpenImageIO/color.h index 9c91130503..3c590b6705 100644 --- a/src/include/OpenImageIO/color.h +++ b/src/include/OpenImageIO/color.h @@ -403,16 +403,16 @@ class OIIO_API ColorConfig { string_view other_color_space) const; /// Find CICP code corresponding to the colorspace. - /// Returns pointer to int[4], and nullptr if not found. + /// Return a cspan of 4 ints, or an empty span if not found. /// /// @version 3.1 - const int* getCICP(string_view colorspace) const; + cspan get_cicp(string_view colorspace) const; /// Find color interop ID corresponding to the CICP code. /// Returns empty string if not found. /// /// @version 3.1 - string_view getColorInteropID(const int cicp[4]) const; + string_view get_color_interop_id(const int cicp[4]) const; /// Return a filename or other identifier for the config we're using. std::string configname() const; diff --git a/src/libOpenImageIO/color_ocio.cpp b/src/libOpenImageIO/color_ocio.cpp index e162cc164e..13064fd829 100644 --- a/src/libOpenImageIO/color_ocio.cpp +++ b/src/libOpenImageIO/color_ocio.cpp @@ -2095,7 +2095,7 @@ constexpr ColorInteropID color_interop_ids[] = { } // namespace string_view -ColorConfig::getColorInteropID(const int cicp[4]) const +ColorConfig::get_color_interop_id(const int cicp[4]) const { for (const ColorInteropID& interop : color_interop_ids) { if (interop.cicp[0] == cicp[0] && interop.cicp[1] == cicp[1]) { @@ -2105,17 +2105,17 @@ ColorConfig::getColorInteropID(const int cicp[4]) const return ""; } -const int* -ColorConfig::getCICP(string_view colorspace) const +cspan +ColorConfig::get_cicp(string_view colorspace) const { if (!colorspace.empty()) { for (const ColorInteropID& interop : color_interop_ids) { if (equivalent(colorspace, interop.interop_id)) { - return interop.cicp.data(); + return interop.cicp; } } } - return nullptr; + return cspan(); } @@ -2776,6 +2776,7 @@ ColorConfig::set_colorspace_rec709_gamma(ImageSpec& spec, float gamma) const } } + void set_colorspace(ImageSpec& spec, string_view colorspace) { diff --git a/src/png.imageio/png_pvt.h b/src/png.imageio/png_pvt.h index 280a9fba00..3c103eb836 100644 --- a/src/png.imageio/png_pvt.h +++ b/src/png.imageio/png_pvt.h @@ -333,7 +333,7 @@ read_info(png_structp& sp, png_infop& ip, int& bit_depth, int& color_type, const int cicp[4] = { pri, trc, mtx, vfr }; spec.attribute(CICP_ATTR, TypeDesc(TypeDesc::INT, 4), cicp); const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); - string_view interop_id = colorconfig.getColorInteropID(cicp); + string_view interop_id = colorconfig.get_color_interop_id(cicp); if (!interop_id.empty()) spec.attribute("oiio:ColorSpace", interop_id); } @@ -740,10 +740,10 @@ write_info(png_structp& sp, png_infop& ip, int& color_type, ImageSpec& spec, // write colorspace metadata yet. const ParamValue* p = spec.find_attribute(CICP_ATTR, TypeDesc(TypeDesc::INT, 4)); - const int* cicp = (p) ? static_cast(p->data()) - : (!wrote_colorspace) ? colorconfig.getCICP(colorspace) - : nullptr; - if (cicp) { + cspan cicp = (p) ? p->as_cspan() + : (!wrote_colorspace) ? colorconfig.get_cicp(colorspace) + : cspan(); + if (!cicp.empty()) { png_byte vals[4]; for (int i = 0; i < 4; ++i) vals[i] = static_cast(cicp[i]); diff --git a/src/python/py_colorconfig.cpp b/src/python/py_colorconfig.cpp index 9dfd5208ab..dd868509df 100644 --- a/src/python/py_colorconfig.cpp +++ b/src/python/py_colorconfig.cpp @@ -161,15 +161,15 @@ declare_colorconfig(py::module& m) return self.equivalent(color_space, other_color_space); }, "color_space"_a, "other_color_space"_a) - .def("getColorInteropID", + .def("get_color_interop_id", [](const ColorConfig& self, const std::array cicp) { - return std::string(self.getColorInteropID(cicp.data())); + return std::string(self.get_color_interop_id(cicp.data())); }) - .def("getCICP", + .def("get_cicp", [](const ColorConfig& self, const std::string& colorspace) -> std::optional> { - const int* cicp = self.getCICP(colorspace); - if (cicp) { + cspan cicp = self.get_cicp(colorspace); + if (!cicp.empty()) { return std::array( { cicp[0], cicp[1], cicp[2], cicp[3] }); } diff --git a/testsuite/python-colorconfig/ref/out-ocio23.txt b/testsuite/python-colorconfig/ref/out-ocio23.txt index 2dc2f0c9be..6feb4fcdfd 100644 --- a/testsuite/python-colorconfig/ref/out-ocio23.txt +++ b/testsuite/python-colorconfig/ref/out-ocio23.txt @@ -26,9 +26,9 @@ equivalent('linear', 'lin_srgb'): False equivalent('scene_linear', 'lin_srgb'): False equivalent('ACEScg', 'scene_linear'): True equivalent('lnf', 'scene_linear'): False -getColorInteropID([1, 13, 1, 1]) = srgb_rec709_scene -getCICP('pq_rec2020_display') = [9, 16, 9, 1] -getCICP('unknown_interop_id') = None +get_color_interop_id([1, 13, 1, 1]) = srgb_rec709_scene +get_cicp('pq_rec2020_display') = [9, 16, 9, 1] +get_cicp('unknown_interop_id') = None Loaded test OCIO config: oiio_test_v0.9.2.ocio Parsed color space for filepath 'foo_lin_ap1.exr': ACEScg diff --git a/testsuite/python-colorconfig/ref/out-ocio24.txt b/testsuite/python-colorconfig/ref/out-ocio24.txt index 68ae85d9c8..1e2ca5a055 100644 --- a/testsuite/python-colorconfig/ref/out-ocio24.txt +++ b/testsuite/python-colorconfig/ref/out-ocio24.txt @@ -26,9 +26,9 @@ equivalent('linear', 'lin_srgb'): False equivalent('scene_linear', 'lin_srgb'): False equivalent('ACEScg', 'scene_linear'): True equivalent('lnf', 'scene_linear'): False -getColorInteropID([1, 13, 1, 1]) = srgb_rec709_scene -getCICP('pq_rec2020_display') = [9, 16, 9, 1] -getCICP('unknown_interop_id') = None +get_color_interop_id([1, 13, 1, 1]) = srgb_rec709_scene +get_cicp('pq_rec2020_display') = [9, 16, 9, 1] +get_cicp('unknown_interop_id') = None Loaded test OCIO config: oiio_test_v0.9.2.ocio Parsed color space for filepath 'foo_lin_ap1.exr': ACEScg diff --git a/testsuite/python-colorconfig/ref/out-ocio25.txt b/testsuite/python-colorconfig/ref/out-ocio25.txt index 557b2f29c9..6e8738d5ac 100644 --- a/testsuite/python-colorconfig/ref/out-ocio25.txt +++ b/testsuite/python-colorconfig/ref/out-ocio25.txt @@ -26,9 +26,9 @@ equivalent('linear', 'lin_srgb'): False equivalent('scene_linear', 'lin_srgb'): False equivalent('ACEScg', 'scene_linear'): True equivalent('lnf', 'scene_linear'): False -getColorInteropID([1, 13, 1, 1]) = srgb_rec709_scene -getCICP('pq_rec2020_display') = [9, 16, 9, 1] -getCICP('unknown_interop_id') = None +get_color_interop_id([1, 13, 1, 1]) = srgb_rec709_scene +get_cicp('pq_rec2020_display') = [9, 16, 9, 1] +get_cicp('unknown_interop_id') = None Loaded test OCIO config: oiio_test_v0.9.2.ocio Parsed color space for filepath 'foo_lin_ap1.exr': ACEScg diff --git a/testsuite/python-colorconfig/src/test_colorconfig.py b/testsuite/python-colorconfig/src/test_colorconfig.py index cfa32b893a..01f31ce510 100755 --- a/testsuite/python-colorconfig/src/test_colorconfig.py +++ b/testsuite/python-colorconfig/src/test_colorconfig.py @@ -48,9 +48,9 @@ print ("equivalent('scene_linear', 'lin_srgb'):", config.equivalent("scene_linear", "lin_srgb")) print ("equivalent('ACEScg', 'scene_linear'):", config.equivalent("ACEScg", "scene_linear")) print ("equivalent('lnf', 'scene_linear'):", config.equivalent("lnf", "scene_linear")) - print ("getColorInteropID([1, 13, 1, 1]) = ", config.getColorInteropID([1, 13, 1, 1])) - print ("getCICP('pq_rec2020_display') = ", config.getCICP("pq_rec2020_display")) - print ("getCICP('unknown_interop_id') = ", config.getCICP("unknown_interop_id")) + print ("get_color_interop_id([1, 13, 1, 1]) = ", config.get_color_interop_id([1, 13, 1, 1])) + 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 ("") config = oiio.ColorConfig(str(TEST_CONFIG_PATH)) From 2ed2e06795ac6e1e25e3eb15732e3e2901b47aa4 Mon Sep 17 00:00:00 2001 From: Larry Gritz Date: Sat, 29 Nov 2025 14:16:51 -0800 Subject: [PATCH 06/12] Update python stubs Signed-off-by: Larry Gritz --- src/python/stubs/OpenImageIO/__init__.pyi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/python/stubs/OpenImageIO/__init__.pyi b/src/python/stubs/OpenImageIO/__init__.pyi index 200be7d774..93a038d009 100644 --- a/src/python/stubs/OpenImageIO/__init__.pyi +++ b/src/python/stubs/OpenImageIO/__init__.pyi @@ -207,6 +207,8 @@ class ColorConfig: def getRoles(self) -> list[str]: ... def getViewNameByIndex(self, display: str = ..., *, index: typing.SupportsInt) -> str: ... def getViewNames(self, display: str = ...) -> list[str]: ... + def get_cicp(self, *args, **kwargs): ... + def get_color_interop_id(self, arg0, /) -> str: ... def geterror(self) -> str: ... def parseColorSpaceFromString(self, arg0: str, /) -> str: ... def resolve(self, name: str) -> str: ... From 8cd3c0fcd8864b8f7d27ef864c2a23f5ff4bb983 Mon Sep 17 00:00:00 2001 From: Larry Gritz Date: Sat, 29 Nov 2025 16:51:22 -0800 Subject: [PATCH 07/12] Emergency fix: change sonarqube action Signed-off-by: Larry Gritz --- .github/workflows/build-steps.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-steps.yml b/.github/workflows/build-steps.yml index 403d94e7b0..7c3fedd55c 100644 --- a/.github/workflows/build-steps.yml +++ b/.github/workflows/build-steps.yml @@ -156,7 +156,7 @@ jobs: fi - name: Install sonar-scanner and build-wrapper if: inputs.sonar == '1' - uses: sonarsource/sonarcloud-github-c-cpp@e4882e1621ad2fb48dddfa48287411bed34789b1 # v2.0.2 + uses: sonarsource/sonarqube-scan-action@fd88b7d7ccbaefd23d8f36f73b59db7a3d246602 # v6.0.0 - name: Build if: inputs.skip_build != '1' shell: bash From 6c5f7f23d92135212ff0b4f9e09a80f5066a6d71 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Mon, 1 Dec 2025 01:05:26 +0100 Subject: [PATCH 08/12] Take into account OCIO config interop_id for mapping to CICP on write Signed-off-by: Brecht Van Lommel --- src/include/OpenImageIO/color.h | 6 ++ src/libOpenImageIO/color_ocio.cpp | 65 +++++++++++++++---- src/python/py_colorconfig.cpp | 4 ++ .../python-colorconfig/ref/out-ocio23.txt | 2 + .../python-colorconfig/ref/out-ocio24.txt | 2 + .../python-colorconfig/ref/out-ocio25.txt | 2 + testsuite/python-colorconfig/ref/out.txt | 5 ++ .../src/test_colorconfig.py | 2 + 8 files changed, 77 insertions(+), 11 deletions(-) diff --git a/src/include/OpenImageIO/color.h b/src/include/OpenImageIO/color.h index 3c590b6705..99eba56396 100644 --- a/src/include/OpenImageIO/color.h +++ b/src/include/OpenImageIO/color.h @@ -408,6 +408,12 @@ class OIIO_API ColorConfig { /// @version 3.1 cspan get_cicp(string_view colorspace) const; + /// Find color interop ID for the given colorspace. + /// Returns empty string if not found. + /// + /// @version 3.1 + string_view get_color_interop_id(string_view colorspace) const; + /// Find color interop ID corresponding to the CICP code. /// Returns empty string if not found. /// diff --git a/src/libOpenImageIO/color_ocio.cpp b/src/libOpenImageIO/color_ocio.cpp index 13064fd829..40aaa38177 100644 --- a/src/libOpenImageIO/color_ocio.cpp +++ b/src/libOpenImageIO/color_ocio.cpp @@ -2033,38 +2033,56 @@ enum class CICPRange : int { }; struct ColorInteropID { + constexpr ColorInteropID(const char* interop_id) + : interop_id(interop_id) + , cicp({ 0, 0, 0, 0 }) + , has_cicp(false) + { + } + constexpr ColorInteropID(const char* interop_id, CICPPrimaries primaries, CICPTransfer transfer, CICPMatrix matrix) : interop_id(interop_id) , cicp({ int(primaries), int(transfer), int(matrix), int(CICPRange::Full) }) + , has_cicp(true) { } const char* interop_id; std::array cicp; + bool has_cicp; }; // Mapping between color interop ID and CICP, based on Color Interop Forum // recommendations. constexpr ColorInteropID color_interop_ids[] = { // Scene referred interop IDs first so they are the default in automatic - // conversion from CICP to interop ID. - { "srgb_rec709_scene", CICPPrimaries::Rec709, CICPTransfer::sRGB, - CICPMatrix::BT709 }, - { "srgb_p3d65_scene", CICPPrimaries::P3D65, CICPTransfer::sRGB, - CICPMatrix::BT709 }, - { "g22_rec709_scene", CICPPrimaries::Rec709, CICPTransfer::Gamma22, - CICPMatrix::BT709 }, - // These are not display color spaces at all, but can be represented by CICP. + // conversion from CICP to interop ID. Some are not display color spaces + // at all, but can be represented by CICP anyway. + { "lin_ap1_scene" }, + { "lin_ap0_scene" }, { "lin_rec709_scene", CICPPrimaries::Rec709, CICPTransfer::Linear, CICPMatrix::BT709 }, { "lin_p3d65_scene", CICPPrimaries::P3D65, CICPTransfer::Linear, CICPMatrix::BT709 }, { "lin_rec2020_scene", CICPPrimaries::Rec2020, CICPTransfer::Linear, CICPMatrix::Rec2020_CL }, + { "lin_adobergb_scene" }, { "lin_ciexyzd65_scene", CICPPrimaries::XYZD65, CICPTransfer::Linear, CICPMatrix::Unspecified }, + { "srgb_rec709_scene", CICPPrimaries::Rec709, CICPTransfer::sRGB, + CICPMatrix::BT709 }, + { "g22_rec709_scene", CICPPrimaries::Rec709, CICPTransfer::Gamma22, + CICPMatrix::BT709 }, + { "g18_rec709_scene" }, + { "srgb_ap1_scene" }, + { "g22_ap1_scene" }, + { "srgb_p3d65_scene", CICPPrimaries::P3D65, CICPTransfer::sRGB, + CICPMatrix::BT709 }, + { "g22_adobergb_scene" }, + { "data" }, + { "unknown" }, // Display referred interop IDs. { "srgb_rec709_display", CICPPrimaries::Rec709, CICPTransfer::sRGB, @@ -2084,7 +2102,7 @@ constexpr ColorInteropID color_interop_ids[] = { { "g22_rec709_display", CICPPrimaries::Rec709, CICPTransfer::Gamma22, CICPMatrix::BT709 }, // No CICP code for Adobe RGB primaries. - // { "g22_adobergb_display" } + { "g22_adobergb_display" }, { "g26_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::Gamma26, CICPMatrix::BT709 }, { "g26_xyzd65_display", CICPPrimaries::XYZD65, CICPTransfer::Gamma26, @@ -2094,11 +2112,35 @@ constexpr ColorInteropID color_interop_ids[] = { }; } // namespace +string_view +ColorConfig::get_color_interop_id(string_view colorspace) const +{ + if (colorspace.empty()) + return ""; +#if OCIO_VERSION_HEX >= MAKE_OCIO_VERSION_HEX(2, 5, 0) + if (getImpl()->config_ && !disable_ocio) { + OCIO::ConstColorSpaceRcPtr c = getImpl()->config_->getColorSpace( + std::string(resolve(colorspace)).c_str()); + const char* interop_id = (c) ? c->getInteropID() : nullptr; + if (interop_id) { + return interop_id; + } + } +#endif + for (const ColorInteropID& interop : color_interop_ids) { + if (equivalent(colorspace, interop.interop_id)) { + return interop.interop_id; + } + } + return ""; +} + string_view ColorConfig::get_color_interop_id(const int cicp[4]) const { for (const ColorInteropID& interop : color_interop_ids) { - if (interop.cicp[0] == cicp[0] && interop.cicp[1] == cicp[1]) { + if (interop.has_cicp && interop.cicp[0] == cicp[0] + && interop.cicp[1] == cicp[1]) { return interop.interop_id; } } @@ -2110,7 +2152,8 @@ ColorConfig::get_cicp(string_view colorspace) const { if (!colorspace.empty()) { for (const ColorInteropID& interop : color_interop_ids) { - if (equivalent(colorspace, interop.interop_id)) { + if (interop.has_cicp + && equivalent(colorspace, interop.interop_id)) { return interop.cicp; } } diff --git a/src/python/py_colorconfig.cpp b/src/python/py_colorconfig.cpp index dd868509df..35f8e304e8 100644 --- a/src/python/py_colorconfig.cpp +++ b/src/python/py_colorconfig.cpp @@ -161,6 +161,10 @@ declare_colorconfig(py::module& m) return self.equivalent(color_space, other_color_space); }, "color_space"_a, "other_color_space"_a) + .def("get_color_interop_id", + [](const ColorConfig& self, const std::string& colorspace) { + return std::string(self.get_color_interop_id(colorspace)); + }) .def("get_color_interop_id", [](const ColorConfig& self, const std::array cicp) { return std::string(self.get_color_interop_id(cicp.data())); diff --git a/testsuite/python-colorconfig/ref/out-ocio23.txt b/testsuite/python-colorconfig/ref/out-ocio23.txt index 6feb4fcdfd..1badffcb7f 100644 --- a/testsuite/python-colorconfig/ref/out-ocio23.txt +++ b/testsuite/python-colorconfig/ref/out-ocio23.txt @@ -26,6 +26,8 @@ equivalent('linear', 'lin_srgb'): False equivalent('scene_linear', 'lin_srgb'): False equivalent('ACEScg', 'scene_linear'): True 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 get_cicp('pq_rec2020_display') = [9, 16, 9, 1] get_cicp('unknown_interop_id') = None diff --git a/testsuite/python-colorconfig/ref/out-ocio24.txt b/testsuite/python-colorconfig/ref/out-ocio24.txt index 1e2ca5a055..2882859127 100644 --- a/testsuite/python-colorconfig/ref/out-ocio24.txt +++ b/testsuite/python-colorconfig/ref/out-ocio24.txt @@ -26,6 +26,8 @@ equivalent('linear', 'lin_srgb'): False equivalent('scene_linear', 'lin_srgb'): False equivalent('ACEScg', 'scene_linear'): True 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 get_cicp('pq_rec2020_display') = [9, 16, 9, 1] get_cicp('unknown_interop_id') = None diff --git a/testsuite/python-colorconfig/ref/out-ocio25.txt b/testsuite/python-colorconfig/ref/out-ocio25.txt index 6e8738d5ac..b12d8ce08d 100644 --- a/testsuite/python-colorconfig/ref/out-ocio25.txt +++ b/testsuite/python-colorconfig/ref/out-ocio25.txt @@ -26,6 +26,8 @@ equivalent('linear', 'lin_srgb'): False equivalent('scene_linear', 'lin_srgb'): False equivalent('ACEScg', 'scene_linear'): True 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 get_cicp('pq_rec2020_display') = [9, 16, 9, 1] get_cicp('unknown_interop_id') = None diff --git a/testsuite/python-colorconfig/ref/out.txt b/testsuite/python-colorconfig/ref/out.txt index 9dee6146d9..cb9a3dddfc 100644 --- a/testsuite/python-colorconfig/ref/out.txt +++ b/testsuite/python-colorconfig/ref/out.txt @@ -26,6 +26,11 @@ equivalent('linear', 'lin_srgb'): False equivalent('scene_linear', 'lin_srgb'): False equivalent('ACEScg', 'scene_linear'): True 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 +get_cicp('pq_rec2020_display') = [9, 16, 9, 1] +get_cicp('unknown_interop_id') = None Loaded test OCIO config: oiio_test_v0.9.2.ocio Parsed color space for filepath 'foo_lin_ap1.exr': ACEScg diff --git a/testsuite/python-colorconfig/src/test_colorconfig.py b/testsuite/python-colorconfig/src/test_colorconfig.py index 01f31ce510..766e6a037b 100755 --- a/testsuite/python-colorconfig/src/test_colorconfig.py +++ b/testsuite/python-colorconfig/src/test_colorconfig.py @@ -48,6 +48,8 @@ print ("equivalent('scene_linear', 'lin_srgb'):", config.equivalent("scene_linear", "lin_srgb")) print ("equivalent('ACEScg', 'scene_linear'):", config.equivalent("ACEScg", "scene_linear")) print ("equivalent('lnf', 'scene_linear'):", config.equivalent("lnf", "scene_linear")) + 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 ("get_cicp('pq_rec2020_display') = ", config.get_cicp("pq_rec2020_display")) print ("get_cicp('unknown_interop_id') = ", config.get_cicp("unknown_interop_id")) From 151e75b3d7dd04165fd506ed70665c1e3f66cba9 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Mon, 1 Dec 2025 01:10:28 +0100 Subject: [PATCH 09/12] Don't map g22_rec709_display to any CICP code for now This is unlikely meant to be written with Gamma 2.2 color metadata, so keep behavior unchanged until there is decision on what to do. Signed-off-by: Brecht Van Lommel --- src/libOpenImageIO/color_ocio.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libOpenImageIO/color_ocio.cpp b/src/libOpenImageIO/color_ocio.cpp index 40aaa38177..8f923aabbd 100644 --- a/src/libOpenImageIO/color_ocio.cpp +++ b/src/libOpenImageIO/color_ocio.cpp @@ -2099,8 +2099,11 @@ constexpr ColorInteropID color_interop_ids[] = { CICPMatrix::Rec2020_NCL }, { "hlg_rec2020_display", CICPPrimaries::Rec2020, CICPTransfer::HLG, CICPMatrix::Rec2020_NCL }, - { "g22_rec709_display", CICPPrimaries::Rec709, CICPTransfer::Gamma22, - CICPMatrix::BT709 }, + // No CICP mapping to keep previous behavior unchanged, as Gamma 2.2 + // display is more likely meant to be written as sRGB. On read the + // scene referred interop ID will be used. + { "g22_rec709_display", + /* CICPPrimaries::Rec709, CICPTransfer::Gamma22, CICPMatrix::BT709 */ }, // No CICP code for Adobe RGB primaries. { "g22_adobergb_display" }, { "g26_p3d65_display", CICPPrimaries::P3D65, CICPTransfer::Gamma26, From 5366f577b7d7fd95948256bc2c219d6bddebe673 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Mon, 1 Dec 2025 01:31:20 +0100 Subject: [PATCH 10/12] Add Python docs for new API functions The ColorConfig class was not documented yet, this only documents the new methods. Signed-off-by: Brecht Van Lommel --- src/doc/pythonbindings.rst | 67 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/doc/pythonbindings.rst b/src/doc/pythonbindings.rst index 35a73ff00b..d07735eed2 100644 --- a/src/doc/pythonbindings.rst +++ b/src/doc/pythonbindings.rst @@ -3888,6 +3888,73 @@ sections) work with deep inputs:: | +.. _sec-pythoncolorconfig: + + +ColorConfig +=========== + +The `ColorConfig` class that represents the set of color transformations that +are allowed. + +If OpenColorIO is enabled at build time, this configuration is loaded at +runtime, allowing the user to have complete control of all color transformation +math. See the +`OpenColorIO documentation `_ for details. + +If OpenColorIO is not enabled at build time, a generic color configuration +is provided for minimal color support. + +.. + TODO: The documentation for this class is incomplete. + +.. py:method:: get_cicp (colorspace) + + Find CICP code corresponding to the colorspace. + Return a sequence of 4 ints, or None if not found. + + Example: + + .. code-block:: python + + colorconfig = oiio.ColorConfig() + cicp = colorconfig.get_cicp("pq_rec2020_display") + if cicp: + primaries, transfer, matrix, color_range = cicp + + This function was added in OpenImageIO 3.1. + + +.. py:method:: get_color_interop_id (colorspace) + + Find color interop ID for the given colorspace. + Returns empty string if not found. + + Example: + + .. code-block:: python + + colorconfig = oiio.ColorConfig() + interop_id = colorconfig.get_color_interop_id("Rec.2100-PQ - Display") + + This function was added in OpenImageIO 3.1. + + +.. py:method:: get_color_interop_id (cicp) + + Find color interop ID corresponding to the CICP code. + Returns empty string if not found. + + Example: + + .. code-block:: python + + colorconfig = oiio.ColorConfig() + interop_id = colorconfig.get_color_interop_id([9, 16, 9, 1]) + + This function was added in OpenImageIO 3.1. + + .. _sec-pythonmiscapi: Miscellaneous Utilities From 086b82f428b79afe5cc43bae11daaef2c7bbb431 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Mon, 1 Dec 2025 02:45:46 +0100 Subject: [PATCH 11/12] Update python stubs Signed-off-by: Brecht Van Lommel --- src/python/stubs/OpenImageIO/__init__.pyi | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/python/stubs/OpenImageIO/__init__.pyi b/src/python/stubs/OpenImageIO/__init__.pyi index 93a038d009..88681897d6 100644 --- a/src/python/stubs/OpenImageIO/__init__.pyi +++ b/src/python/stubs/OpenImageIO/__init__.pyi @@ -208,6 +208,9 @@ class ColorConfig: def getViewNameByIndex(self, display: str = ..., *, index: typing.SupportsInt) -> str: ... def getViewNames(self, display: str = ...) -> list[str]: ... def get_cicp(self, *args, **kwargs): ... + @overload + def get_color_interop_id(self, arg0: str, /) -> str: ... + @overload def get_color_interop_id(self, arg0, /) -> str: ... def geterror(self) -> str: ... def parseColorSpaceFromString(self, arg0: str, /) -> str: ... From f94d0524dddf31ea129f0a0a09248ba85e50ae51 Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Mon, 1 Dec 2025 13:29:04 +0100 Subject: [PATCH 12/12] Fix missing logic to use interop_id attribute for CICP Signed-off-by: Brecht Van Lommel --- src/libOpenImageIO/color_ocio.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libOpenImageIO/color_ocio.cpp b/src/libOpenImageIO/color_ocio.cpp index 8f923aabbd..a365cd9b48 100644 --- a/src/libOpenImageIO/color_ocio.cpp +++ b/src/libOpenImageIO/color_ocio.cpp @@ -2153,10 +2153,10 @@ ColorConfig::get_color_interop_id(const int cicp[4]) const cspan ColorConfig::get_cicp(string_view colorspace) const { - if (!colorspace.empty()) { + string_view interop_id = get_color_interop_id(colorspace); + if (!interop_id.empty()) { for (const ColorInteropID& interop : color_interop_ids) { - if (interop.has_cicp - && equivalent(colorspace, interop.interop_id)) { + if (interop.has_cicp && interop_id == interop.interop_id) { return interop.cicp; } }