diff --git a/src/doc/pythonbindings.md b/src/doc/pythonbindings.md index 85c59e0d40..85be59e7d1 100644 --- a/src/doc/pythonbindings.md +++ b/src/doc/pythonbindings.md @@ -4044,6 +4044,9 @@ is provided for minimal color support. Get the RGB primaries and white point of a color space with a known Color Interop ID, as `get_color_interop_id()` finds it: a sequence of eight floats (Rx, Ry, Gx, Gy, Bx, By, Wx, Wy, CIE 1931 xy), or None if they are unknown. +A color space of the config without such an ID, or whose ID OpenImageIO has no +primaries for, has them derived by analyzing its transforms, unless the +`colorconfig:analyze_transforms` attribute is 0. Example: @@ -4059,7 +4062,10 @@ This function was added in OpenImageIO 3.2. :::{py:method} get_transfer_function_gamma (colorspace) Get the exponent of a color space's transfer function if it decodes as a pure power, 1.0 if it is linear, or 0 if it is neither or is unknown. Piecewise -curves such as sRGB's report 0. +curves such as sRGB's report 0. A color space whose config declares an +encoding is linear only if that encoding is linear for its reference type. +Otherwise it answers for a known Color Interop ID, or, like +`get_chromaticities()`, by analyzing transforms. Example: diff --git a/src/include/OpenImageIO/color.h b/src/include/OpenImageIO/color.h index 91be441bcc..6cedc2e918 100644 --- a/src/include/OpenImageIO/color.h +++ b/src/include/OpenImageIO/color.h @@ -453,18 +453,21 @@ class OIIO_API ColorConfig { /// Color Interop ID /// (https://github.com/AcademySoftwareFoundation/ColorInterop), as /// `get_color_interop_id()` finds it: eight floats (Rx, Ry, Gx, Gy, Bx, - /// By, Wx, Wy, CIE 1931 xy), or an empty span if they are unknown. The - /// span is valid until this ColorConfig is reset or destroyed. + /// By, Wx, Wy, CIE 1931 xy), or an empty span if they are unknown. A + /// color space of the config without such an ID, or whose ID OpenImageIO + /// has no primaries for, has them derived by analyzing its transforms, + /// unless the `colorconfig:analyze_transforms` attribute is 0. The span + /// is valid until this ColorConfig is reset or destroyed. /// /// @version 3.2 OIIO_NODISCARD cspan get_chromaticities(string_view colorspace) const; /// Get the exponent of a color space's transfer function if it decodes /// as a pure power, 1.0 if it is linear, or 0 if it is neither or is - /// unknown. Piecewise curves such as sRGB's report 0. Like - /// `get_chromaticities()`, it answers for a known Color Interop ID, and - /// it reports 1.0 for any color space whose config declares a linear - /// encoding. + /// unknown. Piecewise curves such as sRGB's report 0. A color space whose + /// config declares an encoding is linear only if that encoding is linear + /// for its reference type. Otherwise it answers for a known Color Interop + /// ID, or, like `get_chromaticities()`, by analyzing transforms. /// /// @version 3.2 OIIO_NODISCARD float diff --git a/src/include/OpenImageIO/imageio.h b/src/include/OpenImageIO/imageio.h index 0c1e245536..1b8df31709 100644 --- a/src/include/OpenImageIO/imageio.h +++ b/src/include/OpenImageIO/imageio.h @@ -4064,6 +4064,14 @@ OIIO_API std::string geterror(bool clear = true); /// enable globally in an environment where security is a higher priority /// than being tolerant of partially broken image files. /// +/// - `colorconfig:analyze_transforms` (int: 1) +/// +/// If nonzero (the default), `ColorConfig::get_chromaticities()` and +/// `get_transfer_function_gamma()` may analyze the transforms of a color +/// space with no known Color Interop ID to derive its primaries and +/// whether it is linear. If zero, they answer only from what the config +/// declares and the names OpenImageIO knows. +/// /// - `ustring:cleanup` (int: 0) /// /// If nonzero, upon exit, do a thorough (and possibly expensive) teardown diff --git a/src/include/imageio_pvt.h b/src/include/imageio_pvt.h index 28cbc25253..1a788df0a3 100644 --- a/src/include/imageio_pvt.h +++ b/src/include/imageio_pvt.h @@ -50,6 +50,7 @@ extern int limit_resolution; extern int imagebuf_print_uncaught_errors; extern int imagebuf_use_imagecache; extern int imageinput_strict; +extern int colorconfig_analyze_transforms; extern atomic_ll IB_local_mem_current; extern atomic_ll IB_local_mem_peak; extern std::atomic IB_total_open_time; diff --git a/src/libOpenImageIO/CMakeLists.txt b/src/libOpenImageIO/CMakeLists.txt index b7eaa011ce..c916fc9d20 100644 --- a/src/libOpenImageIO/CMakeLists.txt +++ b/src/libOpenImageIO/CMakeLists.txt @@ -58,7 +58,7 @@ set (libOpenImageIO_srcs imageinput.cpp imageio.cpp imageioplugin.cpp imageoutput.cpp iptc.cpp xmp.cpp - color_ocio.cpp + color_ocio.cpp color_derive.cpp maketexture.cpp bluenoise.cpp printinfo.cpp @@ -209,7 +209,8 @@ if (CMAKE_UNITY_BUILD) # ${libOpenImageIO_srcs} deepdata.cpp exif.cpp exif-canon.cpp formatspec.cpp imagebuf.cpp imageinput.cpp imageio.cpp imageioplugin.cpp imageoutput.cpp - iptc.cpp xmp.cpp color_ocio.cpp maketexture.cpp bluenoise.cpp + iptc.cpp xmp.cpp color_ocio.cpp color_derive.cpp maketexture.cpp + bluenoise.cpp PROPERTIES UNITY_GROUP oiiolib) foreach (plugin_dir ${all_format_plugin_dirs} ../libtexture) diff --git a/src/libOpenImageIO/color_derive.cpp b/src/libOpenImageIO/color_derive.cpp new file mode 100644 index 0000000000..0145e63f82 --- /dev/null +++ b/src/libOpenImageIO/color_derive.cpp @@ -0,0 +1,491 @@ +// Copyright Contributors to the OpenImageIO project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/AcademySoftwareFoundation/OpenImageIO + +// Deriving the primaries of a color space from what its transforms do. + +#include +#include +#include +#include +#include +#include + +#include + +#include "color_derive.h" + +namespace OCIO = OCIO_NAMESPACE; + + +OIIO_NAMESPACE_BEGIN + +namespace { + +using Imath::M33d; +using Imath::V3d; + +// Matrices here apply to column vectors. +V3d +apply(const M33d& m, const V3d& v) +{ + return V3d(m[0][0] * v.x + m[0][1] * v.y + m[0][2] * v.z, + m[1][0] * v.x + m[1][1] * v.y + m[1][2] * v.z, + m[2][0] * v.x + m[2][1] * v.y + m[2][2] * v.z); +} + + + +V3d +xy_to_XYZ(double x, double y) +{ + return V3d(x / y, 1.0, (1.0 - x - y) / y); +} + + + +// The RGB to CIE XYZ matrix of primaries and white given as CIE xy. +M33d +rgb_to_XYZ(const double xy[8]) +{ + const V3d r = xy_to_XYZ(xy[0], xy[1]); + const V3d g = xy_to_XYZ(xy[2], xy[3]); + const V3d b = xy_to_XYZ(xy[4], xy[5]); + const M33d primaries(r.x, g.x, b.x, r.y, g.y, b.y, r.z, g.z, b.z); + const V3d s = apply(primaries.inverse(), xy_to_XYZ(xy[6], xy[7])); + return primaries * M33d(s.x, 0.0, 0.0, 0.0, s.y, 0.0, 0.0, 0.0, s.z); +} + + + +// CIE XYZ of the white points a reading is tried under. +const V3d white_D65(0.95045592705167, 1.0, 1.08905775075988); +const V3d white_D60(0.95264607456985, 1.0, 1.00882518435159); // ACES +const V3d white_DCI(0.89458689458689, 1.0, 0.95441595441595); +const V3d white_D50(0.96429567642956771, 1.0, 0.82510460251046047); + +enum class Adaptation { Bradford, CAT02, None }; + +// Chromatic adaptation of CIE XYZ from white `from` to white `to`. +M33d +adapt(const V3d& from, const V3d& to, Adaptation method) +{ + static const M33d bradford(0.8951, 0.2664, -0.1614, -0.7502, 1.7135, 0.0367, + 0.0389, -0.0685, 1.0296); + static const M33d cat02(0.7328, 0.4296, -0.1624, -0.7036, 1.6975, 0.0061, + 0.0030, 0.0136, 0.9834); + if (method == Adaptation::None + || (std::abs(from.x - to.x) <= 1e-12 && std::abs(from.y - to.y) <= 1e-12 + && std::abs(from.z - to.z) <= 1e-12)) + return M33d(); + const M33d& cone = method == Adaptation::Bradford ? bradford : cat02; + const V3d response = apply(cone, from); + const V3d target = apply(cone, to); + return cone.inverse() + * M33d(target.x / response.x, 0.0, 0.0, 0.0, target.y / response.y, + 0.0, 0.0, 0.0, target.z / response.z) + * cone; +} + + + +// The interchange space a color space's matrices are read against: +// ACES2065-1 for a scene-referred space, CIE XYZ with a D65 white for a +// display-referred one. +struct Reference { + M33d to_XYZ; + M33d from_XYZ; + V3d white; // CIE XYZ +}; + +const Reference& +reference(bool display) +{ + static const Reference aces = [] { + const double ap0[8] = { .7347, .2653, 0.0, 1.0, + .0001, -.077, .32168, .33767 }; + Reference r; + r.to_XYZ = rgb_to_XYZ(ap0); + r.from_XYZ = r.to_XYZ.inverse(); + r.white = apply(r.to_XYZ, V3d(1.0, 1.0, 1.0)); + return r; + }(); + static const Reference xyz = [] { + Reference r; + r.white = white_D65; + return r; + }(); + return display ? xyz : aces; +} + + + +// The coordinate rounded to six decimals, or to fewer when it is within 2e-7 +// of that, so that equal primaries read through different matrices compare +// equal. +double +round_coordinate(double value) +{ + const double rounded = std::round(value * 1e6) / 1e6; + for (double scale : { 1e5, 1e4, 1e3, 1e2 }) { + const double coarser = std::round(rounded * scale) / scale; + if (std::abs(rounded - coarser) <= 2e-7) + return coarser + 0.0; // No negative zero + } + return rounded + 0.0; +} + + + +// The chromaticities (R G B W) that a linear RGB to CIE XYZ matrix gives. +std::array +chromaticities(const M33d& rgb_to_xyz) +{ + std::array xy; + const V3d basis[4] = { V3d(1.0, 0.0, 0.0), V3d(0.0, 1.0, 0.0), + V3d(0.0, 0.0, 1.0), V3d(1.0, 1.0, 1.0) }; + for (int i = 0; i < 4; ++i) { + const V3d XYZ = apply(rgb_to_xyz, basis[i]); + const double sum = XYZ.x + XYZ.y + XYZ.z; + xy[2 * i] = round_coordinate(XYZ.x / sum); + xy[2 * i + 1] = round_coordinate(XYZ.y / sum); + } + // A white with x equal to y is CIE XYZ's own, illuminant E. + if (xy[6] == xy[7]) + xy[6] = xy[7] = 1.0 / 3.0; + return xy; +} + + + +// Decimal places of a coordinate, up to nine. +int +decimal_places(double value) +{ + long long scaled = std::llround(std::abs(value) * 1e9); + if (scaled == 0) + return 0; + int places = 9; + for (; places > 0 && scaled % 10 == 0; --places) + scaled /= 10; + return places; +} + + + +// How few decimals the primaries' coordinates have: 1 when none has any, 0 +// when each has `cap` or more. +double +exactness(const std::array& xy, int cap) +{ + int total = 0; + for (int i = 0; i < 6; ++i) + total += std::min(decimal_places(xy[i]), cap); + return 1.0 - double(total) / double(cap * 6); +} + + + +// The gamut among `gamuts` that the matrix reproduces within 1e-5, reached +// under Bradford, CAT02 or no chromatic adaptation. +const float* +known_gamut(const M33d& rgb_to_reference, const Reference& ref, + cspan gamuts) +{ + const float* found = nullptr; + double closest = 1e-5; + for (const float* gamut : gamuts) { + // CIE XYZ taken as RGB has primaries on y = 0, and no such matrix. + if (gamut[1] == 0.0f || gamut[3] == 0.0f || gamut[5] == 0.0f) + continue; + double xy[8]; + std::copy(gamut, gamut + 8, xy); + const M33d to_XYZ = rgb_to_XYZ(xy); + const V3d white = apply(to_XYZ, V3d(1.0, 1.0, 1.0)); + for (Adaptation method : + { Adaptation::Bradford, Adaptation::CAT02, Adaptation::None }) { + const M33d candidate = ref.from_XYZ + * adapt(white, ref.white, method) * to_XYZ; + double residual = 0.0; + for (int r = 0; r < 3; ++r) + for (int c = 0; c < 3; ++c) + residual = std::max(residual, + std::abs(candidate[r][c] + - rgb_to_reference[r][c])); + if (residual < closest) { + closest = residual; + found = gamut; + } + } + } + return found; +} + + + +// For primaries no known gamut matches: read them under each usual white +// point and adaptation, take the first reading whose coordinates are round +// enough, and otherwise the roundest, or failing that the first (D65, +// Bradford). +std::array +roundest_reading(const M33d& rgb_to_reference, const Reference& ref) +{ + const M33d rgb_to_XYZ = ref.to_XYZ * rgb_to_reference; + std::array best; + double best_rank = 0.0; + bool first = true; + for (const V3d& white : { white_D65, white_D60, white_DCI, white_D50 }) { + for (Adaptation method : { Adaptation::Bradford, Adaptation::CAT02 }) { + const std::array xy = chromaticities( + adapt(ref.white, white, method) * rgb_to_XYZ); + if (first) { + best = xy; + first = false; + } + if (exactness(xy, 6) > 0.6) + return xy; + const double rank = exactness(xy, 9); + if (rank > best_rank && rank > 0.2) { + best_rank = rank; + best = xy; + } + } + } + return best; +} + + + +// The matrices that lead the conversion from the interchange space `role` to +// the color space `name`, multiplied into one: clamps are skipped, so are +// matrices that only scale, and the first curve ends them. +bool +leading_matrices(const OCIO::Config& config, const char* role, const char* name, + M33d& matrices) +{ + OCIO::ConstGroupTransformRcPtr group; + try { + // Lossless optimization folds and cancels matrices without composing + // LUTs, which only the curves after the matrices would use; over + // OCIO's built-in configs it reads the same matrices as any heavier + // optimization, much faster. + group = config.getProcessor(role, name) + ->getOptimizedProcessor(OCIO::OptimizationFlags( + OCIO::OPTIMIZATION_LOSSLESS + & ~OCIO::OPTIMIZATION_SIMPLIFY_OPS)) + ->createGroupTransform(); + } catch (const std::exception&) { + return false; + } + matrices.makeIdentity(); + for (int i = 0, n = group->getNumTransforms(); i < n; ++i) { + const OCIO::ConstTransformRcPtr transform = group->getTransform(i); + if (transform->getTransformType() == OCIO::TRANSFORM_TYPE_RANGE) + continue; + auto matrix = OCIO::DynamicPtrCast( + transform); + if (!matrix) + break; + double m[16]; + matrix->getMatrix(m); + M33d step(m[0], m[1], m[2], m[4], m[5], m[6], m[8], m[9], m[10]); + bool mixes = false; + for (int r = 0; r < 3; ++r) + for (int c = 0; c < 3; ++c) + mixes |= r != c && std::abs(step[r][c]) > 0.1; + if (!mixes) + continue; + if (matrix->getDirection() == OCIO::TRANSFORM_DIR_INVERSE) + step.invert(); + matrices = step * matrices; + } + return true; +} + + + +// Whether a transform, other than a group, file or color space reference, is +// one whose matrices and curves describe a color encoding: not a 3D LUT, +// grade, look, view or rendering. +bool +is_simple_transform(const OCIO::ConstTransformRcPtr& transform) +{ + switch (transform->getTransformType()) { + case OCIO::TRANSFORM_TYPE_BUILTIN: { + auto builtin = OCIO::DynamicPtrCast( + transform); + const std::string style(builtin ? builtin->getStyle() : ""); + return style.rfind("ACES-OUTPUT", 0) != 0 + && style.rfind("ACES-LMT", 0) != 0; + } + case OCIO::TRANSFORM_TYPE_FIXED_FUNCTION: { +#if OCIO_VERSION_HEX > 0x02040000 + auto ff = OCIO::DynamicPtrCast( + transform); + return ff + && (ff->getStyle() == OCIO::FIXED_FUNCTION_LIN_TO_DOUBLE_LOG + || ff->getStyle() == OCIO::FIXED_FUNCTION_LIN_TO_GAMMA_LOG); +#else + return false; +#endif + } + case OCIO::TRANSFORM_TYPE_MATRIX: + case OCIO::TRANSFORM_TYPE_RANGE: + case OCIO::TRANSFORM_TYPE_EXPONENT: + case OCIO::TRANSFORM_TYPE_EXPONENT_WITH_LINEAR: + case OCIO::TRANSFORM_TYPE_LOG: + case OCIO::TRANSFORM_TYPE_LOG_AFFINE: + case OCIO::TRANSFORM_TYPE_LOG_CAMERA: + case OCIO::TRANSFORM_TYPE_ALLOCATION: + case OCIO::TRANSFORM_TYPE_LUT1D: + case OCIO::TRANSFORM_TYPE_GRADING_RGB_CURVE: return true; + default: return false; + } +} + + + +bool +has_context_variable(const char* s) +{ + return s && (std::strchr(s, '$') || std::strchr(s, '%')); +} + + + +// Whether a color space's transforms (from the reference if it has them, +// otherwise to it) include anything but simple ones, following color space +// references. Data and is-unique spaces count as complex, and so does a +// reference cycle. +bool +is_complex(const OCIO::Config& config, const OCIO::ConstColorSpaceRcPtr& cs, + std::unordered_set& visiting) +{ + if (!cs || cs->isData() || cs->hasCategory("is-unique") + || has_context_variable(cs->getName())) + return true; + if (!visiting.insert(cs->getName()).second) + return true; + std::function complex_transform; + complex_transform = [&](const OCIO::ConstTransformRcPtr& transform) { + if (!transform) + return false; + switch (transform->getTransformType()) { + // Files are taken as simple, whatever they hold. + case OCIO::TRANSFORM_TYPE_FILE: return false; + case OCIO::TRANSFORM_TYPE_GROUP: { + auto group = OCIO::DynamicPtrCast( + transform); + for (int i = 0, n = group ? group->getNumTransforms() : 0; i < n; + ++i) + if (complex_transform(group->getTransform(i))) + return true; + return !group; + } + case OCIO::TRANSFORM_TYPE_COLORSPACE: { + auto cst = OCIO::DynamicPtrCast( + transform); + if (!cst) + return true; + if (has_context_variable(cst->getSrc()) + || has_context_variable(cst->getDst())) + return false; + return is_complex(config, config.getColorSpace(cst->getSrc()), + visiting) + || is_complex(config, config.getColorSpace(cst->getDst()), + visiting); + } + default: return !is_simple_transform(transform); + } + }; + auto from = cs->getTransform(OCIO::COLORSPACE_DIR_FROM_REFERENCE); + const bool complex = complex_transform( + from ? from : cs->getTransform(OCIO::COLORSPACE_DIR_TO_REFERENCE)); + visiting.erase(cs->getName()); + return complex; +} + + + +// The color space a transform chain ends in, when its last step is a +// ColorSpaceTransform, following groups and their inversion. +std::string +trailing_color_space(const OCIO::ConstTransformRcPtr& transform, + OCIO::TransformDirection direction) +{ + if (!transform) + return {}; + const bool inverse = (direction == OCIO::TRANSFORM_DIR_INVERSE) + != (transform->getDirection() + == OCIO::TRANSFORM_DIR_INVERSE); + if (auto cst = OCIO::DynamicPtrCast( + transform)) { + const char* end = inverse ? cst->getSrc() : cst->getDst(); + return end ? end : ""; + } + auto group = OCIO::DynamicPtrCast(transform); + if (!group || group->getNumTransforms() == 0) + return {}; + return trailing_color_space( + group->getTransform(inverse ? 0 : group->getNumTransforms() - 1), + inverse ? OCIO::TRANSFORM_DIR_INVERSE : OCIO::TRANSFORM_DIR_FORWARD); +} + + + +bool +derive(const OCIO::Config& config, const OCIO::ColorSpace& cs, + cspan gamuts, std::array& xy, int depth) +{ + if (cs.isData()) + return false; + const bool display = cs.getReferenceSpaceType() + == OCIO::REFERENCE_SPACE_DISPLAY; + const char* role = display ? OCIO::ROLE_INTERCHANGE_DISPLAY + : OCIO::ROLE_INTERCHANGE_SCENE; + if (!config.hasRole(role)) + return false; + + // A space whose transforms are not simple but that ends in another + // color space answers as that one. + std::unordered_set visiting; + if (depth < 8 + && is_complex(config, config.getColorSpace(cs.getName()), visiting)) { + const std::string target = trailing_color_space( + cs.getTransform(OCIO::COLORSPACE_DIR_FROM_REFERENCE), + OCIO::TRANSFORM_DIR_FORWARD); + if (!target.empty() && target != cs.getName()) + if (auto other = config.getColorSpace(target.c_str())) + if (derive(config, *other, gamuts, xy, depth + 1)) + return true; + } + + M33d reference_to_rgb; + if (!leading_matrices(config, role, cs.getName(), reference_to_rgb) + || std::abs(reference_to_rgb.determinant()) < 1e-12) + return false; + const M33d rgb_to_reference = reference_to_rgb.inverse(); + const Reference& ref = reference(display); + if (const float* gamut = known_gamut(rgb_to_reference, ref, gamuts)) { + std::copy(gamut, gamut + 8, xy.begin()); + return true; + } + const std::array read = roundest_reading(rgb_to_reference, ref); + for (int i = 0; i < 8; ++i) + xy[i] = float(read[i]); + return true; +} + +} // namespace + + + +bool +pvt::derive_chromaticities(const OCIO::Config& config, + const OCIO::ColorSpace& cs, + cspan gamuts, std::array& xy) +{ + return derive(config, cs, gamuts, xy, 0); +} + +OIIO_NAMESPACE_END diff --git a/src/libOpenImageIO/color_derive.h b/src/libOpenImageIO/color_derive.h new file mode 100644 index 0000000000..28f83a78d2 --- /dev/null +++ b/src/libOpenImageIO/color_derive.h @@ -0,0 +1,32 @@ +// Copyright Contributors to the OpenImageIO project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/AcademySoftwareFoundation/OpenImageIO + +#pragma once + +#include + +#include + +#include + + +OIIO_NAMESPACE_BEGIN +namespace pvt { + +/// The RGB primaries and white point (Rx, Ry, Gx, Gy, Bx, By, Wx, Wy, CIE +/// 1931 xy) that the transforms of `cs` establish. They come from the +/// matrices that lead the conversion from `config`'s interchange space for +/// the reference type of `cs` (ACES2065-1 or CIE XYZ-D65): named as one of +/// `gamuts` when the matrices match it under Bradford, CAT02 or no chromatic +/// adaptation, and otherwise under the white point and adaptation that give +/// the most exact coordinates. A space whose transforms are not simple but +/// end in a ColorSpaceTransform answers as that transform's color space. +/// Returns false, leaving `xy` alone, if nothing is established. +bool derive_chromaticities(const OCIO_NAMESPACE::Config& config, + const OCIO_NAMESPACE::ColorSpace& cs, + cspan gamuts, + std::array& xy); + +} // namespace pvt +OIIO_NAMESPACE_END diff --git a/src/libOpenImageIO/color_ocio.cpp b/src/libOpenImageIO/color_ocio.cpp index 7ce81d487d..158b74133e 100644 --- a/src/libOpenImageIO/color_ocio.cpp +++ b/src/libOpenImageIO/color_ocio.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -21,6 +22,7 @@ #include #include +#include "color_derive.h" #include "imageio_pvt.h" #include "interop_config.h" @@ -218,13 +220,20 @@ class ColorConfig::Impl { OCIO::ConfigRcPtr config_; OCIO::ConfigRcPtr builtinconfig_; - // A color space's chromaticities (in the ColorInteropID table, or null) - // and transfer function gamma. + // A color space's chromaticities (in the ColorInteropID table, or derived + // and held below, or null) and transfer function gamma. struct Properties { const float* xy = nullptr; float gamma = 0.0f; }; + // What a color space's transforms establish about it. + struct Derived { + std::array xy; + bool has_xy = false; + bool linear = false; + }; + private: std::vector colorspaces; std::string scene_linear_alias; // Alias for a scene-linear color space @@ -240,6 +249,8 @@ class ColorConfig::Impl { ColorProcessorMap colorprocmap; // cache of ColorProcessors atomic_int colorprocs_requested; atomic_int colorprocs_created; + // Node-based, so the chromaticities stay where they are as it grows. + mutable std::map> m_derived; std::string m_configname; ColorConfig* m_self = nullptr; bool m_config_is_built_in = false; @@ -347,6 +358,10 @@ class ColorConfig::Impl { // report. Properties properties(string_view colorspace) const; + // What the transforms of a color space of this config establish about + // it, worked out once per color space. + const Derived& derived(const OCIO::ColorSpace& cs) const; + int getNumColorSpaces() const { return (int)colorspaces.size(); } const char* getColorSpaceNameByIndex(int index) const @@ -2659,6 +2674,45 @@ constexpr float xy_ap1[] = { .713f, .293f, .165f, .83f, // CIE XYZ taken as RGB, so R = G = B is illuminant E. constexpr float xy_xyz[] = { 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f / 3, 1.0f / 3 }; +// Camera gamuts, as their makers publish them. +constexpr float xy_awg3[] = { .684f, .313f, .221f, .848f, + .0861f, -.102f, .3127f, .329f }; +constexpr float xy_awg4[] = { .7347f, .2653f, .1424f, .8576f, + .0991f, -.0308f, .3127f, .329f }; +constexpr float xy_bmdwg5[] = { .7177215f, .3171181f, .228041f, .861569f, + .1005841f, -.0820452f, .312717f, .3290312f }; +constexpr float xy_cgamut[] = { .74f, .27f, .17f, 1.14f, + .08f, -.1f, .3127f, .329f }; +constexpr float xy_dgamut[] = { .71f, .31f, .21f, .88f, + .09f, -.08f, .3127f, .329f }; +constexpr float xy_dwg[] = { .8f, .313f, .1682f, .9877f, + .079f, -.1155f, .3127f, .329f }; +constexpr float xy_rwg[] = { .780308f, .304253f, .121595f, 1.493994f, + .095612f, -.084589f, .3127f, .329f }; +constexpr float xy_sgamut3[] = { .73f, .28f, .14f, .855f, + .1f, -.05f, .3127f, .329f }; +constexpr float xy_sgamut3cine[] = { .766f, .275f, .225f, .8f, + .089f, -.087f, .3127f, .329f }; +constexpr float xy_sgamut3venice[] = { .74046426f, .27936437f, .08924115f, + .89380953f, .11048824f, -.05257933f, + .3127f, .329f }; +constexpr float xy_sgamut3cinevenice[] = { .77590187f, .27450239f, .1886829f, + .82868494f, .10133738f, -.08918752f, + .3127f, .329f }; +constexpr float xy_vgamut[] = { .73f, .28f, .165f, .84f, + .1f, -.03f, .3127f, .329f }; +constexpr float xy_applewg[] = { .725f, .301f, .221f, .814f, + .068f, -.076f, .3127f, .329f }; +// Published gamuts that no Color Interop ID OpenImageIO knows uses yet, which +// derivation still recognizes. +constexpr float xy_p3d60[] = { .68f, .32f, .265f, .69f, + .15f, .06f, .32168f, .33767f }; +constexpr float xy_p3dci[] + = { .68f, .32f, .265f, .69f, .15f, .06f, .314f, .351f }; +constexpr float xy_rec601[] = { .63f, .34f, .31f, .595f, + .155f, .07f, .3127f, .329f }; +constexpr float xy_rec601pal[] = { .64f, .33f, .29f, .60f, + .15f, .06f, .3127f, .329f }; // A color interop ID with what OIIO knows about it: a legacy alias for older // configs, its CICP code if it has one, and its chromaticities (Rx, Ry, Gx, Gy, @@ -2778,7 +2832,58 @@ constexpr ColorInteropID color_interop_ids[] = { // priority when converting a CICP to an interop ID. { "ocio:lin_ciexyzd65_display", "cie_xyz_d65", CICPPrimaries::XYZD65, CICPTransfer::Linear, CICPMatrix::Unspecified }, + { "ocio:acescc_ap1_scene", nullptr, xy_ap1, 0.0f }, + { "ocio:acescct_ap1_scene", nullptr, xy_ap1, 0.0f }, + { "ocio:itu709_rec709_scene", nullptr, xy_rec709, 0.0f }, + { "ocio:arrilogc3_awg3_scene", nullptr, xy_awg3, 0.0f }, + { "ocio:lin_awg3_scene", nullptr, xy_awg3, 1.0f }, + { "ocio:arrilogc4_awg4_scene", nullptr, xy_awg4, 0.0f }, + { "ocio:lin_awg4_scene", nullptr, xy_awg4, 1.0f }, + { "ocio:bmdfilm5_wg5_scene", nullptr, xy_bmdwg5, 0.0f }, + { "ocio:lin_bmdwg5_scene", nullptr, xy_bmdwg5, 1.0f }, + { "ocio:davinci_dwg_scene", nullptr, xy_dwg, 0.0f }, + { "ocio:lin_dwg_scene", nullptr, xy_dwg, 1.0f }, + { "ocio:canonlog2_cgamutd55_scene", nullptr, xy_cgamut, 0.0f }, + { "ocio:canonlog3_cgamutd55_scene", nullptr, xy_cgamut, 0.0f }, + { "ocio:lin_cgamutd55_scene", nullptr, xy_cgamut, 1.0f }, + { "ocio:djilog_dgamut_scene", nullptr, xy_dgamut, 0.0f }, + { "ocio:lin_dgamut_scene", nullptr, xy_dgamut, 1.0f }, + { "ocio:vlog_vgamut_scene", nullptr, xy_vgamut, 0.0f }, + { "ocio:lin_vgamut_scene", nullptr, xy_vgamut, 1.0f }, + { "ocio:redlog3g10_rwg_scene", nullptr, xy_rwg, 0.0f }, + { "ocio:lin_rwg_scene", nullptr, xy_rwg, 1.0f }, + { "ocio:slog3_sgamut3_scene", nullptr, xy_sgamut3, 0.0f }, + { "ocio:lin_sgamut3_scene", nullptr, xy_sgamut3, 1.0f }, + { "ocio:slog3_sgamut3cine_scene", nullptr, xy_sgamut3cine, 0.0f }, + { "ocio:lin_sgamut3cine_scene", nullptr, xy_sgamut3cine, 1.0f }, + { "ocio:slog3_sgamut3venice_scene", nullptr, xy_sgamut3venice, 0.0f }, + { "ocio:lin_sgamut3venice_scene", nullptr, xy_sgamut3venice, 1.0f }, + { "ocio:slog3_sgamut3cinevenice_scene", nullptr, xy_sgamut3cinevenice, + 0.0f }, + { "ocio:lin_sgamut3cinevenice_scene", nullptr, xy_sgamut3cinevenice, 1.0f }, + { "ocio:lin_applewg_scene", nullptr, xy_applewg, 1.0f }, }; + +// The gamuts derivation names a color space's primaries as: the table's, then +// those no ID uses yet. +const std::vector& +known_gamuts() +{ + static const std::vector gamuts = [] { + std::vector unique; + for (const ColorInteropID& interop : color_interop_ids) + if (interop.chromaticities + && std::find(unique.begin(), unique.end(), + interop.chromaticities) + == unique.end()) + unique.push_back(interop.chromaticities); + for (const float* extra : + { xy_p3d60, xy_p3dci, xy_rec601, xy_rec601pal }) + unique.push_back(extra); + return unique; + }(); + return gamuts; +} } // namespace const std::vector>& @@ -2864,6 +2969,67 @@ ColorConfig::get_cicp(string_view colorspace) const +const ColorConfig::Impl::Derived& +ColorConfig::Impl::derived(const OCIO::ColorSpace& cs) const +{ + const string_view name(cs.getName()); + { + spin_rw_read_lock lock(m_mutex); + auto found = m_derived.find(name); + if (found != m_derived.end()) + return found->second; + } + // Worked out without the lock; the first answer stored is kept. + Derived result; + result.has_xy = OIIO::pvt::derive_chromaticities(*config_, cs, + known_gamuts(), result.xy); + try { + result.linear = config_->isColorSpaceLinear(cs.getName(), + cs.getReferenceSpaceType()); + } catch (const std::exception& e) { + DBG("OCIO exception checking linearity of {}: {}\n", name, e.what()); + } + spin_rw_write_lock lock(m_mutex); + return m_derived.emplace(name, result).first->second; +} + + + +// The chromaticities that OpenImageIO's built-in definition of a Color +// Interop ID establishes, worked out once per process, or null if it has +// none or doesn't define the ID. +static const float* +builtin_id_chromaticities(string_view interop_id) +{ + // Node-based, so the chromaticities stay where they are as it grows. + using Found = std::pair>; + static spin_rw_mutex mutex; + static std::map> found; + { + spin_rw_read_lock lock(mutex); + auto f = found.find(interop_id); + if (f != found.end()) + return f->second.first ? f->second.second.data() : nullptr; + } + // Worked out without the lock; the first answer stored is kept. + Found result { false, {} }; + try { + auto identities = interop_identities_config(); + auto cs = identities->getColorSpace(std::string(interop_id).c_str()); + result.first = cs && Strutil::iequals(interop_id, cs->getName()) + && OIIO::pvt::derive_chromaticities(*identities, *cs, + known_gamuts(), + result.second); + } catch (const std::exception& e) { + DBG("OCIO exception deriving {}: {}\n", interop_id, e.what()); + } + spin_rw_write_lock lock(mutex); + const Found& stored = found.emplace(interop_id, result).first->second; + return stored.first ? stored.second.data() : nullptr; +} + + + ColorConfig::Impl::Properties ColorConfig::Impl::properties(string_view colorspace) const { @@ -2880,20 +3046,46 @@ ColorConfig::Impl::properties(string_view colorspace) const } if (cs && cs->isData()) return {}; - const string_view encoding(cs ? cs->getEncoding() : ""); - const bool linear = encoding == "scene-linear" - || encoding == "display-linear"; - Properties result; + const bool analyze = cs && OIIO::pvt::colorconfig_analyze_transforms; + const ColorInteropID* row = nullptr; for (const ColorInteropID& interop : color_interop_ids) { - if (interop.chromaticities && interop_id == interop.interop_id) { - // An ID contradicting a declared linear encoding is ignored. - if (!(linear && interop.gamma != 1.0f)) - result = { interop.chromaticities, interop.gamma }; + if (interop_id == interop.interop_id) { + row = &interop; break; } } - if (linear) + + // The primaries are the ID's: the table's (a row without them says the + // ID has none), or those of OpenImageIO's own definition of an ID the + // table lacks. A space with neither has them derived from its transforms. + const Derived* own = analyze && !row ? &derived(*cs) : nullptr; + Properties result; + if (row) { + result.xy = row->chromaticities; + } else if (own) { + if (!interop_id.empty() && !disable_builtin_configs) + result.xy = builtin_id_chromaticities(interop_id); + if (!result.xy && own->has_xy) + result.xy = own->xy.data(); + } + + // The transfer function is the space's own, never its ID's. A declared + // encoding decides, linear only when it names the space's own reference + // type; without one, a space no row describes is linear if its + // transforms are. Until transfer functions are matched against known + // curves, a row still gives its gamma. + result.gamma = row ? row->gamma : 0.0f; + const std::string encoding = Strutil::lower(cs ? cs->getEncoding() : ""); + if (!encoding.empty()) { + const bool display = cs->getReferenceSpaceType() + == OCIO::REFERENCE_SPACE_DISPLAY; + if (encoding == (display ? "display-linear" : "scene-linear")) + result.gamma = 1.0f; + else if (result.gamma == 1.0f) + result.gamma = 0.0f; + } else if (own && own->linear) { result.gamma = 1.0f; + } return result; } diff --git a/src/libOpenImageIO/color_test.cpp b/src/libOpenImageIO/color_test.cpp index 06d448dd91..b260f499b5 100644 --- a/src/libOpenImageIO/color_test.cpp +++ b/src/libOpenImageIO/color_test.cpp @@ -430,10 +430,15 @@ test_color_space_properties() OIIO_CHECK_EQUAL(config.get_chromaticities("sRGB").size(), 8); OIIO_CHECK_EQUAL(config.get_transfer_function_gamma("Adobe"), 563.0f / 256.0f); - for (auto name : { "Plain", "Mislabeled" }) { + for (auto name : { "Plain", "Mislabeled" }) OIIO_CHECK_EQUAL(config.get_transfer_function_gamma(name), 1.0f); - OIIO_CHECK_ASSERT(config.get_chromaticities(name).empty()); - } + OIIO_CHECK_ASSERT(config.get_chromaticities("Plain").empty()); + // The ID gives the primaries even when the encoding contradicts its + // transfer function, which the encoding decides. + const float rec709[] = { .64f, .33f, .30f, .60f, + .15f, .06f, .3127f, .329f }; + OIIO_CHECK_ASSERT(config.get_chromaticities("Mislabeled") + == cspan(rec709)); for (auto name : { "Data", "data", "missing" }) { OIIO_CHECK_EQUAL(config.get_transfer_function_gamma(name), 0.0f); OIIO_CHECK_ASSERT(config.get_chromaticities(name).empty()); @@ -482,6 +487,215 @@ test_color_space_properties() +static void +test_derived_color_space_properties() +{ + if (!ColorConfig::supportsOpenColorIO()) + return; + + // Color spaces with no Color Interop ID, written with OCIO's built-in + // transforms, report what their transforms establish. + const std::string filename = Filesystem::temp_directory_path() + "/" + + Filesystem::unique_path() + ".ocio"; + const std::string cube = Filesystem::temp_directory_path() + "/" + + Filesystem::unique_path() + ".cube"; + OIIO_CHECK_ASSERT(Filesystem::write_text_file( + cube, "LUT_3D_SIZE 2\n0.1 0.1 0.1\n1 0 0\n0 1 0\n1 1 0\n" + "0 0 1\n1 0 1\n0 1 1\n1 1 1\n")); + auto scene = [](const char* name, const char* transform) { + return Strutil::fmt::format( + " - !\n name: {}\n to_scene_reference: {}\n", + name, transform); + }; + auto display = [](const char* name, const char* builtin, + const char* encoding = nullptr) { + return Strutil::fmt::format( + " - !\n name: {}\n{} from_display_reference: " + "! {{style: DISPLAY - CIE-XYZ-D65_to_{}}}\n", + name, + encoding ? Strutil::fmt::format(" encoding: {}\n", encoding) + : std::string(), + builtin); + }; + const char* acescg = "! {style: ACEScg_to_ACES2065-1}"; + const std::string from_xyz + = "! {style: UTILITY - ACES-AP0_to_CIE-XYZ-D65_BFD, " + "direction: inverse}"; + // RGB to CIE XYZ of P3-D65, and of the primaries (0.7, 0.3), (0.2, 0.75) + // and (0.12, 0.05) with a D65 white. + const std::string p3_matrix + = "! {matrix: [0.4865709486482162, " + "0.2656676931690931, 0.1982172852343625, 0, 0.2289745640697488, " + "0.6917385218365064, 0.0792869140937450, 0, 0, 0.0451133818589026, " + "1.0439443689009760, 0, 0, 0, 0, 1]}"; + const std::string custom_matrix + = "! {matrix: [0.6204938816007054, " + "0.1789769595413957, 0.1509850859095705, 0, 0.2659259492574452, " + "0.6711635982802339, 0.06291045246232105, 0, 0, " + "0.04474423988534896, 1.044313510874529, 0, 0, 0, 0, 1]}"; + OIIO_CHECK_ASSERT(Filesystem::write_text_file( + filename, + "ocio_profile_version: 2.3\n" + "roles: {aces_interchange: ACES2065-1, " + "cie_xyz_d65_interchange: CIE-XYZ-D65, default: ACES2065-1}\n" + "file_rules:\n - ! {name: Default, colorspace: default}\n" + "colorspaces:\n" + " - ! {name: ACES2065-1}\n" + + scene("ACEScg", acescg) + + scene("Power AP1", + "! {children: [! " + "{value: 2.2}, ! {style: " + "ACEScg_to_ACES2065-1}]}") + + scene("Log AP1", + "! {style: ACEScct_to_ACES2065-1}") + // ACEScc's decoder clamps negatives after its matrix. + + scene("Clamped Log AP1", + "! {style: ACEScc_to_ACES2065-1}") + // P3-D65 reaching ACES2065-1 through Bradford adaptation. + + scene("Plain P3", ("! {children: [" + p3_matrix + + ", " + from_xyz + "]}") + .c_str()) + // A camera gamut, named as its maker publishes it. + + scene("RED Log", + "! {style: RED_LOG3G10-RWG_to_ACES2065-1}") + // Primaries no published gamut has. + + scene("Custom Gamut", ("! {children: [" + + custom_matrix + ", " + from_xyz + "]}") + .c_str()) + // A grade ahead of ACEScg answers as ACEScg. + + " - !\n name: Graded AP1\n" + " from_scene_reference: ! {children: [" + "! {slope: [1.1, 1, 0.9]}, " + "! {src: ACES2065-1, dst: ACEScg}]}\n" + // A curve applied after the matrix leaves only ACES2065-1's own. + + scene("Curve Last", + "! {children: [! " + "{style: ACEScg_to_ACES2065-1}, ! " + "{value: 2.2}]}") + // A declared linear encoding is honored over the transforms. + + " - !\n name: Said Linear\n" + " encoding: scene-linear\n" + " to_scene_reference: ! {children: [" + "! {value: 2.2}, ! " + "{style: ACEScg_to_ACES2065-1}]}\n" + + scene("Cube", + ("! {children: [! " + "{src: \"" + + cube + "\", interpolation: linear}, " + acescg + "]}") + .c_str()) + + "display_colorspaces:\n" + " - ! {name: CIE-XYZ-D65}\n" + + display("Monitor 1886", "REC.1886-REC.709") + + display("Monitor P3", "DisplayP3") + + display("Cinema P3-D60", "G2.6-P3-D60-BFD") + + display("Cinema P3-DCI", "G2.6-P3-DCI-BFD") + // A scene encoding on a display space is not linear for it. + + display("Mistagged 1886", "REC.1886-REC.709", "scene-linear"))); + + const float ap0[] = { .7347f, .2653f, 0.0f, 1.0f, + .0001f, -.077f, .32168f, .33767f }; + const float ap1[] = { .713f, .293f, .165f, .83f, + .128f, .044f, .32168f, .33767f }; + const float rec709[] = { .64f, .33f, .30f, .60f, .15f, .06f, .3127f, .329f }; + const float p3d65[] = { .68f, .32f, .265f, .69f, .15f, .06f, .3127f, .329f }; + const float p3d60[] = { .68f, .32f, .265f, .69f, + .15f, .06f, .32168f, .33767f }; + const float p3dci[] = { .68f, .32f, .265f, .69f, .15f, .06f, .314f, .351f }; + const float rwg[] = { .780308f, .304253f, .121595f, 1.493994f, + .095612f, -.084589f, .3127f, .329f }; + const float custom[] = { .7f, .3f, .2f, .75f, .12f, .05f, .3127f, .329f }; + // CIE XYZ taken as RGB, so R = G = B is illuminant E. + const float xyz[] + = { 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f / 3, 1.0f / 3 }; + { + ColorConfig config(filename); + OIIO_CHECK_FALSE(config.has_error()); + struct Expected { + const char* name; + const float* xy; + float gamma; + }; + for (const Expected& e : { + Expected { "ACES2065-1", ap0, 1.0f }, + Expected { "ACEScg", ap1, 1.0f }, + Expected { "Power AP1", ap1, 0.0f }, + Expected { "Log AP1", ap1, 0.0f }, + Expected { "Clamped Log AP1", ap1, 0.0f }, + Expected { "Plain P3", p3d65, 1.0f }, + Expected { "RED Log", rwg, 0.0f }, + Expected { "Custom Gamut", custom, 1.0f }, + Expected { "Graded AP1", ap1, 1.0f }, + Expected { "Curve Last", ap0, 0.0f }, + Expected { "Said Linear", ap1, 1.0f }, + Expected { "Cube", ap1, 0.0f }, + Expected { "CIE-XYZ-D65", xyz, 1.0f }, + Expected { "Monitor 1886", rec709, 0.0f }, + Expected { "Monitor P3", p3d65, 0.0f }, + Expected { "Cinema P3-D60", p3d60, 0.0f }, + Expected { "Cinema P3-DCI", p3dci, 0.0f }, + Expected { "Mistagged 1886", rec709, 0.0f }, + }) { + // Compared as text, so a failure names the color space. + auto describe = [&](cspan xy, float gamma) { + return Strutil::fmt::format("{}: [{}] {}", e.name, + Strutil::join(xy, " "), gamma); + }; + OIIO_CHECK_EQUAL( + describe(config.get_chromaticities(e.name), + config.get_transfer_function_gamma(e.name)), + describe(e.xy ? cspan(e.xy, 8) : cspan(), + e.gamma)); + } + + // With analysis off, only declarations and known names answer, even + // for a space already analyzed. + OIIO::attribute("colorconfig:analyze_transforms", 0); + OIIO_CHECK_ASSERT(config.get_chromaticities("Plain P3").empty()); + OIIO_CHECK_EQUAL(config.get_transfer_function_gamma("Plain P3"), 0.0f); + OIIO::attribute("colorconfig:analyze_transforms", 1); + } + Filesystem::remove(filename); + Filesystem::remove(cube); + + // OpenColorIO 2.5 reads a declared interop_id. The ID gives the + // primaries, from the table or from OpenImageIO's own definition of it, + // whatever the transforms do; an ID OpenImageIO doesn't know leaves them + // to the transforms. + if (ColorConfig::OpenColorIO_version_hex() >= 0x02050000) { + OIIO_CHECK_ASSERT(Filesystem::write_text_file( + filename, + "ocio_profile_version: 2.5\n" + "roles: {aces_interchange: ACES2065-1, " + "cie_xyz_d65_interchange: CIE-XYZ-D65, default: ACES2065-1}\n" + "file_rules:\n - ! {name: Default, colorspace: default}\n" + "colorspaces:\n" + " - ! {name: ACES2065-1}\n" + " - ! {name: Camera, interop_id: ocio:lin_awg3_scene}\n" + " - ! {name: Film, interop_id: ocio:adx10_apd_scene}\n" + " - !\n name: Studio Wide\n" + " interop_id: acme:lin_wide_scene\n" + " to_scene_reference: ! {children: [" + + p3_matrix + ", " + from_xyz + + "]}\n" + "display_colorspaces:\n" + " - ! {name: CIE-XYZ-D65}\n")); + const float awg3[] = { .684f, .313f, .221f, .848f, + .0861f, -.102f, .3127f, .329f }; + ColorConfig config(filename); + OIIO_CHECK_FALSE(config.has_error()); + OIIO_CHECK_ASSERT(config.get_chromaticities("Camera") + == cspan(awg3)); + OIIO_CHECK_EQUAL(config.get_chromaticities("Film").size(), 8); + OIIO_CHECK_ASSERT(config.get_chromaticities("Studio Wide") + == cspan(p3d65)); + OIIO_CHECK_EQUAL(config.get_transfer_function_gamma("Studio Wide"), + 1.0f); + Filesystem::remove(filename); + } +} + + + int main(int argc, char* argv[]) { @@ -504,6 +718,7 @@ main(int argc, char* argv[]) test_interop_identities_conversion(); test_gamma_pair_conversion(); test_color_space_properties(); + test_derived_color_space_properties(); return unit_test_failures != 0; } diff --git a/src/libOpenImageIO/imageio.cpp b/src/libOpenImageIO/imageio.cpp index 8fc67d3057..dfb4e1df19 100644 --- a/src/libOpenImageIO/imageio.cpp +++ b/src/libOpenImageIO/imageio.cpp @@ -67,6 +67,7 @@ int limit_imagesize_MB(std::min(32 * 1024, int(Sysutil::physical_memory() >> 20))); int limit_resolution(1 << 20); // max pixels along any single dimension int imageinput_strict(0); +int colorconfig_analyze_transforms(1); ustring font_searchpath(Sysutil::getenv("OPENIMAGEIO_FONTS")); ustring plugin_searchpath(OIIO_DEFAULT_PLUGIN_SEARCHPATH); std::string format_list; // comma-separated list of all formats @@ -454,6 +455,10 @@ attribute(string_view name, TypeDesc type, const void* val) imageinput_strict = *(const int*)val; return true; } + if (name == "colorconfig:analyze_transforms" && type == TypeInt) { + colorconfig_analyze_transforms = *(const int*)val; + return true; + } if (name == "use_tbb" && type == TypeInt) { oiio_use_tbb = *(const int*)val; return true; @@ -664,6 +669,10 @@ getattribute(string_view name, TypeDesc type, void* val) *(int*)val = imageinput_strict; return true; } + if (name == "colorconfig:analyze_transforms" && type == TypeInt) { + *(int*)val = colorconfig_analyze_transforms; + return true; + } if (name == "use_tbb" && type == TypeInt) { *(int*)val = oiio_use_tbb; return true;