Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/releases/ocio_2_6.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,33 @@ available for config authors to set.) This enables applications to provide tool-
similar help text for both displays and views.


Color Interop ID Support
************************

For Developers
++++++++++++++

New functions have been added to assist developers in implementing support for the ASWF
Color Interop Forum's `Color Interop ID. <https://github.com/AcademySoftwareFoundation/ColorInterop/blob/main/Recommendations/03_ColorInteropID/ColorInteropID.md>`_
These allow applications to either find a color space for an ID or, conversely, find an ID
for a color space (even if its ``interop_id`` is missing).

* The function ``Config::findColorSpaceForID`` searches a config for the color space that
should be used for a given interop ID. This implements the fallback rules defined in the
ASWF CIF Recommendation.

* The function ``Config::LocateBuiltinColorSpace`` searches for a color space in a built-in
config that is equivalent to a source color space. Since all color spaces in the recent
built-in configs have interop IDs populated, it allows you to determine the interop ID
for most color spaces, even if the interop ID is not populated in your source config.
This function leverages the color space "finger-printing" technique developed for the
config merging feature in OCIO 2.5.

* The function ``Config::generateLocalIDForColorSpace`` allows applications to generate
an interop ID, even if the config does not contain one for that color space and one
cannot be found using LocateBuiltinColorSpace.


New Fixed Function Transforms
*****************************

Expand Down
89 changes: 89 additions & 0 deletions include/OpenColorIO/OpenColorIO.h
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,43 @@ class OCIOEXPORT Config
const ConstConfigRcPtr & builtinConfig,
const char * builtinColorSpaceName);

/**
* \brief Find the name of the color space in the given built-in config that is equivalent to
* the color space in the source config. This is useful as a way of finding a color
* interop ID for a color space that doesn't have one, since the recent built-in
* configs have the ID populated for all color spaces. Note that this method relies
* on heuristics which may evolve over time and which may not work on all configs.
*
* Both active and inactive color spaces are searched in the built-in config (note
* that this differs from IdentifyBuiltinColorSpace, which only searches the active
* color spaces of the source config).
*
* A built-in color space is evaluated by processing test colors and comparing to
* the source color space. The interchange roles are used to make this comparison
* independent of the reference spaces in the configs. If the source config does not
* have the interchange roles, heuristics will be used but these may be less accurate
* or may not identify the reference space, particularly for display-referred spaces.
*
* Note that the set of test results (the "fingerprints") for the built-in config
* are expensive to calculate, since it requires building a Processor for each of
* its color spaces. They are calculated on the first call and then cached on the
* builtinConfig until the config is modified. For that reason, an application
* that calls this method more than once should hold on to the built-in config
* object rather than calling \ref Config::CreateFromBuiltinConfig each time.
*
* \param srcConfig The config containing the color space to locate an equivalent for.
* \param srcColorSpaceName Color space name in the source config. Roles and aliases may
* be used, and inactive color spaces are allowed.
* \param builtinConfig The built-in config to search. See \ref Config::CreateFromBuiltinConfig.
* \return Matching color space name from the built-in config. Empty if not found.
*
* \throw Exception if the source color space does not exist or if an interchange space
* cannot be found in srcConfig.
Comment thread
doug-walker marked this conversation as resolved.
*/
static const char * LocateBuiltinColorSpace(const ConstConfigRcPtr & srcConfig,
const char * srcColorSpaceName,
const ConstConfigRcPtr & builtinConfig);

/**
* \brief Identify the two names of a common color space that exists in both the
* given config and the provided built-in config that may be used for converting
Expand Down Expand Up @@ -788,6 +825,58 @@ class OCIOEXPORT Config
const ConstConfigRcPtr & builtinConfig,
const char * builtinColorSpaceName);

/**
* \brief Find the color space in this config identified by a Color Interop ID, per the
* ASWF Color Interop Forum ColorInteropID recommendation.
*
* Only color space names and aliases are searched (not the interop_id attribute).
* The following steps are tried in order, and the first match is returned:
* - idString itself is looked up directly.
* - If idString contains a ':', the leftmost namespace and one separator are
* stripped (only one level, regardless of how many colons remain) and the
* remainder is looked up.
* - If step 2 failed, and the stripped remainder string from step 2 is of the form
* "local:BASE" (exactly one colon), and the namespace removed in step 2 equals
* the sanitized form of this config's own \ref Config::getName(), BASE is
* looked up. Since the BASE of such an ID was itself generated by sanitizing
* a color space name (see \ref Config::generateLocalIDForColorSpace), this
* step compares BASE against the sanitized form of the color space names and
* aliases, rather than against the names as they appear in the config.
*
* \param idString A color interop ID.
*
* \return The matching color space, or null if not found (including for a null or empty
* idString).
*/
ConstColorSpaceRcPtr findColorSpaceForID(const char * idString) const;

