diff --git a/include/OpenColorIO/OpenColorIO.h b/include/OpenColorIO/OpenColorIO.h index ad294fc80..5f0477738 100644 --- a/include/OpenColorIO/OpenColorIO.h +++ b/include/OpenColorIO/OpenColorIO.h @@ -752,6 +752,52 @@ 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 the same + * as a color space in the source config. This is the inverse of + * \ref Config::IdentifyBuiltinColorSpace. For example, if the source config + * contains a color space named "cct_ap1", passing that name would return + * "ACEScct" when using the default CG config. 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). Color spaces that are a data space, that + * define both a to_reference and a from_reference transform, or that have the + * "is-unique" category are not candidates for a match. + * + * A color space is identified by sending a set of test colors through it and + * comparing the results, so the source config and the built-in config must each + * have an interchange role set or the heuristics must be able to identify a known + * color space in them (this is the same requirement as + * \ref Config::IdentifyInterchangeSpace). Note that the heuristics look at active + * and inactive color spaces but only support scene-referred color spaces, so if the + * requested color space is display-referred, the source config must have the + * cie_xyz_d65_interchange role set. + * + * Note that the set of test results (the "fingerprints") for the built-in config + * is expensive to calculate, since it requires building a Processor for each of + * its color spaces. It is calculated on the first call and then cached on the + * config object 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 + * (that method returns a new Config object on each call). + * + * \param srcConfig The config containing the color space to search for. + * \param srcColorSpaceName Color space name in the source config. Roles and aliases may + * be used, and inactive color spaces are available. + * \param builtinConfig The built-in config to search. See \ref Config::CreateFromBuiltinConfig. + * (Any config may be used, if it has the interchange roles set.) + * \return Matching color space name from the built-in config. Empty if not found (which + * is also the case if the source color space is a data space). + * + * \throw Exception if the source color space does not exist or if an interchange space + * cannot be found in either config. + */ + 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 @@ -789,6 +835,53 @@ 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 string stripped in 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. + * + * 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). + * + * The result has the form ":local:", where CONFIG-NAME is + * \ref Config::getName() and BASE is the color space's own canonical name (i.e. not + * the alias or role that may have been used to look it up). Both parts are + * individually sanitized per the recommendation's Annex C sanitizeIDToken algorithm. + * + * \param srcColorSpaceName Name, role, or alias of an existing color space in this config. + * + * \return The generated ID. + * + * \throw Exception if \ref Config::getName() is empty or disallowed, if srcColorSpaceName is + * null/empty, or if srcColorSpaceName does not resolve to a color space in this config. + */ + std::string generateLocalIDForColorSpace(const char * srcColorSpaceName) const; + /** * Methods related to Roles. * diff --git a/src/OpenColorIO/Config.cpp b/src/OpenColorIO/Config.cpp index a8f25d5ca..41abf21c5 100644 --- a/src/OpenColorIO/Config.cpp +++ b/src/OpenColorIO/Config.cpp @@ -333,6 +333,22 @@ class Config::Impl mutable ProcessorCacheFlags m_cacheFlags { PROCESSOR_CACHE_DEFAULT }; mutable ProcessorCache 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 m_csTestVals; + mutable std::shared_ptr m_csFingerprints; + Impl() : m_majorVersion(LastSupportedMajorVersion), m_minorVersion(LastSupportedMinorVersion[LastSupportedMajorVersion - 1]), @@ -453,6 +469,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; } @@ -933,6 +953,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 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::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 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(); + 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, @@ -2941,6 +3038,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. (Note that the built-in config data space + // is intentionally not returned, since a data space is not equivalent to any other + // color space.) + return ""; + } + + // 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."); + } + + // This will throw if it is unable to identify the interchange spaces. + 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 @@ -5481,6 +5657,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 diff --git a/src/OpenColorIO/ConfigUtils.cpp b/src/OpenColorIO/ConfigUtils.cpp index 9a5ecfcda..4fb9edd95 100644 --- a/src/OpenColorIO/ConfigUtils.cpp +++ b/src/OpenColorIO/ConfigUtils.cpp @@ -7,6 +7,7 @@ #include "MathUtils.h" #include "utils/StringUtils.h" #include "Logging.h" +#include "builtinconfigs/BuiltinConfigRegistry.h" namespace OCIO_NAMESPACE { @@ -1192,9 +1193,9 @@ void initializeRefSpaceConverters(ConstTransformRcPtr & inputToBaseGtScene, // Send the test vals through the color space and store the result in fingerprintVals. // Returns true if the color space should not be considered. // -bool calcColorSpaceFingerprint(std::vector & fingerprintVals, - const ColorSpaceFingerprints & fingerprints, - const ConstConfigRcPtr & config, +bool calcColorSpaceFingerprint(std::vector & fingerprintVals, + const TestVals & testVals, + const ConstConfigRcPtr & config, const ConstColorSpaceRcPtr & cs) { bool skipColorSpace = false; @@ -1218,11 +1219,11 @@ bool calcColorSpaceFingerprint(std::vector & fingerprintVals, if (cs->getReferenceSpaceType() == REFERENCE_SPACE_DISPLAY) { - fingerprintVals = fingerprints.displayRefTestVals; + fingerprintVals = testVals.displayRefTestVals; } else { - fingerprintVals = fingerprints.sceneRefTestVals; + fingerprintVals = testVals.sceneRefTestVals; } const size_t n = fingerprintVals.size(); PackedImageDesc desc( &fingerprintVals[0], (long) n / 4, 1, CHANNEL_ORDERING_RGBA ); @@ -1232,12 +1233,21 @@ bool calcColorSpaceFingerprint(std::vector & fingerprintVals, return skipColorSpace; } -// Define a set of test values to use for a config and store them in the fingerprints struct. +// Define a set of test values to use for a config and store them in the testVals struct. // An attempt is made to convert them to the reference spaces of the config being used. // There are separate values for scene-referred and display-referred color spaces. +// The sceneRefTestValsConverted and displayRefTestValsConverted flags are set to indicate +// whether that conversion succeeded. // -void initializeTestVals(ColorSpaceFingerprints & fingerprints, const ConstConfigRcPtr & config) +void initializeTestVals(TestVals & testVals, const ConstConfigRcPtr & config) { + // The heuristics used below to identify an interchange space create a lot of Processors, + // so avoid polluting the Processor cache with transforms that won't be reused. + SuspendCacheGuard guard(config); + + testVals.sceneRefTestValsConverted = false; + testVals.displayRefTestValsConverted = false; + // Define a set of test values that are slightly inside the Rec.709 gamut // for the most common scene-referred and display-referred reference spaces. @@ -1263,8 +1273,8 @@ void initializeTestVals(ColorSpaceFingerprints & fingerprints, const ConstConfig // Try to convert to the actual reference spaces of the config. - fingerprints.sceneRefTestVals = ACESvals; - fingerprints.displayRefTestVals = XYZvals; + testVals.sceneRefTestVals = ACESvals; + testVals.displayRefTestVals = XYZvals; ConstProcessorRcPtr p; try @@ -1302,11 +1312,12 @@ void initializeTestVals(ColorSpaceFingerprints & fingerprints, const ConstConfig ConstCPUProcessorRcPtr cpu = p->getOptimizedCPUProcessor(OPTIMIZATION_NONE); cpu->apply(descSrc, descDst); - fingerprints.sceneRefTestVals = out; + testVals.sceneRefTestVals = out; + testVals.sceneRefTestValsConverted = true; } - catch (...) - { - fingerprints.sceneRefTestVals = ACESvals; + catch (...) + { + testVals.sceneRefTestVals = ACESvals; } const int m = config->getNumColorSpaces(SEARCH_REFERENCE_SPACE_DISPLAY, COLORSPACE_ALL); @@ -1349,11 +1360,12 @@ void initializeTestVals(ColorSpaceFingerprints & fingerprints, const ConstConfig ConstCPUProcessorRcPtr cpu = p->getOptimizedCPUProcessor(OPTIMIZATION_NONE); cpu->apply(descSrc, descDst); - fingerprints.displayRefTestVals = out; + testVals.displayRefTestVals = out; + testVals.displayRefTestValsConverted = true; } - catch (...) - { - fingerprints.displayRefTestVals = XYZvals; + catch (...) + { + testVals.displayRefTestVals = XYZvals; } } @@ -1361,12 +1373,13 @@ void initializeTestVals(ColorSpaceFingerprints & fingerprints, const ConstConfig // to compare against color spaces in an input config for merging. Store the results in // the fingerprint struct. // -void initializeColorSpaceFingerprints(ColorSpaceFingerprints & fingerprints, const ConstConfigRcPtr & config) +// The test values in the fingerprints struct must have been initialized already, by +// calling initializeTestVals. +// +void initializeFingerprintVec(ColorSpaceFingerprints & fingerprints, const ConstConfigRcPtr & config) { SuspendCacheGuard srcGuard(config); - initializeTestVals(fingerprints, config); - const int n = config->getNumColorSpaces(SEARCH_REFERENCE_SPACE_ALL, COLORSPACE_ALL); fingerprints.vec.clear(); fingerprints.vec.reserve(n); @@ -1399,7 +1412,7 @@ void initializeColorSpaceFingerprints(ColorSpaceFingerprints & fingerprints, con } std::vector fp; - const bool skipColorSpace = calcColorSpaceFingerprint(fp, fingerprints, config, cs); + const bool skipColorSpace = calcColorSpaceFingerprint(fp, fingerprints.testVals, config, cs); if (!skipColorSpace) { Fingerprint fprint; @@ -1411,12 +1424,17 @@ void initializeColorSpaceFingerprints(ColorSpaceFingerprints & fingerprints, con } } +void initializeColorSpaceFingerprints(ColorSpaceFingerprints & fingerprints, const ConstConfigRcPtr & config) +{ + initializeTestVals(fingerprints.testVals, config); + initializeFingerprintVec(fingerprints, config); +} + // If the base config contains a color space equivalent to inputCS, return its name. // Return an empty string if no equivalent color space is found (within the tolerance). -// The ref_space_type specifies the type of inputCS and determines which part of the -// config is searched. +// This version assumes the reference space of both configs is the same. // -const char * findEquivalentColorspace(const ColorSpaceFingerprints & fingerprints, +const char * findEquivalentColorSpace(const ColorSpaceFingerprints & fingerprints, const ConstConfigRcPtr & inputConfig, const ConstColorSpaceRcPtr & inputCS) { @@ -1424,7 +1442,19 @@ const char * findEquivalentColorspace(const ColorSpaceFingerprints & fingerprint // NB: The inputConfig/inputCS must use the same reference space as the base config. // In general, this means that updateReferenceColorspace must be called on inputCS // before calling this function. + return findEquivalentColorSpace(fingerprints, fingerprints.testVals, inputConfig, inputCS); +} +// If the base config contains a color space equivalent to inputCS, return its name. +// Return an empty string if no equivalent color space is found (within the tolerance). +// This version assumes inputTestVals have been converted so that they are in the +// reference space of the inputConfig. +// +const char * findEquivalentColorSpace(const ColorSpaceFingerprints & fingerprints, + const TestVals & inputTestVals, + const ConstConfigRcPtr & inputConfig, + const ConstColorSpaceRcPtr & inputCS) +{ // TODO: Should data spaces ever be replaced? if (inputCS->isData()) { @@ -1433,7 +1463,7 @@ const char * findEquivalentColorspace(const ColorSpaceFingerprints & fingerprint // Calculate the fingerprint of inputCS from inputConfig. std::vector inputVals; - const bool skipColorSpace = calcColorSpaceFingerprint(inputVals, fingerprints, inputConfig, inputCS); + const bool skipColorSpace = calcColorSpaceFingerprint(inputVals, inputTestVals, inputConfig, inputCS); if (skipColorSpace) { return ""; @@ -1465,18 +1495,274 @@ const char * findEquivalentColorspace(const ColorSpaceFingerprints & fingerprint if (!EqualWithAbsError(inputVals[i], fp.vals[i], absTolerance)) { matchFound = false; - continue; + break; } } if (matchFound) { - return fp.csName; + return fp.csName.c_str(); } } return ""; } +// Try to find the name of a color space in the built-in config that is equivalent to +// srcColorSpace. See the declaration in ConfigUtils.h for details. +// +const char * LocateBuiltinColorSpace(const ConstConfigRcPtr & srcConfig, + const ConstColorSpaceRcPtr & srcColorSpace, + const ConstConfigRcPtr & builtinConfig, + const std::shared_ptr & srcTestVals, + const std::shared_ptr & fingerprints) +{ + const char * name = findEquivalentColorSpace(*fingerprints, + *srcTestVals, + srcConfig, + srcColorSpace); + if (!name || !*name) + { + return ""; + } + + // Return the name owned by the color space object rather than the one owned by the + // fingerprints, since the latter may be removed from the cache if the config is edited. + ConstColorSpaceRcPtr builtinColorSpace = builtinConfig->getColorSpace(name); + return builtinColorSpace ? builtinColorSpace->getName() : ""; +} + +////////////////////////////////////////////////////////////////////////////////////// + +// Sanitize a single token for use in a Color Interop ID. See the declaration in ConfigUtils.h +// and Annex C of the ASWF Color Interop Forum ColorInteropID recommendation for details. This +// must match that algorithm exactly (including the specific character mappings below) since +// searching for an ID requires sanitizing with the same algorithm used to generate it. +// +// NB: The reference algorithm operates on Unicode codepoints (a non-ASCII character always maps +// to a single '^', regardless of how many bytes it takes to encode). To match that exactly, and +// so the sanitized result does not depend on the byte-level UTF-8 encoding of the input, this +// decodes each multi-byte UTF-8 sequence and emits a single '^' for it, rather than one '^' per +// byte. A byte that looks like a UTF-8 lead byte but isn't followed by the expected continuation +// bytes (i.e. malformed input) is still safely consumed one byte at a time, one '^' each. +// +std::string SanitizeIDToken(std::string_view token) +{ + static const std::string allowed{ "abcdefghijklmnopqrstuvwxyz0123456789.-_~/*#%^+()[]|" }; + + std::string result; + result.reserve(token.size()); + + const size_t n = token.size(); + size_t i = 0; + while (i < n) + { + const unsigned char c = static_cast(token[i]); + + if (c > 127) + { + // Determine how many bytes the UTF-8 sequence starting here should occupy, then + // only consume as many of them as actually look like continuation bytes (0x80-0xBF). + size_t seqLen = 1; + if ((c & 0xE0) == 0xC0) seqLen = 2; + else if ((c & 0xF0) == 0xE0) seqLen = 3; + else if ((c & 0xF8) == 0xF0) seqLen = 4; + + size_t consumed = 1; + while (consumed < seqLen && i + consumed < n && + (static_cast(token[i + consumed]) & 0xC0) == 0x80) + { + ++consumed; + } + + result.push_back('^'); + i += consumed; + continue; + } + + char mapped = 0; + switch (c) + { + case ' ': case '\t': case '\n': case '\r': mapped = '_'; break; + case '{': case '<': mapped = '('; break; + case '}': case '>': mapped = ')'; break; + case ',': mapped = '.'; break; + case ';': case ':': mapped = '|'; break; + case '\'': case '"': mapped = '#'; break; + case '\\': mapped = '/'; break; + default: break; + } + + if (mapped) + { + result.push_back(mapped); + } + else if (allowed.find(static_cast(c)) != std::string::npos) + { + result.push_back(static_cast(c)); + } + else if (std::isupper(c)) + { + result.push_back(static_cast(std::tolower(c))); + } + else + { + result.push_back('*'); + } + + ++i; + } + + return result; +} + +std::string GenerateLocalIDForColorSpace(const Config & config, const char * srcColorSpaceName) +{ + if (!srcColorSpaceName || !*srcColorSpaceName) + { + throw Exception("generateLocalIDForColorSpace: srcColorSpaceName must not be " + "null or empty."); + } + + const char * configName = config.getName(); + if (!configName || !*configName) + { + throw Exception("May not generate a local interop ID if the config name is empty."); + } + + const std::string sanitizedConfigName = SanitizeIDToken(configName); + + // Disallow usage of config names for one of the OCIO configs for ACES that already + // contains interop IDs for all color spaces. Trying to avoid the situation where + // someone edits one of these configs but forgets to change the config name. This + // would result in a meaningless interop ID. + if (IsReservedConfigName(sanitizedConfigName)) + { + std::ostringstream os; + os << "May not generate a local interop ID if the name matches an ACES config " + << "that already has interop IDs: " << configName << "."; + throw Exception(os.str().c_str()); + } + + // Note that this resolves roles and aliases and finds inactive color spaces. + ConstColorSpaceRcPtr cs = config.getColorSpace(srcColorSpaceName); + if (!cs) + { + std::ostringstream os; + os << "generateLocalIDForColorSpace: This config does not contain the " + << "requested color space: " << srcColorSpaceName << "."; + throw Exception(os.str().c_str()); + } + + // Use the color space's own canonical name (rather than the possibly aliased/role-based + // srcColorSpaceName argument) so that the generated ID is stable and may be resolved back + // by Config::findColorSpaceForID. + std::string base = cs->getName(); + + // If the name would require sanitization, prefer the first alias (in order) that does not + // require any sanitization, so the ID stays as readable as possible. + if (SanitizeIDToken(base) != base) + { + const size_t numAliases = cs->getNumAliases(); + for (size_t i = 0; i < numAliases; ++i) + { + const std::string alias = cs->getAlias(i); + if (SanitizeIDToken(alias) == alias) + { + base = alias; + break; + } + } + } + + return sanitizedConfigName + ":local:" + SanitizeIDToken(base); +} + +ConstColorSpaceRcPtr FindColorSpaceForID(const Config & config, + const ConstColorSpaceSetRcPtr & allColorSpaces, + const char * idString) +{ + const std::string id{ idString ? idString : "" }; + if (id.empty()) + { + return ConstColorSpaceRcPtr(); + } + + // Step 1: the full ID string. + ConstColorSpaceRcPtr cs = config.getColorSpace(id.c_str()); + if (cs) + { + return cs; + } + + // Step 2: strip the leftmost namespace and one separator (only one level, regardless of + // how many colons remain in the result), and look up the remainder. + const size_t firstColon = id.find(':'); + if (firstColon == std::string::npos) + { + return ConstColorSpaceRcPtr(); + } + + const std::string_view outerNamespace{ id.data(), firstColon }; + const std::string stripped = id.substr(firstColon + 1); + + // "local" is a reserved keyword that may only appear as the inner namespace of the mode 3 + // form ("NAMESPACE:local:BASE"). It must not be treated as an ordinary outer namespace to + // strip off, so skip the plain lookup in that case. + if (outerNamespace != "local") + { + cs = config.getColorSpace(stripped.c_str()); + if (cs) + { + return cs; + } + } + + // Step 3: local mode. The stripped remainder must be exactly "local:BASE" (i.e. exactly + // one colon left), and the namespace removed in step 2 must match this config's own + // (sanitized) name. + const size_t innerColon = stripped.find(':'); + if (innerColon != std::string::npos && + stripped.compare(0, innerColon, "local") == 0 && + stripped.find(':', innerColon + 1) == std::string::npos) + { + const char * configName = config.getName(); + if (configName && *configName + && SanitizeIDToken(configName) == SanitizeIDToken(outerNamespace)) + { + // Note that because the base name in the interop ID is sanitized, the comparison + // must sanitize the color space names and aliases in the config as well. + + // The base should already be sanitized, but enforce that it must be in order to match. + const std::string base = SanitizeIDToken( + std::string_view(stripped).substr(innerColon + 1)); + + // This emulates getIndex in ColorSpaceSet.cpp, so it matches what getColorSpace does. + // If two names sanitize to the same string, the first one in the config wins. + const int numColorSpaces = allColorSpaces->getNumColorSpaces(); + for (int idx = 0; idx < numColorSpaces; ++idx) + { + ConstColorSpaceRcPtr candidate = allColorSpaces->getColorSpaceByIndex(idx); + + if (SanitizeIDToken(candidate->getName()) == base) + { + return candidate; + } + + const size_t numAliases = candidate->getNumAliases(); + for (size_t aidx = 0; aidx < numAliases; ++aidx) + { + if (SanitizeIDToken(candidate->getAlias(aidx)) == base) + { + return candidate; + } + } + } + } + } + + return ConstColorSpaceRcPtr(); +} + } // namespace ConfigUtils } // namespace OCIO_NAMESPACE diff --git a/src/OpenColorIO/ConfigUtils.h b/src/OpenColorIO/ConfigUtils.h index 4965a6e2d..8812e83a2 100644 --- a/src/OpenColorIO/ConfigUtils.h +++ b/src/OpenColorIO/ConfigUtils.h @@ -4,6 +4,9 @@ #ifndef INCLUDED_OCIO_CONFIG_UTILS_H #define INCLUDED_OCIO_CONFIG_UTILS_H +#include +#include + #include namespace OCIO_NAMESPACE @@ -52,30 +55,113 @@ void updateReferenceView(ViewTransformRcPtr & vt, struct Fingerprint { - const char * csName; + // NB: The csName is a string rather than a pointer to the name owned by the ColorSpace + // object since the fingerprints may be cached for longer than the lifetime of that object. + std::string csName; ReferenceSpaceType type; std::vector vals; }; -struct ColorSpaceFingerprints +// The test values of a config, expressed in the reference spaces of that config. These +// are all that is needed to calculate the fingerprint of a color space from that config. +// +struct TestVals { - std::vector vec; std::vector sceneRefTestVals; std::vector displayRefTestVals; + + // True if the test values were successfully converted into the reference space of the + // config they were initialized from (i.e., an interchange space was identified). If + // false, the values are the default ACES2065-1 or CIE-XYZ-D65 values, which are only + // meaningful if the config happens to use that space as its reference space. + // + // Fingerprints calculated from two different configs may only be compared with each + // other if the corresponding flag is true for both configs, otherwise the test values + // do not represent the same colors in both configs. + bool sceneRefTestValsConverted = false; + bool displayRefTestValsConverted = false; +}; + +// A fingerprint for each color space of a config, along with the test values they were +// calculated from. This is what is needed to search that config for a color space that is +// equivalent to one from another config. +// +struct ColorSpaceFingerprints +{ + // Keep a copy of the testVals because it is a useful record of what was used to + // compute the fingerprints. + TestVals testVals; + std::vector vec; }; -bool calcColorSpaceFingerprint(std::vector & fingerprintVals, - const ColorSpaceFingerprints & fingerprints, - const ConstConfigRcPtr & config, +bool calcColorSpaceFingerprint(std::vector & fingerprintVals, + const TestVals & testVals, + const ConstConfigRcPtr & config, const ConstColorSpaceRcPtr & cs); +// Initialize the test values needed to calculate fingerprints for a config. +void initializeTestVals(TestVals & testVals, + const ConstConfigRcPtr & config); + +// Initialize the vector of color space fingerprints. +void initializeFingerprintVec(ColorSpaceFingerprints & fingerprints, + const ConstConfigRcPtr & config); + +// Initialize the test values, then the fingerprints. void initializeColorSpaceFingerprints(ColorSpaceFingerprints & fingerprints, const ConstConfigRcPtr & config); -const char * findEquivalentColorspace(const ColorSpaceFingerprints & fingerprints, - const ConstConfigRcPtr & inputConfig, +const char * findEquivalentColorSpace(const ColorSpaceFingerprints & fingerprints, + const ConstConfigRcPtr & inputConfig, + const ConstColorSpaceRcPtr & inputCS); + +// Same as above, but the fingerprint of inputCS is calculated using the test values from +// inputTestVals (which must come from inputConfig) rather than those from fingerprints. +// This allows searching for an equivalent color space without needing to first adjust the +// reference space of inputCS to match the config the fingerprints were built from. +const char * findEquivalentColorSpace(const ColorSpaceFingerprints & fingerprints, + const TestVals & inputTestVals, + const ConstConfigRcPtr & inputConfig, const ConstColorSpaceRcPtr & inputCS); +// Try to find the name of a color space in the built-in config that is equivalent to +// srcColorSpace. Both active and inactive color spaces of the built-in config are searched. +// +// srcConfig/srcColorSpace -- The color space to search for and the config that owns it. +// builtinConfig -- The built-in config object to search. +// srcTestVals -- The (already calculated) test values of srcConfig. +// fingerprints -- The (already calculated) fingerprints of builtinConfig. +// Returns the name of the color space in the built-in config. +// +// \throw Exception if an interchange space cannot be found in either config. +// +const char * LocateBuiltinColorSpace(const ConstConfigRcPtr & srcConfig, + const ConstColorSpaceRcPtr & srcColorSpace, + const ConstConfigRcPtr & builtinConfig, + const std::shared_ptr & srcTestVals, + const std::shared_ptr & fingerprints); + +// Sanitize a single token (e.g. a config name or a color space base name) for use in a Color +// Interop ID, per Annex C of the ASWF Color Interop Forum ColorInteropID recommendation. Not +// meant to be applied to an already-namespaced ID string as a whole. +std::string SanitizeIDToken(std::string_view token); + +// Implements Config::generateLocalIDForColorSpace. See that method's doc comment for the +// algorithm. +// +// \throw Exception if srcColorSpaceName is null/empty, the config's name is empty or disallowed, +// or the config does not contain the requested color space. +std::string GenerateLocalIDForColorSpace(const Config & config, const char * srcColorSpaceName); + +// Implements Config::findColorSpaceForID. See that method's doc comment for the algorithm. +// The allColorSpaces argument must be the complete (unfiltered) set of color spaces of the +// given config. It is needed because the "local" mode fall-back must compare against the +// sanitized form of the color space names and aliases, which requires iterating over the +// color space objects themselves. +ConstColorSpaceRcPtr FindColorSpaceForID(const Config & config, + const ConstColorSpaceSetRcPtr & allColorSpaces, + const char * idString); + // Temporarily deactivate the Processor cache on a Config object. // class SuspendCacheGuard diff --git a/src/OpenColorIO/apphelpers/mergeconfigs/SectionMerger.cpp b/src/OpenColorIO/apphelpers/mergeconfigs/SectionMerger.cpp index 33c41f293..1421a5542 100644 --- a/src/OpenColorIO/apphelpers/mergeconfigs/SectionMerger.cpp +++ b/src/OpenColorIO/apphelpers/mergeconfigs/SectionMerger.cpp @@ -2248,7 +2248,7 @@ bool ColorspacesMerger::handleAvoidDuplicatesOption(ConfigUtils::ColorSpaceFinge // (e.g., consider the CIE-XYZ-D65 space, which is typically inactive). However, // when the inactive list is regenerated to avoid listing removed color spaces, // some color spaces that were inactive may become active. - const char * duplicateInBase = ConfigUtils::findEquivalentColorspace( + const char * duplicateInBase = ConfigUtils::findEquivalentColorSpace( fingerprints, inputConfig, inputCS ); diff --git a/src/OpenColorIO/builtinconfigs/BuiltinConfigRegistry.cpp b/src/OpenColorIO/builtinconfigs/BuiltinConfigRegistry.cpp index 6fd3b7b52..42b68eea4 100644 --- a/src/OpenColorIO/builtinconfigs/BuiltinConfigRegistry.cpp +++ b/src/OpenColorIO/builtinconfigs/BuiltinConfigRegistry.cpp @@ -60,6 +60,21 @@ const char * ResolveConfigPath(const char * originalPath) noexcept return originalPath; } +bool IsReservedConfigName(const std::string & sanitizedConfigName) +{ + static const std::regex builtinConfigNamePattern( + R"(^(?:cg-config|studio-config)-v(\d+)\.\d+\.\d+_aces-v[\d.]+_ocio-v[\d.]+$)"); + + std::smatch match; + if (!std::regex_match(sanitizedConfigName, match, builtinConfigNamePattern)) + { + return false; + } + + const int majorVersion = std::stoi(match[1].str()); + return majorVersion >= 4; +} + const BuiltinConfigRegistry & BuiltinConfigRegistry::Get() noexcept { // Meyer's Singleton pattern. diff --git a/src/OpenColorIO/builtinconfigs/BuiltinConfigRegistry.h b/src/OpenColorIO/builtinconfigs/BuiltinConfigRegistry.h index 21adb6359..05d8893c3 100644 --- a/src/OpenColorIO/builtinconfigs/BuiltinConfigRegistry.h +++ b/src/OpenColorIO/builtinconfigs/BuiltinConfigRegistry.h @@ -5,12 +5,22 @@ #ifndef INCLUDED_OCIO_BUILTIN_CONFIGS_REGISTRY_H #define INCLUDED_OCIO_BUILTIN_CONFIGS_REGISTRY_H +#include #include #include namespace OCIO_NAMESPACE { +// Returns true if sanitizedConfigName (the result of ConfigUtils::SanitizeIDToken on a config +// name) is the name of one of the ASWF OCIO configs (e.g. "cg-config-v4.0.0_aces-v2.0_ocio-v2.5") +// version 4.0.0 or higher, since those configs already have interop_id attributes populated +// and so must not be used as the namespace of a locally-generated interop ID. A name that +// happens to start with "cg-config-"/"studio-config-" followed by a version but continues with +// something other than the expected "_aces-_ocio-" suffix is not one of these +// built-in configs and so is not reserved. See GenerateLocalIDForColorSpace. +bool IsReservedConfigName(const std::string & sanitizedConfigName); + class BuiltinConfigRegistryImpl : public BuiltinConfigRegistry { struct BuiltinConfigData diff --git a/src/bindings/python/PyConfig.cpp b/src/bindings/python/PyConfig.cpp index 6e7971844..89fcc71ef 100644 --- a/src/bindings/python/PyConfig.cpp +++ b/src/bindings/python/PyConfig.cpp @@ -330,7 +330,11 @@ void bindPyConfig(py::module & m) }) .def("getCanonicalName", &Config::getCanonicalName, "name"_a, DOC(Config, getCanonicalName)) - .def("addColorSpace", &Config::addColorSpace, "colorSpace"_a, + .def("generateLocalIDForColorSpace", &Config::generateLocalIDForColorSpace, + "srcColorSpaceName"_a, DOC(Config, generateLocalIDForColorSpace)) + .def("findColorSpaceForID", &Config::findColorSpaceForID, "idString"_a, + DOC(Config, findColorSpaceForID)) + .def("addColorSpace", &Config::addColorSpace, "colorSpace"_a, DOC(Config, addColorSpace)) .def("removeColorSpace", &Config::removeColorSpace, "name"_a, DOC(Config, removeColorSpace)) @@ -363,6 +367,17 @@ void bindPyConfig(py::module & m) "srcConfig"_a, "builtinConfig"_a, "builtinColorSpaceName"_a, DOC(Config, IdentifyBuiltinColorSpace)) + .def_static("LocateBuiltinColorSpace", [](const ConstConfigRcPtr & srcConfig, + const char * srcColorSpaceName, + const ConstConfigRcPtr & builtinConfig) + { + return Config::LocateBuiltinColorSpace(srcConfig, + srcColorSpaceName, + builtinConfig); + }, + "srcConfig"_a, "srcColorSpaceName"_a, "builtinConfig"_a, + DOC(Config, LocateBuiltinColorSpace)) + .def_static("IdentifyInterchangeSpace", [](const ConstConfigRcPtr & srcConfig, const char * srcColorSpaceName, const ConstConfigRcPtr & builtinConfig, diff --git a/tests/cpu/ColorSpace_tests.cpp b/tests/cpu/ColorSpace_tests.cpp index 09819632e..d98fde915 100644 --- a/tests/cpu/ColorSpace_tests.cpp +++ b/tests/cpu/ColorSpace_tests.cpp @@ -1923,6 +1923,140 @@ ocio_profile_version: 2 const char * csname = OCIO::Config::IdentifyBuiltinColorSpace(editableCfg, builtinConfig, "ACEScg"); OCIO_CHECK_EQUAL(std::string(csname), std::string("ACES cg")); } + + // + // Test LocateBuiltinColorSpace. (This is the inverse of IdentifyBuiltinColorSpace.) + // + + // Set both interchange roles, so the heuristics are not needed yet. + editableCfg->setRole("aces_interchange", "ref_cs"); + editableCfg->setRole("cie_xyz_d65_interchange", "CIE-XYZ-D65"); + editableCfg->setInactiveColorSpaces(""); + + { + const char * csname = OCIO::Config::LocateBuiltinColorSpace(editableCfg, "ACES cg", builtinConfig); + OCIO_CHECK_EQUAL(std::string(csname), std::string("ACEScg")); + } + + { + const char * csname = OCIO::Config::LocateBuiltinColorSpace(editableCfg, "Texture -- sRGB", builtinConfig); + OCIO_CHECK_EQUAL(std::string(csname), std::string("sRGB Encoded Rec.709 (sRGB)")); + } + + { + const char * csname = OCIO::Config::LocateBuiltinColorSpace(editableCfg, "Linear ITU-R BT.709", builtinConfig); + OCIO_CHECK_EQUAL(std::string(csname), std::string("Linear Rec.709 (sRGB)")); + } + + { + const char * csname = OCIO::Config::LocateBuiltinColorSpace(editableCfg, "not sRGB", builtinConfig); + OCIO_CHECK_EQUAL(std::string(csname), std::string("ACEScct")); + } + + // Display-referred color spaces work if the display interchange role is present. + { + const char * csname = OCIO::Config::LocateBuiltinColorSpace(editableCfg, "sRGB - Display CS", builtinConfig); + OCIO_CHECK_EQUAL(std::string(csname), std::string("sRGB - Display")); + } + + // Roles may be used for the source color space name and inactive color spaces are found. + editableCfg->setInactiveColorSpaces("ref_cs"); + { + // The scene_linear role is set to "ref_cs", which is the reference space. + const char * csname = OCIO::Config::LocateBuiltinColorSpace(editableCfg, "scene_linear", builtinConfig); + OCIO_CHECK_EQUAL(std::string(csname), std::string("ACES2065-1")); + } + editableCfg->setInactiveColorSpaces(""); + + // A color space with no equivalent in the built-in config returns an empty string. + { + // This is only the sRGB gamma curve, with no primary conversion. + const char * csname = OCIO::Config::LocateBuiltinColorSpace(editableCfg, "sRGB - curve", builtinConfig); + OCIO_CHECK_EQUAL(std::string(csname), std::string("")); + } + + // A data space returns an empty string rather than the data space of the built-in config. + { + const char * csname = OCIO::Config::LocateBuiltinColorSpace(editableCfg, "raw data", builtinConfig); + OCIO_CHECK_EQUAL(std::string(csname), std::string("")); + } + + // Test that a missing or empty source color space name throws. + { + OCIO_CHECK_THROW_WHAT( + OCIO::Config::LocateBuiltinColorSpace(editableCfg, "Foo", builtinConfig), + OCIO::Exception, + "Source config does not contain the requested color space: Foo." + ); + + OCIO_CHECK_THROW_WHAT( + OCIO::Config::LocateBuiltinColorSpace(editableCfg, "", builtinConfig), + OCIO::Exception, + "LocateBuiltinColorSpace: arguments must not be null." + ); + } + + // Test that the fingerprints are recalculated if the source config is modified. + // (Point the interchange role at the wrong color space and check that the previously + // cached test values are not reused.) + { + editableCfg->setRole("aces_interchange", "Texture -- sRGB"); + + const char * csname = OCIO::Config::LocateBuiltinColorSpace(editableCfg, "ACES cg", builtinConfig); + OCIO_CHECK_ASSERT(std::string(csname) != std::string("ACEScg")); + + editableCfg->setRole("aces_interchange", "ref_cs"); + + csname = OCIO::Config::LocateBuiltinColorSpace(editableCfg, "ACES cg", builtinConfig); + OCIO_CHECK_EQUAL(std::string(csname), std::string("ACEScg")); + } + + // Test that the fingerprints are recalculated if the built-in config is modified. + { + OCIO::ConfigRcPtr editableBuiltin = builtinConfig->createEditableCopy(); + + const char * csname = OCIO::Config::LocateBuiltinColorSpace(editableCfg, "ACES cg", editableBuiltin); + OCIO_CHECK_EQUAL(std::string(csname), std::string("ACEScg")); + + // Rename the color space that was found above. If the fingerprints were not + // recalculated, the stale name would no longer be found in the config and an empty + // string would be returned. + OCIO::ColorSpaceRcPtr renamedCS = editableBuiltin->getColorSpace("ACEScg")->createEditableCopy(); + renamedCS->setName("ACES cg 2"); + editableBuiltin->removeColorSpace("ACEScg"); + editableBuiltin->addColorSpace(renamedCS); + + csname = OCIO::Config::LocateBuiltinColorSpace(editableCfg, "ACES cg", editableBuiltin); + OCIO_CHECK_EQUAL(std::string(csname), std::string("ACES cg 2")); + } + + // Test that the heuristics are used if the interchange roles are not present. + editableCfg->setRole("aces_interchange", ""); + editableCfg->setRole("cie_xyz_d65_interchange", ""); + { + // The heuristics use the active color spaces to find the reference space. + const char * csname = OCIO::Config::LocateBuiltinColorSpace(editableCfg, "ACES cg", builtinConfig); + OCIO_CHECK_EQUAL(std::string(csname), std::string("ACEScg")); + } + + // Display-referred color spaces are not supported by the heuristics, but note that + // in this config the display-referred interchange space is found by its name. + { + const char * csname = OCIO::Config::LocateBuiltinColorSpace(editableCfg, "sRGB - Display CS", builtinConfig); + OCIO_CHECK_EQUAL(std::string(csname), std::string("sRGB - Display")); + } + + // Make the color spaces that the heuristics are able to use inactive, so that an + // interchange space can no longer be identified in the source config. + editableCfg->setInactiveColorSpaces("ACES cg, Linear ITU-R BT.709, Texture -- sRGB, " + "OCIO v1 -- sRGB, ref_cs"); + { + OCIO_CHECK_THROW_WHAT( + OCIO::Config::LocateBuiltinColorSpace(editableCfg, "ACES cg", builtinConfig), + OCIO::Exception, + "Heuristics were not able to find an interchange space in the source config" + ); + } } OCIO_ADD_TEST(ConfigUtils, processor_to_known_colorspace_alt_config) diff --git a/tests/cpu/ConfigUtils_tests.cpp b/tests/cpu/ConfigUtils_tests.cpp index db58c3f62..13835ec11 100644 --- a/tests/cpu/ConfigUtils_tests.cpp +++ b/tests/cpu/ConfigUtils_tests.cpp @@ -712,68 +712,401 @@ inactive_colorspaces: [] { OCIO::ConstColorSpaceRcPtr cs = inputConfig->getColorSpace("ref_space"); - const char * name = OCIO::ConfigUtils::findEquivalentColorspace(fingerprints, inputConfig, cs); + const char * name = OCIO::ConfigUtils::findEquivalentColorSpace(fingerprints, inputConfig, cs); OCIO_CHECK_EQUAL(name, std::string("ACES2065-1")); } { OCIO::ConstColorSpaceRcPtr cs = inputConfig->getColorSpace("standard RGB"); - const char * name = OCIO::ConfigUtils::findEquivalentColorspace(fingerprints, inputConfig, cs); + const char * name = OCIO::ConfigUtils::findEquivalentColorSpace(fingerprints, inputConfig, cs); OCIO_CHECK_EQUAL(name, std::string("sRGB - Texture")); } { OCIO::ConstColorSpaceRcPtr cs = inputConfig->getColorSpace("approx. standard RGB"); - const char * name = OCIO::ConfigUtils::findEquivalentColorspace(fingerprints, inputConfig, cs); + const char * name = OCIO::ConfigUtils::findEquivalentColorSpace(fingerprints, inputConfig, cs); OCIO_CHECK_EQUAL(name, std::string("sRGB - Texture")); } { OCIO::ConstColorSpaceRcPtr cs = inputConfig->getColorSpace("very approx. standard RGB"); - const char * name = OCIO::ConfigUtils::findEquivalentColorspace(fingerprints, inputConfig, cs); + const char * name = OCIO::ConfigUtils::findEquivalentColorSpace(fingerprints, inputConfig, cs); OCIO_CHECK_EQUAL(name, std::string("")); } { OCIO::ConstColorSpaceRcPtr cs = inputConfig->getColorSpace("ACES cct"); - const char * name = OCIO::ConfigUtils::findEquivalentColorspace(fingerprints, inputConfig, cs); + const char * name = OCIO::ConfigUtils::findEquivalentColorSpace(fingerprints, inputConfig, cs); OCIO_CHECK_EQUAL(name, std::string("ACEScct")); } { OCIO::ConstColorSpaceRcPtr cs = inputConfig->getColorSpace("requires ACES cct"); - const char * name = OCIO::ConfigUtils::findEquivalentColorspace(fingerprints, inputConfig, cs); + const char * name = OCIO::ConfigUtils::findEquivalentColorSpace(fingerprints, inputConfig, cs); OCIO_CHECK_EQUAL(name, std::string("ACEScct")); } { OCIO::ConstColorSpaceRcPtr cs = inputConfig->getColorSpace("ACES cg"); - const char * name = OCIO::ConfigUtils::findEquivalentColorspace(fingerprints, inputConfig, cs); + const char * name = OCIO::ConfigUtils::findEquivalentColorSpace(fingerprints, inputConfig, cs); OCIO_CHECK_EQUAL(name, std::string("ACEScg")); } { OCIO::ConstColorSpaceRcPtr cs = inputConfig->getColorSpace("ACES cg display"); - const char * name = OCIO::ConfigUtils::findEquivalentColorspace(fingerprints, inputConfig, cs); + const char * name = OCIO::ConfigUtils::findEquivalentColorSpace(fingerprints, inputConfig, cs); OCIO_CHECK_EQUAL(name, std::string("")); } { OCIO::ConstColorSpaceRcPtr cs = inputConfig->getColorSpace("pq display"); - const char * name = OCIO::ConfigUtils::findEquivalentColorspace(fingerprints, inputConfig, cs); + const char * name = OCIO::ConfigUtils::findEquivalentColorSpace(fingerprints, inputConfig, cs); OCIO_CHECK_EQUAL(name, std::string("Rec.2100-PQ - Display")); } { OCIO::ConstColorSpaceRcPtr cs = inputConfig->getColorSpace("pq display scene"); - const char * name = OCIO::ConfigUtils::findEquivalentColorspace(fingerprints, inputConfig, cs); + const char * name = OCIO::ConfigUtils::findEquivalentColorSpace(fingerprints, inputConfig, cs); OCIO_CHECK_EQUAL(name, std::string("")); } // Validate that the fingerprints for the smallest likely gamuts are positive. std::vector fingerprintVals; - OCIO_CHECK_ASSERT(!OCIO::ConfigUtils::calcColorSpaceFingerprint(fingerprintVals, fingerprints, inputConfig, - inputConfig->getColorSpace("Rec.601 - Display"))); + OCIO_CHECK_ASSERT(!OCIO::ConfigUtils::calcColorSpaceFingerprint(fingerprintVals, fingerprints.testVals, + inputConfig, inputConfig->getColorSpace("Rec.601 - Display"))); for (size_t i = 0; i < fingerprintVals.size(); i++) { OCIO_CHECK_ASSERT(fingerprintVals[i] >= 0.); } - OCIO_CHECK_ASSERT(!OCIO::ConfigUtils::calcColorSpaceFingerprint(fingerprintVals, fingerprints, inputConfig, - inputConfig->getColorSpace("Rec.601 (PAL) - Display"))); + OCIO_CHECK_ASSERT(!OCIO::ConfigUtils::calcColorSpaceFingerprint(fingerprintVals, fingerprints.testVals, + inputConfig, inputConfig->getColorSpace("Rec.601 (PAL) - Display"))); for (size_t i = 0; i < fingerprintVals.size(); i++) { OCIO_CHECK_ASSERT(fingerprintVals[i] >= 0.); } + + // Test the overload that uses the test values of the input config rather than those of + // the base config. This is what Config::LocateBuiltinColorSpace uses, since it allows + // the color spaces of two configs to be compared without first adjusting the reference + // space of the input color space. + + OCIO::ConfigUtils::TestVals inputTestVals; + OCIO::ConfigUtils::initializeTestVals(inputTestVals, inputConfig); + + // The base config has both interchange roles. + OCIO_CHECK_ASSERT(fingerprints.testVals.sceneRefTestValsConverted); + OCIO_CHECK_ASSERT(fingerprints.testVals.displayRefTestValsConverted); + + // The input config has neither interchange role, so the heuristics are used. They are + // able to identify the scene-referred reference space ("ref_space"), but they do not + // support display-referred spaces. (The display-referred test values happen to be + // correct anyway here, since the display reference space is CIE-XYZ-D65.) + OCIO_CHECK_ASSERT(inputTestVals.sceneRefTestValsConverted); + OCIO_CHECK_ASSERT(!inputTestVals.displayRefTestValsConverted); + + // The reference space of the input config is also ACES2065-1, so the results are the + // same as the ones above. + { + OCIO::ConstColorSpaceRcPtr cs = inputConfig->getColorSpace("ACES cg"); + const char * name = OCIO::ConfigUtils::findEquivalentColorSpace(fingerprints, inputTestVals, + inputConfig, cs); + OCIO_CHECK_EQUAL(name, std::string("ACEScg")); + } + { + OCIO::ConstColorSpaceRcPtr cs = inputConfig->getColorSpace("standard RGB"); + const char * name = OCIO::ConfigUtils::findEquivalentColorSpace(fingerprints, inputTestVals, + inputConfig, cs); + OCIO_CHECK_EQUAL(name, std::string("sRGB - Texture")); + } + { + OCIO::ConstColorSpaceRcPtr cs = inputConfig->getColorSpace("very approx. standard RGB"); + const char * name = OCIO::ConfigUtils::findEquivalentColorSpace(fingerprints, inputTestVals, + inputConfig, cs); + OCIO_CHECK_EQUAL(name, std::string("")); + } + { + OCIO::ConstColorSpaceRcPtr cs = inputConfig->getColorSpace("pq display"); + const char * name = OCIO::ConfigUtils::findEquivalentColorSpace(fingerprints, inputTestVals, + inputConfig, cs); + OCIO_CHECK_EQUAL(name, std::string("Rec.2100-PQ - Display")); + } +} + +OCIO_ADD_TEST(ConfigUtils, sanitize_id_token) +{ + // Lower-case allowed characters pass through unchanged. + OCIO_CHECK_EQUAL(OCIO::ConfigUtils::SanitizeIDToken("abc.-_~/*#%^+()[]|09"), + std::string("abc.-_~/*#%^+()[]|09")); + + // Upper-case letters are lowered. + OCIO_CHECK_EQUAL(OCIO::ConfigUtils::SanitizeIDToken("ABCxyz"), std::string("abcxyz")); + + // Explicit character mappings. + OCIO_CHECK_EQUAL(OCIO::ConfigUtils::SanitizeIDToken(" \t\n\r"), std::string("____")); + OCIO_CHECK_EQUAL(OCIO::ConfigUtils::SanitizeIDToken("{}<>"), std::string("()()")); + OCIO_CHECK_EQUAL(OCIO::ConfigUtils::SanitizeIDToken(","), std::string(".")); + OCIO_CHECK_EQUAL(OCIO::ConfigUtils::SanitizeIDToken(";:"), std::string("||")); + OCIO_CHECK_EQUAL(OCIO::ConfigUtils::SanitizeIDToken("'\""), std::string("##")); + OCIO_CHECK_EQUAL(OCIO::ConfigUtils::SanitizeIDToken("\\"), std::string("/")); + + // Any other disallowed ASCII character becomes '*'. + OCIO_CHECK_EQUAL(OCIO::ConfigUtils::SanitizeIDToken("a!b@c?"), std::string("a*b*c*")); + + // A multi-byte UTF-8 codepoint becomes a single '^', regardless of how many bytes it takes + // to encode, so the result does not depend on the input's byte-level UTF-8 encoding. + OCIO_CHECK_EQUAL(OCIO::ConfigUtils::SanitizeIDToken("a\xC3\xA9z"), // "a" + 'é' (2 bytes) + "z" + std::string("a^z")); + OCIO_CHECK_EQUAL(OCIO::ConfigUtils::SanitizeIDToken("a\xE4\xB8\xADz"), // "a" + '中' (3 bytes) + "z" + std::string("a^z")); + OCIO_CHECK_EQUAL(OCIO::ConfigUtils::SanitizeIDToken("a\xF0\x9F\x98\x80z"), // "a" + emoji (4 bytes) + "z" + std::string("a^z")); + + // Malformed UTF-8 (a stray continuation/lead byte not followed by the expected continuation + // bytes) is still handled safely, one '^' per byte actually consumed. + OCIO_CHECK_EQUAL(OCIO::ConfigUtils::SanitizeIDToken("a\x80z"), std::string("a^z")); + OCIO_CHECK_EQUAL(OCIO::ConfigUtils::SanitizeIDToken("a\xC3z"), std::string("a^z")); + + // Empty string. + OCIO_CHECK_EQUAL(OCIO::ConfigUtils::SanitizeIDToken(""), std::string("")); +} + +OCIO_ADD_TEST(ConfigUtils, generate_local_id_for_color_space) +{ + auto cfg = OCIO::Config::Create(); + cfg->setName("My Studio!"); + + auto cs = OCIO::ColorSpace::Create(); + cs->setName("sRGB Encoded,Space"); + // This alias also requires sanitization (space, uppercase), so it does not qualify to be + // preferred over the color space name; see the alias-preference test below for that case. + cs->addAlias("My Alias"); + OCIO_CHECK_NO_THROW(cfg->addColorSpace(cs)); + + // Config name and color space name are each sanitized per Annex C: uppercase is lowered, + // spaces become '_', ',' becomes '.', and '!' (not allowed, no explicit mapping) becomes '*'. + std::string id; + OCIO_CHECK_NO_THROW(id = cfg->generateLocalIDForColorSpace("sRGB Encoded,Space")); + OCIO_CHECK_EQUAL(id, std::string("my_studio*:local:srgb_encoded.space")); + + // An alias resolves to the same ID, since the canonical color space name is used, not the + // caller-supplied string. + OCIO_CHECK_NO_THROW(id = cfg->generateLocalIDForColorSpace("My Alias")); + OCIO_CHECK_EQUAL(id, std::string("my_studio*:local:srgb_encoded.space")); + + // A config with no name set cannot generate a local ID. + auto cfgNoName = OCIO::Config::Create(); + OCIO_CHECK_NO_THROW(cfgNoName->addColorSpace(cs)); + OCIO_CHECK_THROW_WHAT(cfgNoName->generateLocalIDForColorSpace("sRGB Encoded,Space"), + OCIO::Exception, "May not generate a local interop ID if the config " + "name is empty."); + + // Null / empty color space name. + OCIO_CHECK_THROW_WHAT(cfg->generateLocalIDForColorSpace(nullptr), OCIO::Exception, + "srcColorSpaceName must not be null or empty"); + OCIO_CHECK_THROW_WHAT(cfg->generateLocalIDForColorSpace(""), OCIO::Exception, + "srcColorSpaceName must not be null or empty"); + + // Color space that does not exist in the config. + OCIO_CHECK_THROW_WHAT(cfg->generateLocalIDForColorSpace("does_not_exist"), OCIO::Exception, + "does not contain the requested color space"); + + // Renaming the config is reflected immediately. + cfg->setName("Other Studio"); + OCIO_CHECK_NO_THROW(id = cfg->generateLocalIDForColorSpace("sRGB Encoded,Space")); + OCIO_CHECK_EQUAL(id, std::string("other_studio:local:srgb_encoded.space")); + + // Disallow usage of config names for one of the OCIO configs for ACES that already + // contains interop IDs for all color spaces. Trying to avoid the situation where + // someone edits one of these configs but forgets to change the config name. This + // would result in a meaningless interop ID. + + // A config named like one of the built-in configs at version 4.0.0 or higher already has + // interop IDs populated, so it may not be used as the namespace of a locally-generated one. + cfg->setName("cg-config-v4.0.0_aces-v2.0_ocio-v2.5"); + OCIO_CHECK_THROW_WHAT(cfg->generateLocalIDForColorSpace("sRGB Encoded,Space"), + OCIO::Exception, "May not generate a local interop ID if the name matches"); + + // Don't allow anything that would sanitize to one of the disallowed config names. + cfg->setName("CG-config-v4.0.0_aces-v2.0_ocio-v2.5"); + OCIO_CHECK_THROW_WHAT(cfg->generateLocalIDForColorSpace("sRGB Encoded,Space"), + OCIO::Exception, "May not generate a local interop ID if the name matches"); + + // Example of a potential future built-in config name. + cfg->setName("studio-config-v5.1.2_aces-v2.2_ocio-v2.7"); + OCIO_CHECK_THROW_WHAT(cfg->generateLocalIDForColorSpace("sRGB Encoded,Space"), + OCIO::Exception, "May not generate a local interop ID if the name matches"); + + // Similar, but with more digits in the ACES version. + cfg->setName("studio-config-v4.1.2_aces-v2.2.1_ocio-v2.7"); + OCIO_CHECK_THROW_WHAT(cfg->generateLocalIDForColorSpace("sRGB Encoded,Space"), + OCIO::Exception, "May not generate a local interop ID if the name matches"); + + // A built-in-looking config name with a major version below 4 does not have interop IDs, so + // it is still legal to use as a local ID namespace. + cfg->setName("studio-config-v3.0.0_aces-v2.0_ocio-v2.4"); + OCIO_CHECK_NO_THROW(id = cfg->generateLocalIDForColorSpace("sRGB Encoded,Space")); + OCIO_CHECK_EQUAL(id, std::string("studio-config-v3.0.0_aces-v2.0_ocio-v2.4:local:" + "srgb_encoded.space")); + + // A name that starts like a built-in config and has a version 4.0.0+, but continues with + // something other than the exact "_aces-_ocio-" suffix, is not actually + // one of the reserved built-in config names and so remains legal. + cfg->setName("cg-config-v4.0.0_aces-v2.0_ocio-v2.5_custom"); + OCIO_CHECK_NO_THROW(id = cfg->generateLocalIDForColorSpace("sRGB Encoded,Space")); + OCIO_CHECK_EQUAL(id, std::string("cg-config-v4.0.0_aces-v2.0_ocio-v2.5_custom:local:srgb_encoded.space")); +} + +OCIO_ADD_TEST(ConfigUtils, generate_local_id_for_color_space_alias_preference) +{ + auto cfg = OCIO::Config::Create(); + cfg->setName("studio"); + + std::string id; + + // If the color space name already requires no sanitization, it is used as-is, even though + // an alias that also requires no sanitization is present. + { + auto cs = OCIO::ColorSpace::Create(); + cs->setName("aces_cg"); + cs->addAlias("some_other_name"); + OCIO_CHECK_NO_THROW(cfg->addColorSpace(cs)); + + OCIO_CHECK_NO_THROW(id = cfg->generateLocalIDForColorSpace("aces_cg")); + OCIO_CHECK_EQUAL(id, std::string("studio:local:aces_cg")); + } + + // If the name requires sanitization but an alias does not, the alias is preferred. + { + auto cs = OCIO::ColorSpace::Create(); + cs->setName("ACES CG"); + cs->addAlias("aces_cg_alias"); + OCIO_CHECK_NO_THROW(cfg->addColorSpace(cs)); + + OCIO_CHECK_NO_THROW(id = cfg->generateLocalIDForColorSpace("ACES CG")); + OCIO_CHECK_EQUAL(id, std::string("studio:local:aces_cg_alias")); + } + + // If several aliases would not require sanitization, the first one (in alias order) wins. + { + auto cs = OCIO::ColorSpace::Create(); + cs->setName("Linear Rec709"); + cs->addAlias("Needs Sanitizing"); + cs->addAlias("first_clean_alias"); + cs->addAlias("second_clean_alias"); + OCIO_CHECK_NO_THROW(cfg->addColorSpace(cs)); + + OCIO_CHECK_NO_THROW(id = cfg->generateLocalIDForColorSpace("Linear Rec709")); + OCIO_CHECK_EQUAL(id, std::string("studio:local:first_clean_alias")); + } + + // If the name and all aliases require sanitization, fall back to the sanitized name, as + // before this feature was added. + { + auto cs = OCIO::ColorSpace::Create(); + cs->setName("Camera Log,Space"); + cs->addAlias("Also Needs Sanitizing"); + OCIO_CHECK_NO_THROW(cfg->addColorSpace(cs)); + + OCIO_CHECK_NO_THROW(id = cfg->generateLocalIDForColorSpace("Camera Log,Space")); + OCIO_CHECK_EQUAL(id, std::string("studio:local:camera_log.space")); + } +} + +OCIO_ADD_TEST(ConfigUtils, find_color_space_for_id) +{ + auto cfg = OCIO::Config::Create(); + cfg->setName("MyStudio!"); + + auto acescg = OCIO::ColorSpace::Create(); + acescg->setName("ACEScg"); + acescg->addAlias("lin_ap1"); + OCIO_CHECK_NO_THROW(cfg->addColorSpace(acescg)); + + auto srgb = OCIO::ColorSpace::Create(); + srgb->setName("srgb"); + OCIO_CHECK_NO_THROW(cfg->addColorSpace(srgb)); + + auto foo = OCIO::ColorSpace::Create(); + foo->setName("foo"); + OCIO_CHECK_NO_THROW(cfg->addColorSpace(foo)); + + auto fancy = OCIO::ColorSpace::Create(); + fancy->setName("My Color Space!"); + fancy->addAlias("Fancy Alias"); + OCIO_CHECK_NO_THROW(cfg->addColorSpace(fancy)); + + // A different name that sanitizes to the same base as the color space above (both '!' and + // '?' sanitize to '*'). The color space added first wins. + auto fancyToo = OCIO::ColorSpace::Create(); + fancyToo->setName("My Color Space?"); + OCIO_CHECK_NO_THROW(cfg->addColorSpace(fancyToo)); + + OCIO::ConstColorSpaceRcPtr cs; + + // Mode 1: exact name and exact alias. + OCIO_CHECK_ASSERT(cs = cfg->findColorSpaceForID("ACEScg")); + OCIO_CHECK_EQUAL(std::string(cs->getName()), "ACEScg"); + OCIO_CHECK_ASSERT(cs = cfg->findColorSpaceForID("lin_ap1")); + OCIO_CHECK_EQUAL(std::string(cs->getName()), "ACEScg"); + + // Mode 2: one namespace, falls back to the base name. + OCIO_CHECK_ASSERT(cs = cfg->findColorSpaceForID("somestudio:acescg")); + OCIO_CHECK_EQUAL(std::string(cs->getName()), "ACEScg"); + + // Two-namespace ID whose one-level-stripped remainder is not itself a valid name/alias: + // no recursive stripping, so this must not match "ACEScg". + OCIO_CHECK_ASSERT(!cfg->findColorSpaceForID("outer:inner:acescg")); + + // Mode 3 (local): outer namespace matches this config's sanitized name. + OCIO_CHECK_ASSERT(cs = cfg->findColorSpaceForID("mystudio*:local:acescg")); + OCIO_CHECK_EQUAL(std::string(cs->getName()), "ACEScg"); + + // Mode 3 negative: outer namespace does not match the config's name. + OCIO_CHECK_ASSERT(!cfg->findColorSpaceForID("othername:local:acescg")); + OCIO_CHECK_ASSERT(!cfg->findColorSpaceForID(":local:acescg")); + OCIO_CHECK_ASSERT(!cfg->findColorSpaceForID("::local:acescg")); + + // Mode 3 with a color space whose name must be sanitized in order to match the ID base. + // (The first of the two color spaces that sanitize to this base is the one returned.) + OCIO_CHECK_ASSERT(cs = cfg->findColorSpaceForID("mystudio*:local:my_color_space*")); + OCIO_CHECK_EQUAL(std::string(cs->getName()), "My Color Space!"); + + // Aliases are searched as well, and are also compared in their sanitized form. + OCIO_CHECK_ASSERT(cs = cfg->findColorSpaceForID("mystudio*:local:fancy_alias")); + OCIO_CHECK_EQUAL(std::string(cs->getName()), "My Color Space!"); + + // In this case, the outer namespace is illegal since it's not lower-case. + // However, the base name would match regardless of case, so it's more predictable + // to allow this to match too. + OCIO_CHECK_ASSERT(cs = cfg->findColorSpaceForID("myStudio*:local:fancy_Alias")); + OCIO_CHECK_EQUAL(std::string(cs->getName()), "My Color Space!"); + + // The sanitized comparison is only used for the local mode fall-back, so the sanitized + // base does not match on its own or with a namespace that is not this config's name. + OCIO_CHECK_ASSERT(!cfg->findColorSpaceForID("my_color_space*")); + OCIO_CHECK_ASSERT(!cfg->findColorSpaceForID("othername:local:my_color_space*")); + + // Empty inner namespace: "my-studio::srgb" strips to ":srgb", which must not match "srgb". + OCIO_CHECK_ASSERT(!cfg->findColorSpaceForID("my-studio::srgb")); + + // Use of "local" as an ordinary namespace is not allowed, it may only be used as a keyword + // for the local (mode 3) form of an ID. + OCIO_CHECK_ASSERT(!cfg->findColorSpaceForID("local:foo")); + + // Garbage input: extra colons, empty string, and null all resolve to "not found" without + // throwing. + OCIO_CHECK_ASSERT(!cfg->findColorSpaceForID("a:b:c:d")); + OCIO_CHECK_ASSERT(!cfg->findColorSpaceForID("")); + OCIO_CHECK_ASSERT(!cfg->findColorSpaceForID(nullptr)); + + // Round-trip: an ID generated by this config is resolved back to the same color space. + const std::string generated = cfg->generateLocalIDForColorSpace("ACEScg"); + OCIO_CHECK_ASSERT(cs = cfg->findColorSpaceForID(generated.c_str())); + OCIO_CHECK_EQUAL(std::string(cs->getName()), "ACEScg"); + + // Round-trip for a color space whose name requires more than case sanitizing. + const std::string generatedFancy = cfg->generateLocalIDForColorSpace("My Color Space!"); + OCIO_CHECK_EQUAL(generatedFancy, std::string("mystudio*:local:my_color_space*")); + OCIO_CHECK_ASSERT(cs = cfg->findColorSpaceForID(generatedFancy.c_str())); + OCIO_CHECK_EQUAL(std::string(cs->getName()), "My Color Space!"); + + // A config with no name never matches a local mode ID, not even one with an empty outer + // namespace (such an ID could not have been generated by an unnamed config). + auto cfgNoName = OCIO::Config::Create(); + OCIO_CHECK_NO_THROW(cfgNoName->addColorSpace(foo)); + OCIO_CHECK_ASSERT(cs = cfgNoName->findColorSpaceForID("foo")); + OCIO_CHECK_ASSERT(!cfgNoName->findColorSpaceForID(":local:foo")); + OCIO_CHECK_ASSERT(!cfgNoName->findColorSpaceForID("mystudio*:local:foo")); } diff --git a/tests/python/ColorSpaceTest.py b/tests/python/ColorSpaceTest.py index 6fae105b6..fd1786e47 100644 --- a/tests/python/ColorSpaceTest.py +++ b/tests/python/ColorSpaceTest.py @@ -1110,4 +1110,44 @@ def check_processor_inv(self, p): self.assertEqual( str(cm.exception), "Could not find destination color space ''." + ) + + + # + # Test LocateBuiltinColorSpace. (This is the inverse of IdentifyBuiltinColorSpace.) + # + + editableCfg.setRole("aces_interchange", "ref_cs") + editableCfg.setRole("cie_xyz_d65_interchange", "CIE-XYZ-D65") + editableCfg.setInactiveColorSpaces("") + + csname = OCIO.Config.LocateBuiltinColorSpace(editableCfg, "ACES cg", builtinConfig) + self.assertEqual(csname, "ACEScg") + + csname = OCIO.Config.LocateBuiltinColorSpace(editableCfg, "Texture -- sRGB", builtinConfig) + self.assertEqual(csname, "sRGB Encoded Rec.709 (sRGB)") + + csname = OCIO.Config.LocateBuiltinColorSpace(editableCfg, "not sRGB", builtinConfig) + self.assertEqual(csname, "ACEScct") + + # Display-referred color spaces work if the display interchange role is present. + csname = OCIO.Config.LocateBuiltinColorSpace(editableCfg, "sRGB - Display CS", builtinConfig) + self.assertEqual(csname, "sRGB - Display") + + # Roles may be used for the source color space name and inactive color spaces + # are searched. (The scene_linear role is set to the reference space.) + editableCfg.setInactiveColorSpaces("ref_cs") + csname = OCIO.Config.LocateBuiltinColorSpace(editableCfg, "scene_linear", builtinConfig) + self.assertEqual(csname, "ACES2065-1") + + # A data space returns an empty string. + csname = OCIO.Config.LocateBuiltinColorSpace(editableCfg, "raw", builtinConfig) + self.assertEqual(csname, "") + + # Check what happens if the source color space doesn't exist. + with self.assertRaises(OCIO.Exception) as cm: + csname = OCIO.Config.LocateBuiltinColorSpace(editableCfg, "Foo", builtinConfig) + self.assertEqual( + str(cm.exception), + "LocateBuiltinColorSpace: Source config does not contain the requested color space: Foo." ) \ No newline at end of file diff --git a/tests/python/ConfigTest.py b/tests/python/ConfigTest.py index 62c2c7d04..03826ec85 100644 --- a/tests/python/ConfigTest.py +++ b/tests/python/ConfigTest.py @@ -798,6 +798,111 @@ def test_canonical_name(self): self.assertEqual(cfg.getCanonicalName('Alias1'), 'nt1') self.assertEqual(cfg.getCanonicalName('Test1'), 'nt1') + def test_generate_local_id_for_color_space(self): + # Test the generateLocalIDForColorSpace method. + + cfg = OCIO.Config() + cfg.setName('My Studio!') + + cs = OCIO.ColorSpace(name='sRGB Encoded,Space', aliases=['My Alias']) + cfg.addColorSpace(cs) + + # Config name and color space name are each sanitized per Annex C: uppercase is + # lowered, spaces become '_', ',' becomes '.', and '!' becomes '*'. + self.assertEqual(cfg.generateLocalIDForColorSpace('sRGB Encoded,Space'), + 'my_studio*:local:srgb_encoded.space') + + # An alias (that requires sanitization) resolves to the same ID, since the + # canonical color space name is used. + self.assertEqual(cfg.generateLocalIDForColorSpace('My Alias'), + 'my_studio*:local:srgb_encoded.space') + + # A config with no name set cannot generate a local ID. + cfgNoName = OCIO.Config() + cfgNoName.addColorSpace(cs) + with self.assertRaises(OCIO.Exception): + cfgNoName.generateLocalIDForColorSpace('sRGB Encoded,Space') + + # Color space that does not exist in the config. + with self.assertRaises(OCIO.Exception): + cfg.generateLocalIDForColorSpace('does_not_exist') + + # Renaming the config is reflected immediately (no stale caching). + cfg.setName('Other Studio') + self.assertEqual(cfg.generateLocalIDForColorSpace('sRGB Encoded,Space'), + 'other_studio:local:srgb_encoded.space') + + def test_find_color_space_for_id(self): + # Test the findColorSpaceForID method. + + cfg = OCIO.Config() + cfg.setName('MyStudio!') + + cfg.addColorSpace(OCIO.ColorSpace(name='ACEScg', aliases=['lin_ap1'])) + cfg.addColorSpace(OCIO.ColorSpace(name='srgb')) + cfg.addColorSpace(OCIO.ColorSpace(name='foo')) + + # A color space whose name and alias differ from their sanitized form by more than + # just case, so they are only found via the local mode fall-back. + cfg.addColorSpace(OCIO.ColorSpace(name='My Color Space!', aliases=['Fancy Alias'])) + + # Mode 1: exact name and exact alias. + self.assertEqual(cfg.findColorSpaceForID('acescg').getName(), 'ACEScg') + self.assertEqual(cfg.findColorSpaceForID('lin_ap1').getName(), 'ACEScg') + + # Mode 2: one namespace, falls back to the base name. + self.assertEqual(cfg.findColorSpaceForID('somestudio:acescg').getName(), 'ACEScg') + + # Two-namespace ID whose one-level-stripped remainder is not itself a valid name/alias: + # no recursive stripping, so this must not match "acescg". + self.assertIsNone(cfg.findColorSpaceForID('outer:inner:acescg')) + + # Mode 3 (local): outer namespace matches this config's sanitized name. + self.assertEqual(cfg.findColorSpaceForID('mystudio*:local:acescg').getName(), 'ACEScg') + + # Mode 3 negative: outer namespace does not match the config's name. + self.assertIsNone(cfg.findColorSpaceForID('othername:local:acescg')) + + # Mode 3 with names and aliases that must be sanitized to match the ID base. + self.assertEqual(cfg.findColorSpaceForID('mystudio*:local:my_color_space*').getName(), + 'My Color Space!') + self.assertEqual(cfg.findColorSpaceForID('mystudio*:local:fancy_alias').getName(), + 'My Color Space!') + + # The sanitized comparison is only used for the local mode fall-back. + self.assertIsNone(cfg.findColorSpaceForID('my_color_space*')) + self.assertIsNone(cfg.findColorSpaceForID('othername:local:my_color_space*')) + + # Empty inner namespace: "my-studio::srgb" strips to ":srgb", which must not match "srgb". + self.assertIsNone(cfg.findColorSpaceForID('my-studio::srgb')) + + # Use of "local" as an ordinary namespace is not allowed, it may only be used as a keyword + # for the local (mode 3) form of an ID. + self.assertIsNone(cfg.findColorSpaceForID('local:foo')) + + # Garbage input: extra colons, empty string, and None all resolve to "not found" + # without raising. + self.assertIsNone(cfg.findColorSpaceForID('a:b:c:d')) + self.assertIsNone(cfg.findColorSpaceForID('')) + self.assertIsNone(cfg.findColorSpaceForID(None)) + + # Round-trip: an ID generated by this config is resolved back to the same color space. + generated = cfg.generateLocalIDForColorSpace('ACEScg') + self.assertEqual(cfg.findColorSpaceForID(generated).getName(), 'ACEScg') + + # Round-trip for a color space whose name requires more than case sanitizing. + generated = cfg.generateLocalIDForColorSpace('My Color Space!') + self.assertEqual(generated, 'mystudio*:local:my_color_space*') + self.assertEqual(cfg.findColorSpaceForID(generated).getName(), 'My Color Space!') + + # A config with no name never matches a local mode ID, not even one with an empty + # outer namespace (such an ID could not have been generated by an unnamed config). + cfgNoName = OCIO.Config() + cfgNoName.addColorSpace(OCIO.ColorSpace(name='foo')) + self.assertEqual(cfgNoName.findColorSpaceForID('foo').getName(), 'foo') + self.assertIsNone(cfgNoName.findColorSpaceForID(':local:foo')) + self.assertIsNone(cfgNoName.findColorSpaceForID('mystudio*:local:foo')) + def test_virtual_display(self): # Test platform agnostic virtual display interface.