/**
* \brief Generate an on-demand local Color Interop ID for a color space in this config,
* per the ASWF Color Interop Forum ColorInteropID recommendation.
*
* The result has the form "<CONFIG-NAME>:local:<BASE>", where CONFIG-NAME is from
* \ref Config::getName() and then sanitized per the recommendation's Annex C
* sanitizeIDToken algorithm. The canonical name and the aliases of the color space
* are then checked to see if any are left unmodified by sanitization. If so, the
* first one found is used for BASE. Otherwise, the sanitized canonical color space
* name is used for BASE.
*
* This should only be called if \ref ColorSpace::getInteropID returns nothing and
* \ref Config::LocateBuiltinColorSpace was unable to find an equivalent to this
* color space in a built-in config (which has interop IDs populated). In fact,
* this method ignores an interop ID in the color space, if present, to allow
* calculating the local ID, if necessary.
*
* \param srcColorSpaceName Name, role, or alias of an existing color space in this config.
*
* \return The generated ID.
*
* \throw Exception if srcColorSpaceName does not resolve to a color space in this config, or
* if \ref Config::getName() is empty or matches one of the built-in config names that
* is already populated with interop IDs.
*/
std::string generateLocalIDForColorSpace(const char * srcColorSpaceName) const;

/**
* Methods related to Roles.
*
Expand Down
184 changes: 184 additions & 0 deletions src/OpenColorIO/Config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,22 @@ class Config::Impl
mutable ProcessorCacheFlags m_cacheFlags { PROCESSOR_CACHE_DEFAULT };
mutable ProcessorCache<std::size_t, ProcessorRcPtr> m_processorCache;

// Cache of the color space fingerprints used by LocateBuiltinColorSpace. These are
// expensive to calculate (a Processor is built for each color space of the config), so
// they are calculated on first use and then kept until the config is modified.
//
// NB: These are held by pointer so that a caller which is using the results is not
// affected if the config is modified (resetCacheIDs below resets the ptr reference).
// So an object a caller is still holding is unaffected until it is done with it.
//
// Two mutexes are used. The first one protects the pointers and is only held for very
// short periods. The second one serializes the calculation itself, which must not be
// done while holding the first one (see the comments in getColorSpaceFingerprints).
mutable Mutex m_fingerprintMutex;
mutable Mutex m_fingerprintCalcMutex;
mutable std::shared_ptr<const ConfigUtils::TestVals> m_csTestVals;
mutable std::shared_ptr<const ConfigUtils::ColorSpaceFingerprints> m_csFingerprints;

Impl() :
m_majorVersion(LastSupportedMajorVersion),
m_minorVersion(LastSupportedMinorVersion[LastSupportedMajorVersion - 1]),
Expand Down Expand Up @@ -455,6 +471,10 @@ class Config::Impl

m_processorCache.clear();
m_processorCache.enable((m_cacheFlags & PROCESSOR_CACHE_ENABLED) == PROCESSOR_CACHE_ENABLED);

// The fingerprints are not copied, they will be recalculated if needed.
m_csTestVals.reset();
m_csFingerprints.reset();
}
return *this;
}
Expand Down Expand Up @@ -960,6 +980,83 @@ class Config::Impl
m_processorCache.enable((m_cacheFlags & PROCESSOR_CACHE_ENABLED) == PROCESSOR_CACHE_ENABLED);
}

// Get the fingerprint test values for this config, calculating them if necessary.
// The config argument must be the Config object that owns this Impl.
//
std::shared_ptr<const ConfigUtils::TestVals> getColorSpaceTestVals(
const ConstConfigRcPtr & config) const
{
{
AutoMutex lock(m_fingerprintMutex);
if (m_csTestVals)
{
return m_csTestVals;
}
}

// The calculation is serialized with its own mutex rather than the one that
// protects the pointers. That is because the calculation calls getProcessor, which
// takes m_cacheidMutex, whereas a config that is being modified takes m_cacheidMutex
// and then clears this cache. Don't want the calculation to be holding the mutex
// that resetCacheIDs needs, in case threads take them in the opposite order.
AutoMutex calcLock(m_fingerprintCalcMutex);

{
// Another thread may have completed the calculation in the meantime.
AutoMutex lock(m_fingerprintMutex);
if (m_csTestVals)
{
return m_csTestVals;
}
}

auto testVals = std::make_shared<ConfigUtils::TestVals>();
ConfigUtils::initializeTestVals(*testVals, config);

AutoMutex lock(m_fingerprintMutex);
m_csTestVals = testVals;
return m_csTestVals;
}

// Get the complete set of color space fingerprints of this config, calculating them if
// necessary. The config argument must be the Config object that owns this Impl.
//
std::shared_ptr<const ConfigUtils::ColorSpaceFingerprints> getColorSpaceFingerprints(
const ConstConfigRcPtr & config) const
{
{
AutoMutex lock(m_fingerprintMutex);
if (m_csFingerprints)
{
return m_csFingerprints;
}
}

// Get the test values used to calculate the fingerprints. Note that this must be
// done before taking the calculation mutex below.
auto testVals = getColorSpaceTestVals(config);

// As above, the calculation must not be done while holding m_fingerprintMutex.
AutoMutex calcLock(m_fingerprintCalcMutex);

{
// Another thread may have completed the calculation in the meantime.
AutoMutex lock(m_fingerprintMutex);
if (m_csFingerprints)
{
return m_csFingerprints;
}
}

auto fingerprints = std::make_shared<ConfigUtils::ColorSpaceFingerprints>();
fingerprints->testVals = *testVals;
ConfigUtils::initializeFingerprintVec(*fingerprints, config);

AutoMutex lock(m_fingerprintMutex);
m_csFingerprints = fingerprints;
return m_csFingerprints;
}

ConstProcessorRcPtr getProcessorWithoutCaching(
const Config & config,
const ConstTransformRcPtr & transform,
Expand Down Expand Up @@ -2984,6 +3081,85 @@ const char * Config::IdentifyBuiltinColorSpace(const ConstConfigRcPtr & srcConfi
builtinColorSpaceName);
}

const char * Config::LocateBuiltinColorSpace(const ConstConfigRcPtr & srcConfig,
const char * srcColorSpaceName,
const ConstConfigRcPtr & builtinConfig)
{
if (!srcConfig || !builtinConfig || !srcColorSpaceName || !*srcColorSpaceName)
{
throw Exception("LocateBuiltinColorSpace: arguments must not be null.");
}

// Note that this resolves roles and aliases and finds inactive color spaces.
ConstColorSpaceRcPtr srcColorSpace = srcConfig->getColorSpace(srcColorSpaceName);
if (!srcColorSpace)
{
std::ostringstream os;
os << "LocateBuiltinColorSpace: Source config does not contain the requested color space: "
<< srcColorSpaceName << ".";
throw Exception(os.str().c_str());
}

if (srcColorSpace->isData())
{
// Data spaces have no colorimetry to compare, so return early rather than requiring
// an interchange space to be identified. Built-in configs always have a "raw" data
// color space, so just use that rather than searching for one.
ConstColorSpaceRcPtr rawColorSpace = builtinConfig->getColorSpace("raw");
return rawColorSpace ? rawColorSpace->getName() : "";
}

// Calculate (or reuse) the cached fingerprints of both configs. This requires access to
// the private Impl of Config, which is why it is done here rather than in ConfigUtils.
auto srcTestVals = srcConfig->getImpl()->getColorSpaceTestVals(srcConfig);

const ReferenceSpaceType refSpaceType = srcColorSpace->getReferenceSpaceType();

auto testValsAreUsable = [refSpaceType](const ConfigUtils::TestVals & tv)
{
return refSpaceType == REFERENCE_SPACE_DISPLAY ? tv.displayRefTestValsConverted
: tv.sceneRefTestValsConverted;
};

// The src test values are the same colors as the ones used for the built-in config,
// but expressed in the reference space of the source config, which is what allows the
// fingerprints of the two configs to be compared without needing to adjust the
// reference space of the src color space itself.
if (!testValsAreUsable(*srcTestVals))
{
std::ostringstream os;
os << "Heuristics were not able to find an interchange space in the source config, "
<< "so it is not possible to search for the color space: "
<< srcColorSpace->getName() << ".";
throw Exception(os.str().c_str());
}

auto builtinFingerprints = builtinConfig->getImpl()->getColorSpaceFingerprints(builtinConfig);

if (!testValsAreUsable(builtinFingerprints->testVals))
{
throw Exception("Heuristics were not able to find an interchange space in the "
"built-in config.");
}

// Returns null if no match is found.
return ConfigUtils::LocateBuiltinColorSpace(srcConfig,
srcColorSpace,
builtinConfig,
srcTestVals,
builtinFingerprints);
}

std::string Config::generateLocalIDForColorSpace(const char * srcColorSpaceName) const
{
return ConfigUtils::GenerateLocalIDForColorSpace(*this, srcColorSpaceName);
}

ConstColorSpaceRcPtr Config::findColorSpaceForID(const char * idString) const
{
return ConfigUtils::FindColorSpaceForID(*this, getImpl()->m_allColorSpaces, idString);
}

///////////////////////////////////////////////////////////////////////////

const char * Config::parseColorSpaceFromString(const char * str) const
Expand Down Expand Up @@ -5926,6 +6102,14 @@ void Config::Impl::resetCacheIDs()
// As any changes could impact the cache keys, it's better to always flush the cache
// of processors to not keep in memory useless instances.
m_processorCache.clear();

// Any change could also affect the color space fingerprints (e.g. the transform of a
// color space or the interchange roles), so they must be recalculated on next use.
{
AutoMutex lock(m_fingerprintMutex);
m_csTestVals.reset();
m_csFingerprints.reset();
}
}

void Config::Impl::getAllInternalTransforms(ConstTransformVec & transformVec) const
Expand Down
Loading
Loading