From 26dc20e1df0aa6da8190f7584d49d606f53ea6c4 Mon Sep 17 00:00:00 2001 From: Doug Walker Date: Thu, 10 Sep 2026 00:28:26 -0400 Subject: [PATCH] Implement display and view aliases Signed-off-by: Doug Walker --- docs/guides/authoring/displays_views.rst | 19 + include/OpenColorIO/OpenColorIO.h | 109 ++- src/OpenColorIO/Config.cpp | 408 ++++++++- src/OpenColorIO/OCIOYaml.cpp | 32 + src/OpenColorIO/ViewTransform.cpp | 75 ++ .../apphelpers/LegacyViewingPipeline.cpp | 41 +- .../transforms/DisplayViewTransform.cpp | 70 +- src/apps/ocioconvert/main.cpp | 5 +- src/bindings/python/PyConfig.cpp | 13 + src/bindings/python/PyViewTransform.cpp | 67 +- tests/cpu/Config_tests.cpp | 786 +++++++++++++++++- tests/cpu/ViewTransform_tests.cpp | 71 ++ .../LegacyViewingPipeline_tests.cpp | 132 +++ .../transforms/DisplayViewTransform_tests.cpp | 373 +++++++++ tests/python/ConfigTest.py | 123 +++ tests/python/ViewTransformTest.py | 64 ++ 16 files changed, 2326 insertions(+), 62 deletions(-) diff --git a/docs/guides/authoring/displays_views.rst b/docs/guides/authoring/displays_views.rst index 1cdc8c51bf..adede1c874 100644 --- a/docs/guides/authoring/displays_views.rst +++ b/docs/guides/authoring/displays_views.rst @@ -132,6 +132,25 @@ A View Transform may use the following keys: .. TODO: Good spot for an example in a future revision. + +``use_display_view_aliases`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Optional. Activates aliases for display and view names. + +By default, the arguments to DisplayViewTransform must be the exact strings found +in the display / view section of the config. However, if ``ocio_profile_version`` +is 2.6 or higher, ``use_display_view_aliases`` may be set to true. This allows a +display to be referred to by the name or aliases of its corresponding display +ColorSpace and the view to be referred to by the name or aliases of its +corresponding ViewTransform. This config-level attribute defaults to false and +must be omitted from the config file if its value is not "true". + +.. code-block:: yaml + + use_display_view_aliases: true + + ``default_view_transform`` ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/include/OpenColorIO/OpenColorIO.h b/include/OpenColorIO/OpenColorIO.h index ad294fc805..8e10024d40 100644 --- a/include/OpenColorIO/OpenColorIO.h +++ b/include/OpenColorIO/OpenColorIO.h @@ -656,10 +656,9 @@ class OCIOEXPORT Config void removeColorSpace(const char * name); /** - * Return true if the color space is used by a transform, a role, or a look. - * - * \note - * Name must be the canonical name. + * Return true if the color space is used by a transform, a role, a look, a (display, view) + * pair, or a file rule. The argument may be either an alias or the canonical name. While + * searching the config, aliases are always resolve to their canonical names for comparison. */ bool isColorSpaceUsed(const char * name) const noexcept; @@ -918,6 +917,8 @@ class OCIOEXPORT Config /** * Returns the colorspace attribute of the (display, view) pair. * (Note that this may be either a color space or a display color space.) + * See \ref Config::getResolvedDisplayViewColorSpaceName to first resolve + * any display or view aliases. */ const char * getDisplayViewColorSpaceName(const char * display, const char * view) const; /// Returns the looks attribute of a (display, view) pair. @@ -985,6 +986,85 @@ class OCIOEXPORT Config /// Clear all the displays. void clearDisplays(); + /** + * Methods related to display and view aliases. + * + */ + + /** + * \brief This property on the Config object allows config authors to use aliases for + * display or view names. This feature is off by default. + * + * Corresponds to the "use_display_view_aliases" config file attribute, which is only + * written to the file when true. Requires config version 2.6 or higher (validation + * will fail if this is enabled on an older config). + */ + bool getUseDisplayViewAliases() const noexcept; + void setUseDisplayViewAliases(bool enabled) noexcept; + + /** + * \brief Resolve display name aliases. + * + * If the argument does not match an existing display, a fallback checks if getColorSpace + * returns a display color space. If so, it checks to see if there is a display whose + * name matches that color space name or one of its aliases. + * + * This fallback is only performed if \ref Config::getUseDisplayViewAliases is true. + * + * Returns "" if no display can be found, even with the fallback. + */ + const char * getCanonicalDisplayName(const char * displayName) const; + + /** + * \brief Resolve view name aliases. + * + * If the arguments do not directly match an existing (display, view) pair, a fallback + * checks if getViewTransform or getNamedTransform returns a result for viewName. If so, + * it checks if the display has a view whose view_transform matches the name or an alias + * of that transform. + * + * This fallback is only performed if \ref Config::getUseDisplayViewAliases is true. + * + * The displayName is first resolved via \ref Config::getCanonicalDisplayName. + * + * Returns "" if no display and view can be found, even with the fallback, or if the + * arguments are null or empty. + */ + const char * getCanonicalViewName(const char * displayName, const char * viewName) const; + + /** + * \brief Returns the name of the color space that a (display, view) pair uses. + * + * This is similar to \ref Config::getDisplayViewColorSpaceName, but it first attempts + * to resolve displayName and viewName (which could be aliases) to their canonical names. + * And unlike that function, the displayName may not be empty. The alias resolution is + * gated by \ref Config::getUseDisplayViewAliases. + * + * In addition, if the display_colorspace of a shared view is , that + * is resolved to the name of the view's display. + * + * Note that, as with getDisplayViewColorSpaceName, the returned name may be that of a + * named transform rather than a color space (this is allowed for views that have no + * view_transform). + * + * Returns either the canonical name of the view's color space or, if that does not + * find a result, the raw color space string (which would likely be used in an error + * message). If the (display, view) pair cannot even be resolved, it returns "". + */ + const char * getResolvedDisplayViewColorSpaceName(const char * displayName, + const char * viewName) const; + + /** + * \brief Return the description of the display color space associated with displayName. + * + * If displayName matches the canonical name of a display color space, its description is + * returned. If \ref Config::getUseDisplayViewAliases is true, the search is broadened to + * include display color spaces that have displayName as an alias. + * + * Returns "" if no such display color space can be found. + */ + const char * getDisplayDescription(const char * displayName) const; + /** * Methods related to the Virtual Display. * @@ -2555,14 +2635,35 @@ class OCIOEXPORT ViewTransform ViewTransformRcPtr createEditableCopy() const; const char * getName() const noexcept; + /// \see ColorSpace::setName void setName(const char * name) noexcept; + /** + * \note + * ViewTransform aliases are available in config file versions 2.6 or higher. They + * are availaable regardless of how \ref Config::getUseDisplayViewAliases is set. + */ + + /// \see ColorSpace::getNumAliases + size_t getNumAliases() const noexcept; + /// \see ColorSpace::getAlias + const char * getAlias(size_t idx) const noexcept; + /// \see ColorSpace::hasAlias + bool hasAlias(const char * alias) const noexcept; + /// \see ColorSpace::addAlias + void addAlias(const char * alias) noexcept; + /// \see ColorSpace::removeAlias + void removeAlias(const char * alias) noexcept; + /// \see ColorSpace::clearAliases + void clearAliases() noexcept; + /// \see ColorSpace::getFamily const char * getFamily() const noexcept; /// \see ColorSpace::setFamily void setFamily(const char * family); const char * getDescription() const noexcept; + /// \see ColorSpace::setDescription void setDescription(const char * description); /** diff --git a/src/OpenColorIO/Config.cpp b/src/OpenColorIO/Config.cpp index a8f25d5cad..2900984cbe 100644 --- a/src/OpenColorIO/Config.cpp +++ b/src/OpenColorIO/Config.cpp @@ -248,7 +248,7 @@ static constexpr unsigned LastSupportedMajorVersion = OCIO_VERSION_MAJOR; // For each major version keep the most recent minor. static const unsigned int LastSupportedMinorVersion[] = {0, // Version 1 - 5 // Version 2 + 6 // Version 2 }; } // namespace @@ -321,6 +321,7 @@ class Config::Impl // Misc std::vector m_defaultLumaCoefs; bool m_strictParsing; + bool m_useDisplayViewAliases{ false }; mutable Validation m_validation; mutable std::string m_validationtext; @@ -440,6 +441,7 @@ class Config::Impl m_defaultViewTransform = rhs.m_defaultViewTransform; m_defaultLumaCoefs = rhs.m_defaultLumaCoefs; m_strictParsing = rhs.m_strictParsing; + m_useDisplayViewAliases = rhs.m_useDisplayViewAliases; m_validation = rhs.m_validation; m_validationtext = rhs.m_validationtext; @@ -539,6 +541,15 @@ class Config::Impl } } + // Not found by name, so look for an alias. + for (const auto & vt : m_viewTransforms) + { + if (vt->hasAlias(name)) + { + return vt; + } + } + return ConstViewTransformRcPtr(); } @@ -1955,7 +1966,8 @@ void Config::validate() const if (!getImpl()->m_defaultViewTransform.empty()) { const auto vt = getDefaultSceneToDisplayViewTransform(); - if (!vt || !StringUtils::Compare(vt->getName(), getImpl()->m_defaultViewTransform)) + if (!vt || !(StringUtils::Compare(vt->getName(), getImpl()->m_defaultViewTransform) || + vt->hasAlias(getImpl()->m_defaultViewTransform.c_str()))) { std::ostringstream os; os << "Config failed validation. Default view transform is defined as: '"; @@ -2674,6 +2686,21 @@ bool Config::isColorSpaceUsed(const char * name) const noexcept if (!name || !*name) return false; + // Resolve any aliases, so both source and target are comparing the canonical names. + auto resolveName = [this](const char * csName) -> std::string + { + const char * canonicalName = getCanonicalName(csName); + return (canonicalName && *canonicalName) ? std::string(canonicalName) + : std::string(csName ? csName : ""); + }; + + const std::string searchName = resolveName(name); + + auto isMatch = [&resolveName, &searchName](const char * csName) + { + return csName && *csName && StringUtils::Compare(resolveName(csName), searchName); + }; + // Check for all color spaces, looks and view transforms. ConstTransformVec allTransforms; @@ -2688,7 +2715,7 @@ bool Config::isColorSpaceUsed(const char * name) const noexcept for (const auto & csName : colorSpaceNames) { - if (0 == Platform::Strcasecmp(name, csName.c_str())) + if (isMatch(csName.c_str())) { return true; } @@ -2701,7 +2728,7 @@ bool Config::isColorSpaceUsed(const char * name) const noexcept { const char * roleName = getRoleName(idx); const char * csName = LookupRole(getImpl()->m_roles, roleName); - if (0 == Platform::Strcasecmp(csName, name)) + if (isMatch(csName)) { return true; } @@ -2712,7 +2739,7 @@ bool Config::isColorSpaceUsed(const char * name) const noexcept for (const auto & view : getImpl()->m_sharedViews) { const char * csName = view.m_colorspace.c_str(); - if (0 == Platform::Strcasecmp(csName, name)) + if (isMatch(csName)) { return true; } @@ -2727,7 +2754,7 @@ bool Config::isColorSpaceUsed(const char * name) const noexcept { const char * viewName = view.m_name.c_str(); const char * csName = getDisplayViewColorSpaceName(dispName, viewName); - if (0 == Platform::Strcasecmp(csName, name)) + if (isMatch(csName)) { return true; } @@ -2740,7 +2767,8 @@ bool Config::isColorSpaceUsed(const char * name) const noexcept if (!((*viewIt).m_viewTransform.empty()) && (*viewIt).useDisplayNameForColorspace()) { - if (0 == Platform::Strcasecmp(dispName, name)) + // The view uses the display color space named after the display. + if (isMatch(dispName)) { return true; } @@ -2757,7 +2785,7 @@ bool Config::isColorSpaceUsed(const char * name) const noexcept const char * lookName = getLookNameByIndex(idx); ConstLookRcPtr l = getLook(lookName); - if (0 == Platform::Strcasecmp(l->getProcessSpace(), name)) + if (isMatch(l->getProcessSpace())) { return true; } @@ -2771,7 +2799,7 @@ bool Config::isColorSpaceUsed(const char * name) const noexcept for (size_t idx = 0; idx < numRules; ++idx) { const char * csName = rules->getColorSpace(idx); - if (0 == Platform::Strcasecmp(csName, name)) + if (isMatch(csName)) { return true; } @@ -3608,6 +3636,8 @@ const char * Config::getDisplayViewTransformName(const char * display, const cha return viewPtr->m_viewTransform.c_str(); } +// NB: See getResolvedDisplayViewColorSpaceName for a version of this that resolves display aliases. + const char * Config::getDisplayViewColorSpaceName(const char * display, const char * view) const { const View * viewPtr = getImpl()->getView(display, view); @@ -3823,6 +3853,275 @@ void Config::clearDisplays() getImpl()->resetCacheIDs(); } +/////////////////////////////////////////////////////////////////////////// +// Unlike the above functions, these are set up to work with display +// and view aliases. + +bool Config::getUseDisplayViewAliases() const noexcept +{ + return getImpl()->m_useDisplayViewAliases; +} + +void Config::setUseDisplayViewAliases(bool enabled) noexcept +{ + getImpl()->m_useDisplayViewAliases = enabled; + + AutoMutex lock(getImpl()->m_cacheidMutex); + getImpl()->resetCacheIDs(); +} + +const char * Config::getCanonicalDisplayName(const char * displayName) const +{ + if (!displayName || !*displayName) + { + return ""; + } + + // This is the normal case, there is a display called displayName. + DisplayMap::const_iterator iter = FindDisplay(getImpl()->m_displays, displayName); + if (iter != getImpl()->m_displays.end()) + { + return iter->first.c_str(); + } + + // Use of the fallback requires a config-level opt-in, which is false by default. + if (!getImpl()->m_useDisplayViewAliases) + { + return ""; + } + + // Normally, getColorSpace searches roles, but that is not the intended use-case and + // could be confusing, so don't resolve against them. + if (hasRole(displayName)) + { + return ""; + } + + // Look for a display color space that has displayName as its name or an alias. + ConstColorSpaceRcPtr cs = getColorSpace(displayName); + + // Only consider display-referred color spaces. + if (!cs || cs->getReferenceSpaceType() == REFERENCE_SPACE_SCENE) + { + return ""; + } + + iter = FindDisplay(getImpl()->m_displays, cs->getName()); + if (iter != getImpl()->m_displays.end()) + { + // A display exists with the name of the color space. In this case, displayName + // was an alias of the color space. + return iter->first.c_str(); + } + + const size_t numAliases = cs->getNumAliases(); + for (size_t i = 0; i < numAliases; ++i) + { + iter = FindDisplay(getImpl()->m_displays, cs->getAlias(i)); + if (iter != getImpl()->m_displays.end()) + { + // A display exists with the name of an alias of the color space. In this case, + // displayName was either the color space name or one of the other aliases. + return iter->first.c_str(); + } + } + + return ""; +} + +const char * Config::getResolvedDisplayViewColorSpaceName(const char * display, + const char * view) const +{ + if (!display || !*display || !view || !*view) + { + return ""; + } + + const char * resolvedDisplay = getCanonicalDisplayName(display); + if (!resolvedDisplay || !*resolvedDisplay) + { + return ""; + } + + const char * resolvedView = getCanonicalViewName(resolvedDisplay, view); + if (!resolvedView || !*resolvedView) + { + return ""; + } + + const View * viewPtr = getImpl()->getView(resolvedDisplay, resolvedView); + if (!viewPtr) return ""; + + // A shared view that has a view transform may set its display_colorspace to + // , meaning that the display color space named after the display is used. + const char * csName = viewPtr->useDisplayNameForColorspace() ? resolvedDisplay + : viewPtr->m_colorspace.c_str(); + + // Return the canonical name, since csName may be a role or an alias. (Note that the view's + // colorspace attribute is allowed to name a named transform rather than a color space, if + // the view has no view_transform, and getCanonicalName handles both.) If it doesn't name + // anything at all, return it as-is so callers can report what they were unable to find. + const char * canonicalName = getCanonicalName(csName); + return (canonicalName && *canonicalName) ? canonicalName : csName; +} + +const char * Config::getDisplayDescription(const char * display) const +{ + if (!display || !*display) + { + return ""; + } + + // Getting a description requires that the display name matches the name or alias of + // a display color space in the config. + ConstColorSpaceRcPtr cs = getColorSpace(display); + + // Only support display color spaces. + if (!cs || cs->getReferenceSpaceType() != REFERENCE_SPACE_DISPLAY) + { + return ""; + } + + // A display color space named exactly "display" always works. If it was only found via + // one of its aliases, the fallback is opt-in. + if (!StringUtils::Compare(cs->getName(), display) && !getImpl()->m_useDisplayViewAliases) + { + return ""; + } + + return cs->getDescription(); +} + +const char * Config::getCanonicalViewName(const char * displayName, const char * viewName) const +{ + if (!displayName || !*displayName || !viewName || !*viewName) + { + return ""; + } + + // Resolve the displayName first, since it may itself be an alias. (The display + // resolution function's fallback is likewise gated by m_useDisplayViewAliases.) + const char * resolvedDisplay = displayName; + const char * canonicalDisplay = getCanonicalDisplayName(displayName); + if (canonicalDisplay && *canonicalDisplay) + { + resolvedDisplay = canonicalDisplay; + } + + // This is the normal case, the display has a view named viewName. + if (getImpl()->getView(resolvedDisplay, viewName)) + { + return viewName; + } + + // Use of the fallback requires a config-level opt-in, which is false by default. + if (!getImpl()->m_useDisplayViewAliases) + { + return ""; + } + + // The view's view_transform attribute may point to a ViewTransform or NamedTransform, + // both of which support alias names. Resolve both source and target to the canonical + // name for all comparisons. + auto resolveVTOrNT = [this](const std::string & name) -> std::string + { + if (name.empty()) + { + return std::string(); + } + // If there is both a VT and NT with that name, the ViewTransform takes priority. + ConstViewTransformRcPtr vt = getViewTransform(name.c_str()); + if (vt) + { + return std::string(vt->getName()); + } + ConstNamedTransformRcPtr nt = getNamedTransform(name.c_str()); + if (nt) + { + return std::string(nt->getName()); + } + return std::string(); + }; + + // Get the canonical name of a ViewTransform or NamedTransform responding to viewName. + const std::string transformName = resolveVTOrNT(viewName); + if (transformName.empty()) + { + // There are no ViewTransforms or NamedTransforms that respond to viewName as + // either a name or alias. No fallbacks are possible. + return ""; + } + + DisplayMap::const_iterator iter = FindDisplay(getImpl()->m_displays, resolvedDisplay); + if (iter == getImpl()->m_displays.end()) + { + // The requested display does not exist. + return ""; + } + + // Consider both display-defined views and shared views used by this display, and both + // active and inactive views. + const ViewPtrVec views = getImpl()->getViews(iter->second); + + // If there is a display color space corresponding to this display, get its pointer. As with + // Config::getCanonicalDisplayName's own alias-based resolution, a match found via one of the + // color space's aliases (rather than its own current name) is accepted. + ConstColorSpaceRcPtr resolvedDisplayCs = getColorSpace(resolvedDisplay); + + // There's nothing that prevents there from being a scene-referred color space that matches + // a display name. In that case, don't try to use this color space to validate the + // display_colorspace of the view candidates. + if (!resolvedDisplayCs || resolvedDisplayCs->getReferenceSpaceType() != REFERENCE_SPACE_DISPLAY) + { + resolvedDisplayCs = ConstColorSpaceRcPtr(); + } + + // Iterate over all views for this display, testing each candidate. + for (const auto * candidate : views) + { + // Does this candidate's view_transform (which may be a NT) resolve to the same one as + // viewName? If not, this candidate is unrelated to viewName and can be skipped outright. + const std::string resolvedCandidate = resolveVTOrNT(candidate->m_viewTransform); + if (resolvedCandidate.empty() || !StringUtils::Compare(resolvedCandidate, transformName)) + { + continue; + } + + // At this point, we've found a view in this display where its view_transform + // corresponds to viewName (either by name or alias). However, don't return it + // if it uses a display_colorspace that does not match the display color space + // corresponding to this display, if one exists. + + if (candidate->useDisplayNameForColorspace()) + { + // The candidate is using for its display_colorspace, so it + // goes with the display, by definition. + return candidate->m_name.c_str(); + } + + if (!resolvedDisplayCs) + { + // There is no display color space in the config corresponding to this display, + // so regardless of what the candidate's display_colorspace is, there is + // nothing to check it against. Accept the match. + return candidate->m_name.c_str(); + } + + // There is a display color space for this display, so only accept this candidate + // if its display_colorspace resolves to that same color space (by name or alias). + // Otherwise keep looking -- another candidate might still satisfy this check. + ConstColorSpaceRcPtr candidateCs = getColorSpace(candidate->m_colorspace.c_str()); + if (candidateCs && StringUtils::Compare(candidateCs->getName(), resolvedDisplayCs->getName())) + { + return candidate->m_name.c_str(); + } + } + + return ""; +} + +/////////////////////////////////////////////////////////////////////////// + bool Config::hasVirtualView(const char * viewName) const { const char * cs = getVirtualDisplayViewColorSpaceName(viewName); @@ -4140,6 +4439,8 @@ int Config::instantiateDisplayFromICCProfile(const char * ICCProfileFilepath) return getImpl()->instantiateDisplay("", monitorDescription, ICCProfileFilepath); } +/////////////////////////////////////////////////////////////////////////// + void Config::setActiveDisplays(const char * displays) { getImpl()->m_activeDisplays.clear(); @@ -4340,6 +4641,8 @@ int Config::getNumActiveViews() const return static_cast(getImpl()->m_activeViews.size()); } +/////////////////////////////////////////////////////////////////////////// + int Config::getNumDisplaysAll() const noexcept { return static_cast(getImpl()->m_displays.size()); @@ -4617,6 +4920,36 @@ void Config::addViewTransform(const ConstViewTransformRcPtr & viewTransform) const std::string namelower = StringUtils::Lower(name); + // The name and aliases must not collide with a different, existing view transform. + for (const auto & vt : getImpl()->m_viewTransforms) + { + if (StringUtils::Lower(vt->getName()) == namelower) + { + continue; + } + + if (vt->hasAlias(name.c_str())) + { + std::ostringstream os; + os << "Cannot add '" << name << "' view transform, existing view transform '"; + os << vt->getName() << "' is using this name as an alias."; + throw Exception(os.str().c_str()); + } + + const size_t numAliases = viewTransform->getNumAliases(); + for (size_t aidx = 0; aidx < numAliases; ++aidx) + { + const char * alias = viewTransform->getAlias(aidx); + if (StringUtils::Compare(vt->getName(), alias) || vt->hasAlias(alias)) + { + std::ostringstream os; + os << "Cannot add '" << name << "' view transform, it has an alias '" << alias; + os << "' that is already used by view transform '" << vt->getName() << "'."; + throw Exception(os.str().c_str()); + } + } + } + bool addIt = true; // If the view transform exists, replace it. @@ -5133,15 +5466,8 @@ ConstProcessorRcPtr Config::GetProcessorFromConfigs(const ConstContextRcPtr & sr "the source color space."); } - const char* csName = dstConfig->getDisplayViewColorSpaceName(dstDisplay, dstView); - const char* displayColorSpaceName = View::UseDisplayName(csName) ? dstDisplay : csName; - ConstColorSpaceRcPtr displayColorSpace = dstConfig->getColorSpace(displayColorSpaceName); - if (!displayColorSpace) - { - throw Exception("Can't create the processor for the destination config: " - "display color space not found."); - } - + // This creates a DisplayViewTransform and uses the standard BuildDisplayOps to build it, + // handle aliases, and do any necessary error handling. auto p2 = dstConfig->getProcessor(dstContext, dstInterchangeName, dstDisplay, dstView, direction); if (!p2) { @@ -5149,13 +5475,35 @@ ConstProcessorRcPtr Config::GetProcessorFromConfigs(const ConstContextRcPtr & sr "and the destination display view transform."); } + // Although we now have a valid processor, we still need to get the view's color space + // to check if it's actually a data space. This resolution process handles aliases and + // the case. + bool viewIsData = false; + const char * csName = dstConfig->getResolvedDisplayViewColorSpaceName(dstDisplay, dstView); + ConstColorSpaceRcPtr viewColorSpace = dstConfig->getColorSpace(csName); + if (!viewColorSpace) + { + // A view's color space could be a Named Transform. In this case, viewIsData + // should remain false. + ConstNamedTransformRcPtr nt = dstConfig->getNamedTransform(csName); + if (!nt) + { + // Given that the getProcessor call succeeded above, this should never happen. + throw Exception("Can't create the processor for the destination config."); + } + } + else + { + viewIsData = viewColorSpace->isData(); + } + ProcessorRcPtr processor = Processor::Create(); processor->getImpl()->setProcessorCacheFlags(srcConfig->getImpl()->m_cacheFlags); // If either of the color spaces are data spaces, its corresponding processor // will be empty, but need to make sure the entire result is also empty to // better match the semantics of how data spaces are handled. - if (!srcColorSpace->isData() && !displayColorSpace->isData()) + if (!srcColorSpace->isData() && !viewIsData) { if (direction == TRANSFORM_DIR_INVERSE) { @@ -5978,6 +6326,28 @@ void Config::Impl::checkVersionConsistency() const } } + if (hexVersion < 0x02060000) + { + for (const auto& vt : m_viewTransforms) + { + if (vt->getNumAliases() > 0) + { + std::ostringstream os; + os << "Config failed validation. The view transform '" << vt->getName() << "' "; + os << "has aliases and config version is less than 2.6."; + throw Exception(os.str().c_str()); + } + } + } + + // Check for use_display_view_aliases. + + if (hexVersion < 0x02060000 && m_useDisplayViewAliases) + { + throw Exception("Config failed validation: use_display_view_aliases is true and config " + "version is less than 2.6."); + } + // Check for new Look properties. if (hexVersion < 0x02050000) diff --git a/src/OpenColorIO/OCIOYaml.cpp b/src/OpenColorIO/OCIOYaml.cpp index 9010dcb606..8f2ad05e40 100644 --- a/src/OpenColorIO/OCIOYaml.cpp +++ b/src/OpenColorIO/OCIOYaml.cpp @@ -3822,6 +3822,15 @@ inline void load(const YAML::Node & node, ViewTransformRcPtr & vt) load(iter->second, stringval); vt->setName(stringval.c_str()); } + else if (key == "aliases") + { + StringUtils::StringVec aliases; + load(iter->second, aliases); + for (const auto & alias : aliases) + { + vt->addAlias(alias.c_str()); + } + } else if (key == "description") { std::string stringval; @@ -3884,6 +3893,18 @@ inline void save(YAML::Emitter & out, ConstViewTransformRcPtr & vt, unsigned int out << YAML::BeginMap; out << YAML::Key << "name" << YAML::Value << vt->getName(); + const size_t numAliases = vt->getNumAliases(); + if (numAliases) + { + out << YAML::Key << "aliases"; + StringUtils::StringVec aliases; + for (size_t aidx = 0; aidx < numAliases; ++aidx) + { + aliases.push_back(vt->getAlias(aidx)); + } + out << YAML::Flow << YAML::Value << aliases; + } + const char * family = vt->getFamily(); if (family && *family) { @@ -4754,6 +4775,11 @@ inline void load(const YAML::Node& node, ConfigRcPtr & config, const char* filen } } } + else if (key == "use_display_view_aliases") + { + load(iter->second, boolval); + config->setUseDisplayViewAliases(boolval); + } else if(key == "active_displays") { StringUtils::StringVec display; @@ -5272,6 +5298,12 @@ inline void save(YAML::Emitter & out, const Config & config) out << YAML::Newline; out << YAML::Newline; + + if (config.getUseDisplayViewAliases()) + { + out << YAML::Key << "use_display_view_aliases" << YAML::Value << true; + } + out << YAML::Key << "active_displays"; StringUtils::StringVec active_displays; int nDisplays = config.getNumActiveDisplays(); diff --git a/src/OpenColorIO/ViewTransform.cpp b/src/OpenColorIO/ViewTransform.cpp index c14d6021ac..c784ca8ebd 100644 --- a/src/OpenColorIO/ViewTransform.cpp +++ b/src/OpenColorIO/ViewTransform.cpp @@ -5,7 +5,9 @@ #include +#include "Platform.h" #include "TokensManager.h" +#include "utils/StringUtils.h" namespace { @@ -20,6 +22,7 @@ class ViewTransform::Impl { public: std::string m_name; + StringUtils::StringVec m_aliases; std::string m_family; std::string m_description; ReferenceSpaceType m_referenceSpaceType{ REFERENCE_SPACE_SCENE }; @@ -44,6 +47,7 @@ class ViewTransform::Impl if (this != &rhs) { m_name = rhs.m_name; + m_aliases = rhs.m_aliases; m_family = rhs.m_family; m_description = rhs.m_description; @@ -100,6 +104,63 @@ const char * ViewTransform::getName() const noexcept void ViewTransform::setName(const char * name) noexcept { getImpl()->m_name = name ? name : ""; + // Name can no longer be an alias. + StringUtils::Remove(getImpl()->m_aliases, getImpl()->m_name); +} + +size_t ViewTransform::getNumAliases() const noexcept +{ + return getImpl()->m_aliases.size(); +} + +const char * ViewTransform::getAlias(size_t idx) const noexcept +{ + if (idx < getImpl()->m_aliases.size()) + { + return getImpl()->m_aliases[idx].c_str(); + } + return ""; +} + +bool ViewTransform::hasAlias(const char * alias) const noexcept +{ + if (!alias) return false; + for (size_t idx = 0; idx < getImpl()->m_aliases.size(); ++idx) + { + if (0 == Platform::Strcasecmp(getImpl()->m_aliases[idx].c_str(), alias)) + { + return true; + } + } + return false; +} + +void ViewTransform::addAlias(const char * alias) noexcept +{ + if (alias && *alias) + { + if (!StringUtils::Compare(alias, getImpl()->m_name)) + { + if (!StringUtils::Contain(getImpl()->m_aliases, alias)) + { + getImpl()->m_aliases.push_back(alias); + } + } + } +} + +void ViewTransform::removeAlias(const char * name) noexcept +{ + if (name && *name) + { + const std::string alias{ name }; + StringUtils::Remove(getImpl()->m_aliases, alias); + } +} + +void ViewTransform::clearAliases() noexcept +{ + getImpl()->m_aliases.clear(); } const char * ViewTransform::getFamily() const noexcept @@ -267,6 +328,20 @@ std::ostream & operator<< (std::ostream & os, const ViewTransform & vt) { os << " 1) + { + os << "aliases=[" << vt.getAlias(0); + for (size_t aidx = 1; aidx < numAliases; ++aidx) + { + os << ", " << vt.getAlias(aidx); + } + os << "], "; + } os << "family=" << vt.getFamily() << ", "; os << "referenceSpaceType=" << ReferenceSpaceTypeToString(vt.getReferenceSpaceType()); const std::string desc{ vt.getDescription() }; diff --git a/src/OpenColorIO/apphelpers/LegacyViewingPipeline.cpp b/src/OpenColorIO/apphelpers/LegacyViewingPipeline.cpp index 71fe1941fd..89491f80f4 100644 --- a/src/OpenColorIO/apphelpers/LegacyViewingPipeline.cpp +++ b/src/OpenColorIO/apphelpers/LegacyViewingPipeline.cpp @@ -205,24 +205,37 @@ ConstProcessorRcPtr LegacyViewingPipelineImpl::getProcessor(const ConstConfigRcP throw Exception(os.str().c_str()); } - const std::string display = m_displayViewTransform->getDisplay(); - const std::string view = m_displayViewTransform->getView(); + // Resolve the display and view the same way BuildDisplayOps does, so that a display or view + // name that is aliased still finds the right color space and looks. + // + // Note this is not merely duplicating what the DisplayViewTransform appended below will do + // for itself. The looks lookup further down uses Config::getDisplayViewLooks, which reports + // what the config contains and so needs an up-to-date view name, and the color space found + // here decides whether color space conversions are skipped at all. The looks matter most: + // setDisplayViewTransform forces LooksBypass on the stored transform, so the looks are + // applied by this function or not at all. + std::string display = m_displayViewTransform->getDisplay(); + const char * canonicalDisplay = config->getCanonicalDisplayName(display.c_str()); + if (canonicalDisplay && *canonicalDisplay) + { + display = canonicalDisplay; + } - const std::string viewTransformName = config->getDisplayViewTransformName(display.c_str(), - view.c_str()); - ConstViewTransformRcPtr viewTransform; - if (!viewTransformName.empty()) + // Ensure the view name is resolved (needed for getDisplayViewLooks below). The + // DisplayViewTransform will do the same name resolution, so need to stay in sync. + std::string view = m_displayViewTransform->getView(); + const char * canonicalView = config->getCanonicalViewName(display.c_str(), view.c_str()); + if (canonicalView && *canonicalView) { - viewTransform = config->getViewTransform(viewTransformName.c_str()); + view = canonicalView; } - // NB: If the viewTransform is present, then displayColorSpace is a true display color space - // rather than a traditional color space. - const std::string name{ config->getDisplayViewColorSpaceName(display.c_str(), view.c_str()) }; - // A shared view containing a view transform may set the color space to USE_DISPLAY_NAME, - // in which case we look for a display color space with the same name as the display. - const bool nameFromDisplay = (0 == strcmp(name.c_str(), OCIO_VIEW_USE_DISPLAY_NAME)); - const std::string displayColorSpaceName{ nameFromDisplay ? display : name }; + // NB: If the view has a view transform, then displayColorSpace is a true display color space + // rather than a traditional color space. Note that this also handles a shared view that + // sets its display color space to USE_DISPLAY_NAME, by looking for a display color space + // with the same name as the display. + const std::string displayColorSpaceName{ + config->getResolvedDisplayViewColorSpaceName(display.c_str(), view.c_str()) }; ConstColorSpaceRcPtr displayColorSpace = config->getColorSpace(displayColorSpaceName.c_str()); // If this is not a color space it can be a named transform. Error handling (missing color // space or named transform) is handled by display view transform. diff --git a/src/OpenColorIO/transforms/DisplayViewTransform.cpp b/src/OpenColorIO/transforms/DisplayViewTransform.cpp index 629008cf3b..aa490d61c6 100644 --- a/src/OpenColorIO/transforms/DisplayViewTransform.cpp +++ b/src/OpenColorIO/transforms/DisplayViewTransform.cpp @@ -324,7 +324,17 @@ void BuildDisplayOps(OpRcPtrVec & ops, } const std::string display = displayViewTransform.getDisplay(); - if (config.getNumViews(display.c_str()) == 0) + + // Config authors may opt-in to using display color space names/aliases as aliases for + // display names. Resolve those to the actual display name. + std::string resolvedDisplay = display; + const char * canonicalDisplay = config.getCanonicalDisplayName(display.c_str()); + if (canonicalDisplay && *canonicalDisplay) + { + resolvedDisplay = canonicalDisplay; + } + + if (config.getNumViews(resolvedDisplay.c_str()) == 0) { std::ostringstream os; os << "DisplayViewTransform error."; @@ -333,13 +343,23 @@ void BuildDisplayOps(OpRcPtrVec & ops, } const std::string view = displayViewTransform.getView(); + // Config authors may opt-in to using view transform names/aliases as aliases for + // view names. Resolve those to the actual view name. + std::string resolvedView = view; + const char * canonicalView = config.getCanonicalViewName(resolvedDisplay.c_str(), view.c_str()); + if (canonicalView && *canonicalView) + { + resolvedView = canonicalView; + } + // Get the view transform if any: if it exists, it can be a view transform or a named transform. - const std::string viewTransformName = config.getDisplayViewTransformName(display.c_str(), - view.c_str()); + const std::string viewTransformName = config.getDisplayViewTransformName(resolvedDisplay.c_str(), + resolvedView.c_str()); ConstViewTransformRcPtr viewTransform; ConstNamedTransformRcPtr viewNamedTransform; if (!viewTransformName.empty()) { + // If there is both a VT and NT with that name, the ViewTransform takes priority. viewTransform = config.getViewTransform(viewTransformName.c_str()); if (!viewTransform) { @@ -355,19 +375,18 @@ void BuildDisplayOps(OpRcPtrVec & ops, } } - // Get the color space associated to the (display, view) pair. + // Get the color space associated to the (display, view) pair. This also takes care of the + // case of a shared view containing a view transform that sets the color space to + // , by looking for a display color space with the same name as the display. // (Returns an empty string if the view does not exist. This is trapped below.) - const char * csName = config.getDisplayViewColorSpaceName(display.c_str(), view.c_str()); - - // A shared view containing a view transform may set the color space to , - // in which case we look for a display color space with the same name as the display. - const std::string displayColorSpaceName = View::UseDisplayName(csName) ? display : csName; + const std::string displayColorSpaceName + = config.getResolvedDisplayViewColorSpaceName(resolvedDisplay.c_str(), + resolvedView.c_str()); // At this point, displayColorSpaceName is typically one of the following strings: // 1. The "colorspace" attribute of the View, if there is no "view_transform". - // 2. The name of the View's display, if it's a shared_view and the "display_colorspace" - // is "". It is expected this will also be the name of a display - // color space in the config. + // 2. The display color space named after the View's display, if it's a shared_view and the + // "display_colorspace" is "". // 3. Else, the "display_colorspace" string if it's a View with a "view_transform". // // (Though, in the implementation, both the "colorspace" and "display_colorspace" of @@ -427,7 +446,7 @@ void BuildDisplayOps(OpRcPtrVec & ops, LookParseResult looks; if (!displayViewTransform.getLooksBypass()) { - looks.parse(config.getDisplayViewLooks(display.c_str(), view.c_str())); + looks.parse(config.getDisplayViewLooks(resolvedDisplay.c_str(), resolvedView.c_str())); } // Now that all the inputs are found and validated, the following code builds the list of ops @@ -543,7 +562,26 @@ bool CollectContextVariables(const Config & config, foundContextVars = true; } - const char * csName = config.getDisplayViewColorSpaceName(tr.getDisplay(), tr.getView()); + // Resolve the display and view the same way BuildDisplayOps does, so that a + // DisplayViewTransform using an old display or view name still finds the right color space, + // view transform, and looks. + std::string display{ tr.getDisplay() }; + const char * canonicalDisplay = config.getCanonicalDisplayName(display.c_str()); + if (canonicalDisplay && *canonicalDisplay) + { + display = canonicalDisplay; + } + + std::string view{ tr.getView() }; + const char * canonicalView = config.getCanonicalViewName(display.c_str(), view.c_str()); + if (canonicalView && *canonicalView) + { + view = canonicalView; + } + + // Note that this also handles a shared view whose display color space is . + const char * csName = config.getResolvedDisplayViewColorSpaceName(display.c_str(), + view.c_str()); if (csName && *csName) { src = config.getColorSpace(csName); @@ -553,7 +591,7 @@ bool CollectContextVariables(const Config & config, } } - const char * vtName = config.getDisplayViewTransformName(tr.getDisplay(), tr.getView()); + const char * vtName = config.getDisplayViewTransformName(display.c_str(), view.c_str()); if (vtName && *vtName) { ConstViewTransformRcPtr vt = config.getViewTransform(vtName); @@ -576,7 +614,7 @@ bool CollectContextVariables(const Config & config, // TODO: The LooksBypass must be a DynamicProperty to allow live on/off. if (!tr.getLooksBypass()) { - const std::string looksStr = config.getDisplayViewLooks(tr.getDisplay(), tr.getView()); + const std::string looksStr = config.getDisplayViewLooks(display.c_str(), view.c_str()); LookParseResult looks; looks.parse(looksStr); diff --git a/src/apps/ocioconvert/main.cpp b/src/apps/ocioconvert/main.cpp index 92bf188acb..1d0fa6280f 100644 --- a/src/apps/ocioconvert/main.cpp +++ b/src/apps/ocioconvert/main.cpp @@ -654,7 +654,10 @@ int main(int argc, const char **argv) { if (useDisplayView) { - outputcolorspace = config->getDisplayViewColorSpaceName(display, view); + // Note that this resolves the (display, view) pair the same way the processor above + // did, and yields the name of an actual color space even for a shared view that uses + // . + outputcolorspace = config->getResolvedDisplayViewColorSpaceName(display, view); } if (outputcolorspace) diff --git a/src/bindings/python/PyConfig.cpp b/src/bindings/python/PyConfig.cpp index 6e7971844f..aa85845ad4 100644 --- a/src/bindings/python/PyConfig.cpp +++ b/src/bindings/python/PyConfig.cpp @@ -346,6 +346,10 @@ void bindPyConfig(py::module & m) DOC(Config, isStrictParsingEnabled)) .def("setStrictParsingEnabled", &Config::setStrictParsingEnabled, "enabled"_a, DOC(Config, setStrictParsingEnabled)) + .def("getUseDisplayViewAliases", &Config::getUseDisplayViewAliases, + DOC(Config, getUseDisplayViewAliases)) + .def("setUseDisplayViewAliases", &Config::setUseDisplayViewAliases, "enabled"_a, + DOC(Config, setUseDisplayViewAliases)) .def("setInactiveColorSpaces", &Config::setInactiveColorSpaces, "inactiveColorSpaces"_a, DOC(Config, setInactiveColorSpaces)) .def("getInactiveColorSpaces", &Config::getInactiveColorSpaces, @@ -432,6 +436,12 @@ void bindPyConfig(py::module & m) { return DisplayAllIterator(self); }) + .def("getCanonicalDisplayName", &Config::getCanonicalDisplayName, "display"_a, + DOC(Config, getCanonicalDisplayName)) + .def("getDisplayDescription", &Config::getDisplayDescription, "display"_a, + DOC(Config, getDisplayDescription)) + .def("getCanonicalViewName", &Config::getCanonicalViewName, "display"_a, "view"_a, + DOC(Config, getCanonicalViewName)) .def("getDefaultView", (const char * (Config::*)(const char *) const) &Config::getDefaultView, "display"_a, @@ -463,6 +473,9 @@ void bindPyConfig(py::module & m) .def("getDisplayViewColorSpaceName", &Config::getDisplayViewColorSpaceName, "display"_a, "view"_a, DOC(Config, getDisplayViewColorSpaceName)) + .def("getResolvedDisplayViewColorSpaceName", &Config::getResolvedDisplayViewColorSpaceName, + "display"_a, "view"_a, + DOC(Config, getResolvedDisplayViewColorSpaceName)) .def("getDisplayViewLooks", &Config::getDisplayViewLooks, "display"_a, "view"_a, DOC(Config, getDisplayViewLooks)) .def("getDisplayViewRule", &Config::getDisplayViewRule, "display"_a, "view"_a, diff --git a/src/bindings/python/PyViewTransform.cpp b/src/bindings/python/PyViewTransform.cpp index d3de4730e9..564a6df785 100644 --- a/src/bindings/python/PyViewTransform.cpp +++ b/src/bindings/python/PyViewTransform.cpp @@ -11,11 +11,13 @@ namespace enum ViewTransformIterator { - IT_VIEW_TRANSFORM_CATEGORY = 0 + IT_VIEW_TRANSFORM_CATEGORY = 0, + IT_VIEW_TRANSFORM_ALIAS }; using ViewTransformCategoryIterator = PyIterator; +using ViewTransformAliasIterator = PyIterator; std::vector getCategoriesStdVec(const ViewTransformRcPtr & p) { std::vector categories; @@ -27,6 +29,17 @@ std::vector getCategoriesStdVec(const ViewTransformRcPtr & p) { return categories; } +std::vector getAliasesStdVec(const ViewTransformRcPtr & p) +{ + std::vector aliases; + aliases.reserve(p->getNumAliases()); + for (size_t i = 0; i < p->getNumAliases(); i++) + { + aliases.push_back(p->getAlias(i)); + } + return aliases; +} + } // namespace void bindPyViewTransform(py::module & m) @@ -41,6 +54,10 @@ void bindPyViewTransform(py::module & m) py::class_( clsViewTransform, "ViewTransformCategoryIterator"); + auto clsViewTransformAliasIterator = + py::class_( + clsViewTransform, "ViewTransformAliasIterator"); + clsViewTransform .def(py::init([](ReferenceSpaceType referenceSpace) { @@ -54,9 +71,19 @@ void bindPyViewTransform(py::module & m) const std::string & description, const TransformRcPtr & toReference, const TransformRcPtr & fromReference, - const std::vector & categories) + const std::vector & categories, + const std::vector & aliases) { ViewTransformRcPtr p = ViewTransform::Create(referenceSpace); + if (!aliases.empty()) + { + p->clearAliases(); + for (size_t i = 0; i < aliases.size(); i++) + { + p->addAlias(aliases[i].c_str()); + } + } + // Setting the name will remove alias named the same, so set name after. if (!name.empty()) { p->setName(name.c_str()); } if (!family.empty()) { p->setFamily(family.c_str()); } if (!description.empty()) { p->setDescription(description.c_str()); } @@ -85,6 +112,7 @@ void bindPyViewTransform(py::module & m) "toReference"_a = DEFAULT->getTransform(VIEWTRANSFORM_DIR_TO_REFERENCE), "fromReference"_a = DEFAULT->getTransform(VIEWTRANSFORM_DIR_FROM_REFERENCE), "categories"_a = getCategoriesStdVec(DEFAULT), + "aliases"_a = getAliasesStdVec(DEFAULT), DOC(ViewTransform, Create)) .def("__deepcopy__", [](const ConstViewTransformRcPtr & self, py::dict) @@ -97,6 +125,21 @@ void bindPyViewTransform(py::module & m) DOC(ViewTransform, getName)) .def("setName", &ViewTransform::setName, "name"_a, DOC(ViewTransform, setName)) + + // Aliases. + .def("hasAlias", &ViewTransform::hasAlias, "alias"_a, + DOC(ViewTransform, hasAlias)) + .def("addAlias", &ViewTransform::addAlias, "alias"_a.none(false), + DOC(ViewTransform, addAlias)) + .def("removeAlias", &ViewTransform::removeAlias, "alias"_a.none(false), + DOC(ViewTransform, removeAlias)) + .def("getAliases", [](ViewTransformRcPtr & self) + { + return ViewTransformAliasIterator(self); + }) + .def("clearAliases", &ViewTransform::clearAliases, + DOC(ViewTransform, clearAliases)) + .def("getFamily", &ViewTransform::getFamily, DOC(ViewTransform, getFamily)) .def("setFamily", &ViewTransform::setFamily, "family"_a, @@ -151,6 +194,26 @@ void bindPyViewTransform(py::module & m) int i = it.nextIndex(it.m_obj->getNumCategories()); return it.m_obj->getCategory(i); }); + + clsViewTransformAliasIterator + .def("__len__", [](ViewTransformAliasIterator & it) + { + return it.m_obj->getNumAliases(); + }) + .def("__getitem__", [](ViewTransformAliasIterator & it, int i) + { + it.checkIndex(i, (int)it.m_obj->getNumAliases()); + return it.m_obj->getAlias(i); + }) + .def("__iter__", [](ViewTransformAliasIterator & it) -> ViewTransformAliasIterator & + { + return it; + }) + .def("__next__", [](ViewTransformAliasIterator & it) + { + int i = it.nextIndex((int)it.m_obj->getNumAliases()); + return it.m_obj->getAlias(i); + }); } } // namespace OCIO_NAMESPACE diff --git a/tests/cpu/Config_tests.cpp b/tests/cpu/Config_tests.cpp index 28d5e430b1..1722f7e7e8 100644 --- a/tests/cpu/Config_tests.cpp +++ b/tests/cpu/Config_tests.cpp @@ -2093,12 +2093,12 @@ OCIO_ADD_TEST(Config, version) { OCIO_CHECK_THROW_WHAT(config->setVersion(2, 9), OCIO::Exception, "The minor version 9 is not supported for major version 2. " - "Maximum minor version is 5"); + "Maximum minor version is 6"); OCIO_CHECK_NO_THROW(config->setMajorVersion(2)); OCIO_CHECK_THROW_WHAT(config->setMinorVersion(9), OCIO::Exception, "The minor version 9 is not supported for major version 2. " - "Maximum minor version is 5"); + "Maximum minor version is 6"); } { @@ -6578,7 +6578,9 @@ OCIO_ADD_TEST(Config, inactive_color_space_read_write) OCIO_ADD_TEST(Config, get_processor_from_two_configs) { constexpr const char * SIMPLE_CONFIG1{ R"( -ocio_profile_version: 2 +ocio_profile_version: 2.6 + +use_display_view_aliases: true environment: {} @@ -6587,18 +6589,28 @@ ocio_profile_version: 2 default: raw1 aces_interchange: aces1 cie_xyz_d65_interchange: display1 + color_timing: raw1 + compositing_log: raw1 + scene_linear: aces1 displays: displayname: - ! {name: view1, colorspace: displaytest1} - - ! {name: view2, view_transform: vt1, display_colorspace: display2} + - ! {name: view2, view_transform: vt1, display_colorspace: displayname} - ! {name: view3, colorspace: data_space} + - ! {name: view4, view_transform: vt2, display_colorspace: } view_transforms: - ! name: vt1 + aliases: [oldvt1] from_scene_reference: ! {min_in_value: 0., min_out_value: 0.} + - ! + name: vt2 + aliases: [oldvt2] + from_scene_reference: ! {offset: [0.02, 0.03, 0.04, 0]} + colorspaces: - ! name: raw1 @@ -6634,6 +6646,12 @@ ocio_profile_version: 2 allocation: uniform from_display_reference: ! {style: ACES_RedMod03} + - ! + name: displayname + aliases: [olddisplayname] + allocation: uniform + from_display_reference: ! {style: linear, exposure: 1.5} + )" }; constexpr const char * SIMPLE_CONFIG2{ R"( @@ -6832,10 +6850,74 @@ ocio_profile_version: 2 r3 = OCIO_DYNAMIC_POINTER_CAST(t3); OCIO_CHECK_ASSERT(r3); t4 = group->getTransform(4); - auto ff4 = OCIO_DYNAMIC_POINTER_CAST(t4); - OCIO_CHECK_ASSERT(ff4); + auto l4 = OCIO_DYNAMIC_POINTER_CAST(t4); + OCIO_CHECK_ASSERT(l4); + + // Test that aliases for display and view names work. + + // Basic test using aliased display and view names. + OCIO_CHECK_NO_THROW(p = OCIO::Config::GetProcessorFromConfigs( + config2, "test2", "aces2", config1, "olddisplayname", "oldvt1", "aces1", + OCIO::TRANSFORM_DIR_FORWARD)); + OCIO_REQUIRE_ASSERT(p); + group = p->createGroupTransform(); + OCIO_REQUIRE_EQUAL(group->getNumTransforms(), 5); + + // Test that the fallback works with just the single config getProcessor too. The result + // goes through "displayname" itself (a ECTransform), confirming the resolved view actually + // belongs with the resolved display. + OCIO_CHECK_NO_THROW(p = config1->getProcessor("aces1", "olddisplayname", "oldvt1", + OCIO::TRANSFORM_DIR_FORWARD)); + OCIO_REQUIRE_ASSERT(p); + group = p->createGroupTransform(); + OCIO_REQUIRE_EQUAL(group->getNumTransforms(), 3); + t0 = group->getTransform(0); + OCIO_CHECK_ASSERT(OCIO_DYNAMIC_POINTER_CAST(t0)); + t1 = group->getTransform(1); + OCIO_CHECK_ASSERT(OCIO_DYNAMIC_POINTER_CAST(t1)); + t2 = group->getTransform(2); + OCIO_CHECK_ASSERT(OCIO_DYNAMIC_POINTER_CAST(t2)); + + // Verify that substitution happens. + OCIO_CHECK_NO_THROW(p = config1->getProcessor("aces1", "displayname", "oldvt2", + OCIO::TRANSFORM_DIR_FORWARD)); + OCIO_REQUIRE_ASSERT(p); + group = p->createGroupTransform(); + OCIO_REQUIRE_EQUAL(group->getNumTransforms(), 3); + t0 = group->getTransform(0); + OCIO_CHECK_ASSERT(OCIO_DYNAMIC_POINTER_CAST(t0)); + t1 = group->getTransform(1); + OCIO_CHECK_ASSERT(OCIO_DYNAMIC_POINTER_CAST(t1)); + t2 = group->getTransform(2); + OCIO_CHECK_ASSERT(OCIO_DYNAMIC_POINTER_CAST(t2)); + + // Turn off display/view aliasing and ensure the result now throws. + { + OCIO::ConfigRcPtr config1NoFallback = config1->createEditableCopy(); + config1NoFallback->setUseDisplayViewAliases(false); + OCIO_CHECK_THROW_WHAT(OCIO::Config::GetProcessorFromConfigs( + config2, "test2", "aces2", config1NoFallback, "olddisplayname", "oldvt1", "aces1", + OCIO::TRANSFORM_DIR_FORWARD), + OCIO::Exception, + "DisplayViewTransform error. Display 'olddisplayname' not found."); + } + + // Modify view2 to point to a display color space that is different from displayname. + // This should throw since the view aliasing will not accept that as a viable match. + { + OCIO::ConfigRcPtr config1Mismatch = config1->createEditableCopy(); + OCIO_CHECK_NO_THROW(config1Mismatch->addDisplayView("displayname", "view2", "vt1", + "display2", "", "", "")); + OCIO_CHECK_THROW_WHAT(OCIO::Config::GetProcessorFromConfigs( + config2, "test2", "aces2", config1Mismatch, "olddisplayname", "oldvt1", "aces1", + OCIO::TRANSFORM_DIR_FORWARD), + OCIO::Exception, + "DisplayViewTransform error. The display 'olddisplayname' does not have view " + "'oldvt1'."); + } // If one of the spaces is a data space, the whole result must be a no-op. + OCIO_CHECK_NO_THROW(p = OCIO::Config::GetProcessorFromConfigs( config2, "test2", config1, "displayname", "view3", OCIO::TRANSFORM_DIR_FORWARD)); OCIO_REQUIRE_ASSERT(p); @@ -7131,6 +7213,103 @@ OCIO_ADD_TEST(Config, view_transforms) OCIO_CHECK_EQUAL(std::string("NotFirst"), configEdit->getDefaultViewTransformName()); } +OCIO_ADD_TEST(Config, view_transform_alias) +{ + OCIO::ConfigRcPtr config = OCIO::Config::CreateFromBuiltinConfig( + "cg-config-v2.2.0_aces-v1.3_ocio-v2.4")->createEditableCopy(); + // ViewTransform aliases require config version 2.6 or higher. + config->setVersion(2, 6); + // Aliases work even if display/view aliasing is off. + OCIO_REQUIRE_ASSERT(!config->getUseDisplayViewAliases()); + + auto sceneVT = OCIO::ViewTransform::Create(OCIO::REFERENCE_SPACE_SCENE); + sceneVT->setName("scene_vt"); + sceneVT->addAlias("old_scene_vt"); + OCIO_CHECK_NO_THROW(sceneVT->setTransform(OCIO::MatrixTransform::Create(), + OCIO::VIEWTRANSFORM_DIR_FROM_REFERENCE)); + OCIO_CHECK_NO_THROW(config->addViewTransform(sceneVT)); + + auto displayVT = OCIO::ViewTransform::Create(OCIO::REFERENCE_SPACE_DISPLAY); + displayVT->setName("display_vt"); + OCIO_CHECK_NO_THROW(displayVT->setTransform(OCIO::MatrixTransform::Create(), + OCIO::VIEWTRANSFORM_DIR_FROM_REFERENCE)); + OCIO_CHECK_NO_THROW(config->addViewTransform(displayVT)); + + OCIO_CHECK_NO_THROW(config->validate()); + + // Validate getViewTransform resolves the alias. + OCIO_CHECK_ASSERT(config->getViewTransform("old_scene_vt")); + OCIO_CHECK_EQUAL(std::string(config->getViewTransform("old_scene_vt")->getName()), "scene_vt"); + + // Check setDefaultViewTransformName also accepts an alias. + config->setDefaultViewTransformName("old_scene_vt"); + OCIO_CHECK_NO_THROW(config->validate()); + OCIO_REQUIRE_ASSERT(config->getDefaultSceneToDisplayViewTransform()); + OCIO_CHECK_EQUAL(std::string(config->getDefaultSceneToDisplayViewTransform()->getName()), + "scene_vt"); + config->setDefaultViewTransformName(""); + + // Validate addViewTransform is blocked if it would create any collisions. + + // An alias must not collide with another view transform's name. + auto conflict = OCIO::ViewTransform::Create(OCIO::REFERENCE_SPACE_SCENE); + conflict->setName("display_vt"); + conflict->addAlias("scene_vt"); + OCIO_CHECK_NO_THROW(conflict->setTransform(OCIO::MatrixTransform::Create(), + OCIO::VIEWTRANSFORM_DIR_FROM_REFERENCE)); + OCIO_CHECK_THROW_WHAT(config->addViewTransform(conflict), OCIO::Exception, + "it has an alias 'scene_vt' that is already used by view " + "transform 'scene_vt'"); + + conflict->setName("another_vt"); + OCIO_CHECK_THROW_WHAT(config->addViewTransform(conflict), OCIO::Exception, + "it has an alias 'scene_vt' that is already used by view " + "transform 'scene_vt'"); + + // An alias must not collide with another view transform's alias. + conflict->clearAliases(); + conflict->addAlias("old_scene_vt"); + OCIO_CHECK_THROW_WHAT(config->addViewTransform(conflict), OCIO::Exception, + "it has an alias 'old_scene_vt' that is already used by view " + "transform 'scene_vt'"); + + // A name must not collide with another view transform's alias. + conflict->clearAliases(); + conflict->setName("old_scene_vt"); + OCIO_CHECK_THROW_WHAT(config->addViewTransform(conflict), OCIO::Exception, + "existing view transform 'scene_vt' is using this name as an alias"); + + // Aliases require config version 2.6 or higher. + + config->setVersion(2, 5); + OCIO_CHECK_THROW_WHAT(config->validate(), OCIO::Exception, + "The view transform 'scene_vt' has aliases and config version is " + "less than 2.6"); + config->setVersion(2, 6); + OCIO_CHECK_NO_THROW(config->validate()); + + // Aliases survive a serialize / reload round-trip. + + std::ostringstream os; + config->serialize(os); + + std::istringstream is; + is.str(os.str()); + + OCIO::ConstConfigRcPtr configReloaded; + OCIO_CHECK_NO_THROW(configReloaded = OCIO::Config::CreateFromStream(is)); + OCIO_CHECK_NO_THROW(configReloaded->validate()); + + auto reloadedVT = configReloaded->getViewTransform("scene_vt"); + OCIO_REQUIRE_ASSERT(reloadedVT); + OCIO_REQUIRE_EQUAL(reloadedVT->getNumAliases(), 1); + OCIO_CHECK_EQUAL(std::string(reloadedVT->getAlias(0)), "old_scene_vt"); + + OCIO_CHECK_ASSERT(configReloaded->getViewTransform("old_scene_vt")); + OCIO_CHECK_EQUAL(std::string(configReloaded->getViewTransform("old_scene_vt")->getName()), + "scene_vt"); +} + OCIO_ADD_TEST(Config, display_view) { // Create a config with a display that has 2 kinds of views. @@ -7299,6 +7478,470 @@ default_view_transform: view_transform OCIO::Exception, "a non-empty color space name is needed"); } +OCIO_ADD_TEST(Config, aliased_view_name) +{ + constexpr const char * SIMPLE_CONFIG{ R"( +ocio_profile_version: 2.6 + +use_display_view_aliases: true + +environment: + {} +roles: + default: raw1 + aces_interchange: raw1 + cie_xyz_d65_interchange: display_cs + color_timing: raw1 + compositing_log: raw1 + scene_linear: raw1 + +view_transforms: + - ! + name: vt_new + aliases: [vt_old, vt_older] + from_scene_reference: ! {} + + - ! + name: vt_shared_new + aliases: [vt_shared_old] + from_scene_reference: ! {} + + - ! + name: vt_udn + aliases: [vt_udn_old] + from_scene_reference: ! {} + + - ! + name: vt_match + from_scene_reference: ! {} + +named_transforms: + - ! + name: nt_new + aliases: [nt_old, nt_older] + transform: ! {} + +colorspaces: + - ! + name: display_scene + + - ! + name: raw1 + +display_colorspaces: + - ! + name: display_cs + aliases: [old_display_name, display_alias_cs] + + - ! + name: dcs2 + aliases: [dcs_shared] + + - ! + name: dcs3 + +shared_views: + - ! {name: view_shared, view_transform: vt_shared_old, display_colorspace: dcs_shared} + - ! {name: view_udn, view_transform: vt_udn_old, display_colorspace: } + +displays: + display_cs: + - ! {name: view_act, colorspace: raw1} + - ! {name: view2, view_transform: vt_old, display_colorspace: dcs2} + - ! {name: view2b, view_transform: vt_old, display_colorspace: display_cs, + looks: lk1, rule: r1, description: foo} + - ! [view_shared, view_udn] + - ! {name: view_match_alias, view_transform: vt_match, + display_colorspace: display_alias_cs} + display_no_cs: + - ! {name: view_match, view_transform: vt_match, display_colorspace: display_cs} + - ! {name: view2, view_transform: vt_old, display_colorspace: dcs2} + - ! {name: view_nt, view_transform: nt_old, display_colorspace: dcs3} + - ! [view_shared] + display_scene: + - ! {name: view3, view_transform: vt_old, display_colorspace: dcs3} + +active_views: [view_act] +)" }; + + std::istringstream is; + is.str(SIMPLE_CONFIG); + OCIO::ConstConfigRcPtr config; + OCIO_CHECK_NO_THROW(config = OCIO::Config::CreateFromStream(is)); + + // Most views are inactive, the tests confirm the fallback must still find it. + // Like Config::hasView, it must work regardless of whether the view is active. + OCIO_CHECK_EQUAL(config->getNumViews("display_cs"), 1); + OCIO_CHECK_ASSERT(config->hasView("display_cs", "view2b")); + + // The fallback is opt-in and disabled by default (exact matches still work). + { + OCIO::ConfigRcPtr configNoAliases = config->createEditableCopy(); + OCIO_CHECK_NO_THROW(configNoAliases->setUseDisplayViewAliases(false)); + + OCIO_CHECK_ASSERT(!configNoAliases->getUseDisplayViewAliases()); + OCIO_CHECK_EQUAL(std::string( + configNoAliases->getCanonicalViewName("display_cs", "vt_new")), ""); + OCIO_CHECK_NO_THROW(configNoAliases->setUseDisplayViewAliases(true)); + OCIO_CHECK_EQUAL(std::string( + configNoAliases->getCanonicalViewName("display_cs", "vt_new")), "view2b"); + } + OCIO_CHECK_ASSERT(config->getUseDisplayViewAliases()); + + // Normal case: "view_act" is already a view name. + OCIO_CHECK_EQUAL(std::string( + config->getCanonicalViewName("display_cs", "view_act")), "view_act"); + + // Test without a display color space corresponding to the display. + { + // The "vt_match" is not a view name, but it's a view_transform name. + OCIO_CHECK_EQUAL(std::string( + config->getCanonicalViewName("display_no_cs", "vt_match")), "view_match"); + + // The "view2" has "view_transform: vt_old", which is an alias for "vt_new". + OCIO_CHECK_EQUAL(std::string( + config->getCanonicalViewName("display_no_cs", "vt_new")), "view2"); + + // Test where both the requested view and the view's view_transform are aliases. + OCIO_CHECK_EQUAL(std::string( + config->getCanonicalViewName("display_no_cs", "vt_older")), "view2"); + + // Test where the view_transform is a NamedTransform. + OCIO_CHECK_EQUAL(std::string( + config->getCanonicalViewName("display_no_cs", "nt_new")), "view_nt"); + + // Test referring to an alias of a NamedTransform. + OCIO_CHECK_EQUAL(std::string( + config->getCanonicalViewName("display_no_cs", "nt_older")), "view_nt"); + + // Test where the view is a shared view. + OCIO_CHECK_EQUAL(std::string( + config->getCanonicalViewName("display_no_cs", "vt_shared_new")), "view_shared"); + } + + // Now test when there is a display color space that corresponds to the display. In + // this case, the function does additional checking to ensure that it does not return + // a view that has a display_colorspace that differs from the display's color space + + // Repeat the test from above and confirm that "view2" is no longer an option (even + // though it appears earlier in the config) because its display_colorspace is different + // from that of the display. + OCIO_CHECK_EQUAL(std::string( + config->getCanonicalViewName("display_cs", "vt_older")), "view2b"); + + // Now remove view2b and confirm that no match is found. + { + OCIO::ConfigRcPtr configNoView2b = config->createEditableCopy(); + OCIO_CHECK_NO_THROW(configNoView2b->removeDisplayView("display_cs", "view2b")); + + OCIO_CHECK_EQUAL(std::string( + configNoView2b->getCanonicalViewName("display_cs", "vt_older")), ""); + } + + // Verify that a display name alias is resolved as well. + OCIO_CHECK_EQUAL(std::string( + config->getCanonicalViewName("old_display_name", "vt_older")), "view2b"); + + // Test that the display_colorspace resolution succeeds, even if it's an alias. + OCIO_CHECK_EQUAL(std::string( + config->getCanonicalViewName("display_cs", "vt_match")), "view_match_alias"); + + // A candidate using as its display_colorspace is always accepted: by + // definition, its color space is whichever one is named after the display. + OCIO_CHECK_EQUAL(std::string( + config->getCanonicalViewName("display_cs", "vt_udn")), "view_udn"); + + // The display_scene is both a display name and scene-referred color space, which is something + // that could happen (e.g. "sRGB"). In this case, avoid the comparison against the view's + // display_colorspace. There should be no comparison of display_scene and dcs3. + OCIO_CHECK_EQUAL(std::string( + config->getCanonicalViewName("display_scene", "vt_new")), "view3"); + + // No match at all. + OCIO_CHECK_EQUAL(std::string(config->getCanonicalViewName("display_cs", "not a view")), ""); + + // Null/empty display or view. + OCIO_CHECK_EQUAL(std::string(config->getCanonicalViewName("display_cs", "")), ""); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalViewName("display_cs", nullptr)), ""); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalViewName("", "view1")), ""); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalViewName(nullptr, "view1")), ""); + + // Unlike getCanonicalViewName, the other Config methods that take a view name are not meant + // to resolve it against a view transform alias. These functions are meant to tell exactly + // what the Config object contains, which would become less clear if they resolved aliases. + OCIO_CHECK_EQUAL(std::string(config->getCanonicalViewName("display_cs", "vt_old")), "view2b"); + OCIO_CHECK_ASSERT(config->hasView("display_cs", "view2")); + OCIO_CHECK_ASSERT(!config->hasView("display_cs", "vt_new")); + OCIO_CHECK_ASSERT(!config->hasView("display_cs", "vt_old")); + OCIO_CHECK_ASSERT(!config->hasView("display_cs", "vt_older")); + + OCIO_CHECK_EQUAL(std::string(config->getDisplayViewTransformName("display_cs", "vt_old")), ""); + OCIO_CHECK_EQUAL(std::string(config->getDisplayViewColorSpaceName("display_cs", "vt_old")), ""); + OCIO_CHECK_EQUAL(std::string(config->getDisplayViewLooks("display_cs", "vt_old")), ""); + OCIO_CHECK_EQUAL(std::string(config->getDisplayViewRule("display_cs", "vt_old")), ""); + OCIO_CHECK_EQUAL(std::string(config->getDisplayViewDescription("display_cs", "vt_old")), ""); + + OCIO::ConfigRcPtr configEdit = config->createEditableCopy(); + OCIO_CHECK_THROW_WHAT(configEdit->removeDisplayView("display_cs", "vt_old"), OCIO::Exception, + "Could not find a view named 'vt_old"); +} + +OCIO_ADD_TEST(Config, aliased_display_name) +{ + OCIO::ConfigRcPtr config = OCIO::Config::Create(); + config->setVersion(2, 6); + + auto raw = OCIO::ColorSpace::Create(); + raw->setName("raw"); + config->addColorSpace(raw); + + // The display color space associated with the "sRGB - Display" display keeps the display's + // old name, "sRGB", as an alias. + auto dcs = OCIO::ColorSpace::Create(OCIO::REFERENCE_SPACE_DISPLAY); + dcs->setName("sRGB - Display"); + dcs->addAlias("sRGB"); + config->addColorSpace(dcs); + + OCIO_CHECK_NO_THROW(config->addDisplayView("sRGB - Display", "view1", "raw", "")); + + // The fallback is opt-in and disabled by default. + OCIO_CHECK_ASSERT(!config->getUseDisplayViewAliases()); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("sRGB")), ""); + + config->setUseDisplayViewAliases(true); + + // Exact, case-insensitive match. + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("sRGB - Display")), + "sRGB - Display"); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("srgb - display")), + "sRGB - Display"); + + // Resolved via the display color space's alias. + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("sRGB")), "sRGB - Display"); + + // If there is a display named "sRGB" added, make sure it returns that one. + OCIO_CHECK_NO_THROW(config->addDisplayView("sRGB", "view1", "raw", "")); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("sRGB")), "sRGB"); + OCIO_CHECK_NO_THROW(config->removeDisplayView("sRGB", "view1")); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("sRGB")), "sRGB - Display"); + + // If the resolved color space's own name doesn't match any display, its aliases are tried + // too. Here "other_dcs" is a different display color space than the one associated with + // "sRGB - Display": its own name matches no display, but one of its aliases does. + OCIO_CHECK_NO_THROW(config->addDisplayView("AliasedDisplay", "view1", "raw", "")); + auto otherDcs = OCIO::ColorSpace::Create(OCIO::REFERENCE_SPACE_DISPLAY); + otherDcs->setName("other_dcs"); + otherDcs->addAlias("AliasedDisplay"); + otherDcs->addAlias("other_alias"); + OCIO_CHECK_NO_THROW(config->addColorSpace(otherDcs)); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("other_dcs")), "AliasedDisplay"); + + // Similarly any alias of other_dcs will find the AliasedDisplay. + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("other_alias")), "AliasedDisplay"); + + // Don't resolve against role names. + OCIO_CHECK_NO_THROW(config->setRole("display_role", "sRGB - Display")); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("display_role")), ""); + config->setRole("display_role", nullptr); + + // Replace the display color space with a scene-referred one of the same name, keeping the + // same "sRGB" alias. Since it is no longer display-referred, "sRGB" must no longer resolve + // to the display, even though the color space's canonical name still matches it exactly. + auto scs = OCIO::ColorSpace::Create(OCIO::REFERENCE_SPACE_SCENE); + scs->setName("sRGB - Display"); + scs->addAlias("sRGB"); + config->addColorSpace(scs); + + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("sRGB")), ""); + + // No match at all, and null/empty input. + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("does not exist")), ""); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("")), ""); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName(nullptr)), ""); + + // Unlike getCanonicalDisplayName, the other Config methods that take a display name are not + // meant to resolve it via the display color space alias fallback. These functions are meant + // to tell exactly what the Config object contains, which would become less clear if they + // resolved aliases. + OCIO_CHECK_EQUAL(config->getNumViews("sRGB"), 0); + OCIO_CHECK_ASSERT(!config->hasView("sRGB", "view1")); + OCIO_CHECK_EQUAL(std::string(config->getDefaultView("sRGB")), ""); + OCIO_CHECK_EQUAL(std::string(config->getView("sRGB", 0)), ""); + OCIO_CHECK_EQUAL(std::string(config->getDisplayViewColorSpaceName("sRGB", "view1")), ""); + OCIO_CHECK_EQUAL(std::string(config->getDisplayViewTransformName("sRGB", "view1")), ""); + + OCIO_CHECK_THROW_WHAT(config->removeDisplayView("sRGB", "view1"), OCIO::Exception, + "Could not find a display named 'sRGB'"); +} + +OCIO_ADD_TEST(Config, display_description) +{ + OCIO::ConfigRcPtr config = OCIO::Config::Create(); + config->setVersion(2, 6); + + auto dcs = OCIO::ColorSpace::Create(OCIO::REFERENCE_SPACE_DISPLAY); + dcs->setName("sRGB - Display"); + dcs->addAlias("sRGB"); + dcs->setDescription("The sRGB display."); + config->addColorSpace(dcs); + + // Exact match works even though the fallback is disabled by default. + OCIO_CHECK_ASSERT(!config->getUseDisplayViewAliases()); + OCIO_CHECK_EQUAL(std::string(config->getDisplayDescription("sRGB - Display")), + "The sRGB display."); + + // Matching via the color space's alias requires the switch. + OCIO_CHECK_EQUAL(std::string(config->getDisplayDescription("sRGB")), ""); + config->setUseDisplayViewAliases(true); + OCIO_CHECK_EQUAL(std::string(config->getDisplayDescription("sRGB")), "The sRGB display."); + + // A color space that isn't display-referred doesn't count, even with a matching name. + auto scs = OCIO::ColorSpace::Create(OCIO::REFERENCE_SPACE_SCENE); + scs->setName("scene_cs"); + scs->setDescription("A scene color space."); + config->addColorSpace(scs); + OCIO_CHECK_EQUAL(std::string(config->getDisplayDescription("scene_cs")), ""); + + // No color space at all, and null/empty input. + OCIO_CHECK_EQUAL(std::string(config->getDisplayDescription("does not exist")), ""); + OCIO_CHECK_EQUAL(std::string(config->getDisplayDescription("")), ""); + OCIO_CHECK_EQUAL(std::string(config->getDisplayDescription(nullptr)), ""); +} + +OCIO_ADD_TEST(Config, resolved_display_view_color_space_name) +{ + OCIO::ConfigRcPtr config = OCIO::Config::Create(); + config->setVersion(2, 6); + + auto raw = OCIO::ColorSpace::Create(); + raw->setName("raw"); + config->addColorSpace(raw); + + // The display color space associated with the "sRGB - Display" display keeps the display's + // old name, "sRGB", as an alias. + auto dcs = OCIO::ColorSpace::Create(OCIO::REFERENCE_SPACE_DISPLAY); + dcs->setName("sRGB - Display"); + dcs->addAlias("sRGB"); + config->addColorSpace(dcs); + + auto vt = OCIO::ViewTransform::Create(OCIO::REFERENCE_SPACE_SCENE); + vt->setName("vt_new"); + vt->addAlias("vt_old"); + OCIO_CHECK_NO_THROW(vt->setTransform(OCIO::MatrixTransform::Create(), + OCIO::VIEWTRANSFORM_DIR_FROM_REFERENCE)); + OCIO_CHECK_NO_THROW(config->addViewTransform(vt)); + + // A plain view, with just a color space. + OCIO_CHECK_NO_THROW(config->addDisplayView("sRGB - Display", "view1", "raw", "")); + + // A view with a view transform, whose display color space is . It refers to + // the view transform by its old alias, so getCanonicalViewName can find this view from the + // view transform's current name. + OCIO_CHECK_NO_THROW(config->addDisplayView("sRGB - Display", "view2", "vt_old", + "", "", "", "")); + + // The plain case behaves like getDisplayViewColorSpaceName. + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("sRGB - Display", + "view1")), "raw"); + + // is resolved to the display's own display color space, rather than being + // returned as the placeholder string. This does not require getUseDisplayViewAliases. + OCIO_CHECK_ASSERT(!config->getUseDisplayViewAliases()); + OCIO_CHECK_EQUAL(std::string(config->getDisplayViewColorSpaceName("sRGB - Display", "view2")), + ""); + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("sRGB - Display", + "view2")), + "sRGB - Display"); + + // Resolving the display and the view is opt-in, like getCanonicalDisplayName and + // getCanonicalViewName themselves. + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("sRGB", "view1")), + ""); + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("sRGB - Display", + "vt_new")), ""); + + config->setUseDisplayViewAliases(true); + + // The display is resolved from the old name kept as an alias on its display color space. + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("sRGB", "view1")), + "raw"); + + // The view is resolved from the view transform it uses, and the display may be out of date at + // the same time. + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("sRGB - Display", + "vt_new")), + "sRGB - Display"); + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("sRGB", "vt_new")), + "sRGB - Display"); + + // A view whose colorspace attribute is an alias of a color space returns the canonical name + // of that color space. + auto aliased = OCIO::ColorSpace::Create(); + aliased->setName("cs_new"); + aliased->addAlias("cs_old"); + config->addColorSpace(aliased); + OCIO_CHECK_NO_THROW(config->addDisplayView("sRGB - Display", "view3", "cs_old", "")); + OCIO_CHECK_EQUAL(std::string(config->getDisplayViewColorSpaceName("sRGB - Display", "view3")), + "cs_old"); + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("sRGB - Display", + "view3")), "cs_new"); + + // Likewise for a role. + OCIO_CHECK_NO_THROW(config->setRole("cs_role", "cs_new")); + OCIO_CHECK_NO_THROW(config->addDisplayView("sRGB - Display", "view4", "cs_role", "")); + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("sRGB - Display", + "view4")), "cs_new"); + + // A view with no view transform may use a named transform in place of a color space, and + // named transforms have aliases too. + auto nt = OCIO::NamedTransform::Create(); + nt->setName("nt_new"); + nt->addAlias("nt_old"); + nt->setTransform(OCIO::MatrixTransform::Create(), OCIO::TRANSFORM_DIR_FORWARD); + OCIO_CHECK_NO_THROW(config->addNamedTransform(nt)); + OCIO_CHECK_NO_THROW(config->addDisplayView("sRGB - Display", "view5", "nt_old", "")); + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("sRGB - Display", + "view5")), "nt_new"); + + // A shared view attached to the display is found too, and its resolves to + // the display's own color space. + OCIO_CHECK_NO_THROW(config->addSharedView("shared1", "vt_new", "", + "", "", "")); + OCIO_CHECK_NO_THROW(config->addDisplaySharedView("sRGB - Display", "shared1")); + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("sRGB - Display", + "shared1")), + "sRGB - Display"); + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("sRGB", "shared1")), + "sRGB - Display"); + + // If the color space named after the display doesn't exist, the display name is returned + // as-is, so that callers can report which color space they were unable to find. + OCIO_CHECK_NO_THROW(config->addDisplayView("no_cs_display", "view1", "vt_new", + "", "", "", "")); + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("no_cs_display", + "view1")), + "no_cs_display"); + + // Nonexistent display or view, and null/empty input. + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("sRGB - Display", + "not a view")), ""); + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("not a display", + "view1")), ""); + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("sRGB - Display", + "")), ""); + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("sRGB - Display", + nullptr)), ""); + + // Unlike getDisplayViewColorSpaceName, an empty display does not mean "look at the + // config-level shared views", since a shared view's color space is only meaningful in the + // context of a display. + OCIO_CHECK_EQUAL(std::string(config->getDisplayViewColorSpaceName("", "shared1")), + ""); + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("", "shared1")), ""); + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName(nullptr, + "shared1")), ""); +} + OCIO_ADD_TEST(Config, not_case_sensitive) { // Validate that the color spaces and roles are case insensitive. @@ -7740,6 +8383,137 @@ OCIO_ADD_TEST(Config, is_colorspace_used) OCIO_CHECK_ASSERT(!config->isColorSpaceUsed("cs65")); // Unknown color spaces are not used. } +OCIO_ADD_TEST(Config, is_colorspace_used_aliases) +{ + // Config::isColorSpaceUsed must resolve both the name it is given and each of the names it finds + // in the config down to a canonical name before comparing them, since either side may be an + // alias or a role rather than the color space's name. + + constexpr char CONFIG[]{ R"( +ocio_profile_version: 2.6 + +roles: + aces_interchange: raw + cie_xyz_d65_interchange: cie_xyz_d65 + color_timing: raw + compositing_log: raw + default: default_old + scene_linear: raw + some_role: role_old + +file_rules: + - ! {name: rule1, colorspace: fr_old, pattern: "*", extension: "*"} + - ! {name: Default, colorspace: default} + +shared_views: + - ! {name: shared1, view_transform: vt1, display_colorspace: } + +displays: + disp1: + - ! {name: view1, colorspace: view_old} + AliasedDisplay: + - ! [shared1] + +view_transforms: + - ! + name: vt1 + from_scene_reference: ! {offset: [0.1, 0.1, 0.1, 0]} + +display_colorspaces: + - ! + name: other_dcs + aliases: [AliasedDisplay] + to_display_reference: ! {offset: [0.25, 0.15, 0.35, 0]} + + - ! + name: cie_xyz_d65 + +colorspaces: + - ! + name: raw + isdata: true + + - ! + name: default_new + aliases: [default_old] + + - ! + name: view_new + aliases: [view_old] + + - ! + name: role_new + aliases: [role_old] + + - ! + name: fr_new + aliases: [fr_old] + + - ! + name: unused_new + aliases: [unused_old] +)" }; + + std::istringstream iss; + iss.str(CONFIG); + + OCIO::ConstConfigRcPtr config; + OCIO_CHECK_NO_THROW(config = OCIO::Config::CreateFromStream(iss)); + OCIO_CHECK_NO_THROW(config->validate()); + + // The display-defined view "view1" refers to "view_new" by its alias. Nothing else in the + // config mentions this color space, so the view is the only thing that can make it used. + OCIO_CHECK_ASSERT(config->isColorSpaceUsed("view_new")); + OCIO_CHECK_ASSERT(config->isColorSpaceUsed("view_old")); + + OCIO_CHECK_ASSERT(config->isColorSpaceUsed("other_dcs")); + OCIO_CHECK_ASSERT(config->isColorSpaceUsed("AliasedDisplay")); + + OCIO_CHECK_ASSERT(config->isColorSpaceUsed("role_new")); + OCIO_CHECK_ASSERT(config->isColorSpaceUsed("role_old")); + OCIO_CHECK_ASSERT(config->isColorSpaceUsed("some_role")); + + OCIO_CHECK_ASSERT(config->isColorSpaceUsed("fr_new")); + OCIO_CHECK_ASSERT(config->isColorSpaceUsed("fr_old")); + + // The Default rule's color space is written as the "default" role, which in turn refers to + // "default_new" by its alias, so resolving the rule takes two steps. + // + // Note that this path is not independently observable: the roles are checked separately, and + // any color space reachable through the Default rule's role is necessarily reachable through + // the role itself. It is the "rule1" case above that exercises the file rule comparison on + // its own. + OCIO_CHECK_ASSERT(config->isColorSpaceUsed("default_new")); + OCIO_CHECK_ASSERT(config->isColorSpaceUsed("default_old")); + + // A color space that really isn't used anywhere is still reported as unused, whether it is + // named by its canonical name or by an alias. + OCIO_CHECK_ASSERT(!config->isColorSpaceUsed("unused_new")); + OCIO_CHECK_ASSERT(!config->isColorSpaceUsed("unused_old")); + + // None of the above depends on Config::getUseDisplayViewAliases, which was off. + // Turning it on must not change any result. + OCIO::ConfigRcPtr editableConfig = config->createEditableCopy(); + OCIO_CHECK_ASSERT(!editableConfig->getUseDisplayViewAliases()); + OCIO_CHECK_NO_THROW(editableConfig->setUseDisplayViewAliases(true)); + OCIO_CHECK_NO_THROW(editableConfig->validate()); + + OCIO_CHECK_ASSERT(editableConfig->isColorSpaceUsed("view_new")); + OCIO_CHECK_ASSERT(editableConfig->isColorSpaceUsed("view_old")); + OCIO_CHECK_ASSERT(editableConfig->isColorSpaceUsed("role_new")); + OCIO_CHECK_ASSERT(editableConfig->isColorSpaceUsed("role_old")); + OCIO_CHECK_ASSERT(editableConfig->isColorSpaceUsed("some_role")); + OCIO_CHECK_ASSERT(editableConfig->isColorSpaceUsed("fr_new")); + OCIO_CHECK_ASSERT(editableConfig->isColorSpaceUsed("fr_old")); + OCIO_CHECK_ASSERT(editableConfig->isColorSpaceUsed("default_new")); + OCIO_CHECK_ASSERT(editableConfig->isColorSpaceUsed("default_old")); + OCIO_CHECK_ASSERT(editableConfig->isColorSpaceUsed("other_dcs")); + OCIO_CHECK_ASSERT(editableConfig->isColorSpaceUsed("AliasedDisplay")); + + OCIO_CHECK_ASSERT(!editableConfig->isColorSpaceUsed("unused_new")); + OCIO_CHECK_ASSERT(!editableConfig->isColorSpaceUsed("unused_old")); +} + OCIO_ADD_TEST(Config, transform_versions) { // Saving a v1 config containing v2 transforms must fail. diff --git a/tests/cpu/ViewTransform_tests.cpp b/tests/cpu/ViewTransform_tests.cpp index 0055d3ecca..c61f3ce4b4 100644 --- a/tests/cpu/ViewTransform_tests.cpp +++ b/tests/cpu/ViewTransform_tests.cpp @@ -68,3 +68,74 @@ OCIO_ADD_TEST(ViewTransform, basic) OCIO_REQUIRE_ASSERT(vtd); OCIO_CHECK_EQUAL(OCIO::REFERENCE_SPACE_DISPLAY, vtd->getReferenceSpaceType()); } + +OCIO_ADD_TEST(ViewTransform, aliases) +{ + OCIO::ViewTransformRcPtr vt = OCIO::ViewTransform::Create(OCIO::REFERENCE_SPACE_SCENE); + OCIO_REQUIRE_ASSERT(vt); + OCIO_CHECK_EQUAL(vt->getNumAliases(), 0); + constexpr char AliasA[]{ "aliasA" }; + constexpr char AliasAAlt[]{ "aLiaSa" }; + constexpr char AliasB[]{ "aliasB" }; + vt->addAlias(AliasA); + OCIO_CHECK_EQUAL(vt->getNumAliases(), 1); + OCIO_CHECK_ASSERT(vt->hasAlias(AliasA)); + OCIO_CHECK_ASSERT(vt->hasAlias(AliasAAlt)); + OCIO_CHECK_ASSERT(!vt->hasAlias(AliasB)); + vt->addAlias(AliasB); + OCIO_CHECK_EQUAL(vt->getNumAliases(), 2); + OCIO_CHECK_EQUAL(std::string(vt->getAlias(0)), AliasA); + OCIO_CHECK_EQUAL(std::string(vt->getAlias(1)), AliasB); + OCIO_CHECK_ASSERT(vt->hasAlias(AliasB)); + + // Alias with same name (different case) already exists, do nothing. + + vt->addAlias(AliasAAlt); + OCIO_CHECK_EQUAL(vt->getNumAliases(), 2); + OCIO_CHECK_EQUAL(std::string(vt->getAlias(0)), AliasA); + OCIO_CHECK_EQUAL(std::string(vt->getAlias(1)), AliasB); + + // Remove alias. + + vt->removeAlias(AliasAAlt); + OCIO_CHECK_EQUAL(vt->getNumAliases(), 1); + OCIO_CHECK_EQUAL(std::string(vt->getAlias(0)), AliasB); + OCIO_CHECK_ASSERT(!vt->hasAlias(AliasA)); + OCIO_CHECK_ASSERT(!vt->hasAlias(AliasAAlt)); + + // Add with new case. + + vt->addAlias(AliasAAlt); + OCIO_CHECK_EQUAL(vt->getNumAliases(), 2); + OCIO_CHECK_EQUAL(std::string(vt->getAlias(0)), AliasB); + OCIO_CHECK_EQUAL(std::string(vt->getAlias(1)), AliasAAlt); + OCIO_CHECK_ASSERT(vt->hasAlias(AliasA)); + OCIO_CHECK_ASSERT(vt->hasAlias(AliasAAlt)); + + // Setting the name of the view transform to one of its aliases removes the alias. + + vt->setName(AliasA); + OCIO_CHECK_EQUAL(std::string(vt->getName()), AliasA); + OCIO_CHECK_EQUAL(vt->getNumAliases(), 1); + OCIO_CHECK_EQUAL(std::string(vt->getAlias(0)), AliasB); + OCIO_CHECK_ASSERT(!vt->hasAlias(AliasA)); + OCIO_CHECK_ASSERT(!vt->hasAlias(AliasAAlt)); + + // Alias is not added if it is already the view transform name. + + vt->addAlias(AliasAAlt); + OCIO_CHECK_EQUAL(std::string(vt->getName()), AliasA); + OCIO_CHECK_EQUAL(vt->getNumAliases(), 1); + OCIO_CHECK_EQUAL(std::string(vt->getAlias(0)), AliasB); + OCIO_CHECK_ASSERT(!vt->hasAlias(AliasAAlt)); + + // Remove all aliases. + + vt->addAlias("other"); + OCIO_CHECK_EQUAL(vt->getNumAliases(), 2); + OCIO_CHECK_ASSERT(vt->hasAlias("other")); + vt->clearAliases(); + OCIO_CHECK_EQUAL(vt->getNumAliases(), 0); + OCIO_CHECK_ASSERT(!vt->hasAlias(AliasB)); + OCIO_CHECK_ASSERT(!vt->hasAlias("other")); +} diff --git a/tests/cpu/apphelpers/LegacyViewingPipeline_tests.cpp b/tests/cpu/apphelpers/LegacyViewingPipeline_tests.cpp index 1c41d9dd9c..e0bd834a8a 100644 --- a/tests/cpu/apphelpers/LegacyViewingPipeline_tests.cpp +++ b/tests/cpu/apphelpers/LegacyViewingPipeline_tests.cpp @@ -953,3 +953,135 @@ OCIO_ADD_TEST(LegacyViewingPipeline, processorWithNoOpLook) OCIO_REQUIRE_ASSERT(groupTransform); OCIO_CHECK_NO_THROW(groupTransform->validate()); } + +OCIO_ADD_TEST(LegacyViewingPipeline, display_view_alias_fallback) +{ + // A LegacyViewingPipeline resolves the display and view of its DisplayViewTransform, so that + // names that are out of date still produce the same pipeline. + // + // Note that this is not simply duplicating what the DisplayViewTransform does for itself when + // it is appended to the group. It matters most for the looks: setDisplayViewTransform forces + // LooksBypass on the stored transform, so the view's looks are applied by the pipeline itself + // rather than by BuildDisplayOps. If the view name did not resolve here, the looks lookup + // would come back empty and the looks would be silently dropped. + + constexpr char CONFIG[]{ R"( +ocio_profile_version: 2.6 + +use_display_view_aliases: true + +roles: + default: raw + aces_interchange: raw + cie_xyz_d65_interchange: sRGB - Display + color_timing: raw + compositing_log: raw + scene_linear: source + +displays: + sRGB - Display: + - ! {name: view, view_transform: display_vt, display_colorspace: sRGB - Display, looks: look1} + +looks: + - ! + name: look1 + process_space: source + transform: ! {slope: [1.1, 1.2, 1.3]} + +view_transforms: + - ! + name: display_vt + aliases: [old_vt] + to_scene_reference: ! {offset: [0.3, 0.1, 0.1, 0]} + +display_colorspaces: + - ! + name: sRGB - Display + aliases: [sRGB] + to_display_reference: ! {offset: [0.25, 0.15, 0.35, 0]} + +colorspaces: + - ! + name: raw + isdata: true + + - ! + name: source + to_scene_reference: ! {offset: [0, 0.1, 0.2, 0]} +)" }; + + std::istringstream is(CONFIG); + + OCIO::ConstConfigRcPtr cfg; + OCIO_CHECK_NO_THROW(cfg = OCIO::Config::CreateFromStream(is)); + OCIO_CHECK_NO_THROW(cfg->validate()); + + // Build the pipeline for a (display, view) pair and run one pixel through it. Comparing the + // resulting values catches a dropped look, which a structural check on the transform list + // could easily miss. + auto applyPipeline = [&cfg](const char * display, const char * view, bool bypassLooks, + float * rgb) + { + OCIO::DisplayViewTransformRcPtr dt = OCIO::DisplayViewTransform::Create(); + dt->setSrc("source"); + dt->setDisplay(display); + dt->setView(view); + dt->setLooksBypass(bypassLooks); + + OCIO::LegacyViewingPipelineRcPtr vp = OCIO::LegacyViewingPipeline::Create(); + vp->setDisplayViewTransform(dt); + + OCIO::ConstProcessorRcPtr proc = vp->getProcessor(cfg, cfg->getCurrentContext()); + proc->getDefaultCPUProcessor()->applyRGB(rgb); + }; + + constexpr float srcPixel[3]{ 0.3f, 0.5f, 0.7f }; + constexpr float tolerance = 1e-6f; + + // The reference result, using the display's and the view's current names. + float ref[3]{ srcPixel[0], srcPixel[1], srcPixel[2] }; + OCIO_CHECK_NO_THROW(applyPipeline("sRGB - Display", "view", false, ref)); + + // Confirm the look actually changes the result, so that the comparisons below can't pass + // merely because the look happens to be a no-op. Setting LooksBypass before handing the + // transform to the pipeline is what makes it skip the looks (see m_dtOriginalLooksBypass). + { + float noLook[3]{ srcPixel[0], srcPixel[1], srcPixel[2] }; + OCIO_CHECK_NO_THROW(applyPipeline("sRGB - Display", "view", true, noLook)); + + OCIO_CHECK_ASSERT(std::abs(noLook[0] - ref[0]) > 1e-4f || + std::abs(noLook[1] - ref[1]) > 1e-4f || + std::abs(noLook[2] - ref[2]) > 1e-4f); + } + + // The display's old, alias-only name must give the same result. + { + float aliasedDisplay[3]{ srcPixel[0], srcPixel[1], srcPixel[2] }; + OCIO_CHECK_NO_THROW(applyPipeline("sRGB", "view", false, aliasedDisplay)); + + OCIO_CHECK_CLOSE(aliasedDisplay[0], ref[0], tolerance); + OCIO_CHECK_CLOSE(aliasedDisplay[1], ref[1], tolerance); + OCIO_CHECK_CLOSE(aliasedDisplay[2], ref[2], tolerance); + } + + // So must the view transform's old, alias-only name used as the view. This is the case that + // would silently lose the look if the view were not resolved. + { + float aliasedView[3]{ srcPixel[0], srcPixel[1], srcPixel[2] }; + OCIO_CHECK_NO_THROW(applyPipeline("sRGB - Display", "old_vt", false, aliasedView)); + + OCIO_CHECK_CLOSE(aliasedView[0], ref[0], tolerance); + OCIO_CHECK_CLOSE(aliasedView[1], ref[1], tolerance); + OCIO_CHECK_CLOSE(aliasedView[2], ref[2], tolerance); + } + + // And both being out of date at once, since the view is resolved against the resolved display. + { + float bothAliased[3]{ srcPixel[0], srcPixel[1], srcPixel[2] }; + OCIO_CHECK_NO_THROW(applyPipeline("sRGB", "old_vt", false, bothAliased)); + + OCIO_CHECK_CLOSE(bothAliased[0], ref[0], tolerance); + OCIO_CHECK_CLOSE(bothAliased[1], ref[1], tolerance); + OCIO_CHECK_CLOSE(bothAliased[2], ref[2], tolerance); + } +} diff --git a/tests/cpu/transforms/DisplayViewTransform_tests.cpp b/tests/cpu/transforms/DisplayViewTransform_tests.cpp index 082ea3ee0e..66cd32e952 100644 --- a/tests/cpu/transforms/DisplayViewTransform_tests.cpp +++ b/tests/cpu/transforms/DisplayViewTransform_tests.cpp @@ -1491,3 +1491,376 @@ environment: { FILE: cdl_test1.cc } dt->setView("View18"); OCIO_CHECK_ASSERT(!CollectContextVariables(*cfg, *cfg->getCurrentContext(), *dt, usedContextVars)); } + +OCIO_ADD_TEST(DisplayViewTransform, use_display_name_alias) +{ + // Test that USE_DISPLAY_NAME will find a display color space where the display name + // is an alias. + + constexpr const char * SIMPLE_CONFIG{ R"( +ocio_profile_version: 2 + +roles: + default: raw + +shared_views: + - ! {name: view1, view_transform: display_vt, display_colorspace: } + +displays: + sRGB - Display: + - ! [view1] + sRGB: + - ! [view1] + +view_transforms: + - ! + name: display_vt + to_scene_reference: ! {offset: [0.3, 0.1, 0.1, 0]} + +display_colorspaces: + - ! + name: sRGB - Display + aliases: [sRGB] + to_display_reference: ! {offset: [0.25, 0.15, 0.35, 0]} + +colorspaces: + - ! + name: raw + isdata: true + + - ! + name: source + to_scene_reference: ! {offset: [0.11, 0.12, 0.13, 0]} +)" }; + + std::istringstream is; + is.str(SIMPLE_CONFIG); + OCIO::ConstConfigRcPtr config; + OCIO_CHECK_NO_THROW(config = OCIO::Config::CreateFromStream(is)); + OCIO_CHECK_NO_THROW(config->validate()); + + const std::string display{ "sRGB - Display" }; + const std::string aliasDisplay{ "sRGB" }; + const std::string view{ "view1" }; + + auto dt = OCIO::DisplayViewTransform::Create(); + dt->setSrc("source"); + dt->setView(view.c_str()); + + // Build once with the display name matching the display color space name. + dt->setDisplay(display.c_str()); + OCIO::OpRcPtrVec currentOps; + OCIO_CHECK_NO_THROW(OCIO::BuildDisplayOps(currentOps, *config, config->getCurrentContext(), + *dt, OCIO::TRANSFORM_DIR_FORWARD)); + OCIO_CHECK_NO_THROW(currentOps.validate()); + OCIO_REQUIRE_EQUAL(currentOps.size(), 5); // (includes gpu allocation no-ops) + + // Build again with the display name matching an alias. The result must be identical. + dt->setDisplay(aliasDisplay.c_str()); + OCIO::OpRcPtrVec aliasedOps; + OCIO_CHECK_NO_THROW(OCIO::BuildDisplayOps(aliasedOps, *config, config->getCurrentContext(), + *dt, OCIO::TRANSFORM_DIR_FORWARD)); + OCIO_CHECK_NO_THROW(aliasedOps.validate()); + OCIO_REQUIRE_EQUAL(aliasedOps.size(), currentOps.size()); + + for (size_t i = 0; i < aliasedOps.size(); ++i) + { + auto opA = OCIO_DYNAMIC_POINTER_CAST(aliasedOps[i]); + auto opB = OCIO_DYNAMIC_POINTER_CAST(currentOps[i]); + OCIO_CHECK_ASSERT(*opA->data() == *opB->data()); + } +} + +OCIO_ADD_TEST(DisplayViewTransform, display_alias_fallback) +{ + // Validate that BuildDisplayOps resolves a display using Config::getCanonicalDisplayName, + // so that renaming a display in a config (while keeping the old name as an alias on the + // associated display color space) doesn't break a DisplayViewTransform still using the old + // name. + + constexpr char CONFIG[]{ R"( +ocio_profile_version: 2.6 + +# Opt-in to display aliasing. +use_display_view_aliases: true + +roles: + default: raw + aces_interchange: raw + cie_xyz_d65_interchange: sRGB - Display + color_timing: raw + compositing_log: raw + scene_linear: raw + +shared_views: + - ! {name: view, view_transform: display_vt, display_colorspace: } + +displays: + sRGB - Display: + - ! [view] + +view_transforms: + - ! + name: display_vt + to_scene_reference: ! {offset: [0.3, 0.1, 0.1, 0]} + +display_colorspaces: + - ! + name: sRGB - Display + aliases: [sRGB] + to_display_reference: ! {offset: [0.25, 0.15, 0.35, 0]} + +colorspaces: + - ! + name: raw + isdata: true + + - ! + name: source + to_scene_reference: ! {offset: [0, 0.1, 0.2, 0]} +)" }; + + std::istringstream is; + is.str(CONFIG); + + OCIO::ConstConfigRcPtr config; + OCIO_CHECK_NO_THROW(config = OCIO::Config::CreateFromStream(is)); + OCIO_CHECK_NO_THROW(config->validate()); + + const std::string display{ "sRGB - Display" }; + const std::string oldDisplayName{ "sRGB" }; + const std::string view{ "view" }; + + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName(oldDisplayName.c_str())), display); + + auto dt = OCIO::DisplayViewTransform::Create(); + dt->setSrc("source"); + dt->setView(view.c_str()); + + // Build once using the display's current name. + dt->setDisplay(display.c_str()); + OCIO::OpRcPtrVec currentOps; + OCIO_CHECK_NO_THROW(OCIO::BuildDisplayOps(currentOps, *config, config->getCurrentContext(), + *dt, OCIO::TRANSFORM_DIR_FORWARD)); + OCIO_CHECK_NO_THROW(currentOps.validate()); + OCIO_REQUIRE_EQUAL(currentOps.size(), 5); // (includes gpu allocation no-ops) + + // Build again using only the display's old, alias-only name. The result must be identical. + dt->setDisplay(oldDisplayName.c_str()); + OCIO::OpRcPtrVec aliasedOps; + OCIO_CHECK_NO_THROW(OCIO::BuildDisplayOps(aliasedOps, *config, config->getCurrentContext(), + *dt, OCIO::TRANSFORM_DIR_FORWARD)); + OCIO_CHECK_NO_THROW(aliasedOps.validate()); + OCIO_REQUIRE_EQUAL(aliasedOps.size(), currentOps.size()); + + for (size_t i = 0; i < aliasedOps.size(); ++i) + { + auto opA = OCIO_DYNAMIC_POINTER_CAST(aliasedOps[i]); + auto opB = OCIO_DYNAMIC_POINTER_CAST(currentOps[i]); + OCIO_CHECK_ASSERT(*opA->data() == *opB->data()); + } + + // A display name that cannot be resolved at all -- not an existing display, and not a + // display color space name or alias either -- must still throw, referencing the name the + // caller actually provided. + dt->setDisplay("not a display"); + OCIO::OpRcPtrVec badOps; + OCIO_CHECK_THROW_WHAT(OCIO::BuildDisplayOps(badOps, *config, config->getCurrentContext(), + *dt, OCIO::TRANSFORM_DIR_FORWARD), + OCIO::Exception, + "DisplayViewTransform error. Display 'not a display' not found."); +} + +OCIO_ADD_TEST(DisplayViewTransform, view_alias_fallback) +{ + // Validate that BuildDisplayOps resolves a view using Config::getCanonicalViewName, so + // that renaming a view in a config (while keeping the old name as an alias) doesn't + // break a DisplayViewTransform still using the old name as its "view". + + constexpr char CONFIG[]{ R"( +ocio_profile_version: 2.6 + +# Opt-in to view aliasing. +use_display_view_aliases: true + +roles: + default: raw + aces_interchange: raw + cie_xyz_d65_interchange: sRGB - Display + color_timing: raw + compositing_log: raw + scene_linear: raw + +displays: + sRGB - Display: + - ! {name: view, view_transform: display_vt, display_colorspace: sRGB - Display, looks: look1} + +looks: + - ! + name: look1 + process_space: source + transform: ! {slope: [1.1, 1.2, 1.3]} + +view_transforms: + - ! + name: display_vt + aliases: [old_vt] + to_scene_reference: ! {offset: [0.3, 0.1, 0.1, 0]} + +display_colorspaces: + - ! + name: sRGB - Display + to_display_reference: ! {offset: [0.25, 0.15, 0.35, 0]} + +colorspaces: + - ! + name: raw + isdata: true + + - ! + name: source + to_scene_reference: ! {offset: [0, 0.1, 0.2, 0]} +)" }; + + std::istringstream is; + is.str(CONFIG); + + OCIO::ConstConfigRcPtr config; + OCIO_CHECK_NO_THROW(config = OCIO::Config::CreateFromStream(is)); + OCIO_CHECK_NO_THROW(config->validate()); + + const std::string display{ "sRGB - Display" }; + const std::string view{ "view" }; + const std::string oldViewTransformName{ "old_vt" }; + + OCIO_CHECK_EQUAL(std::string(config->getCanonicalViewName(display.c_str(), + oldViewTransformName.c_str())), + view); + + auto dt = OCIO::DisplayViewTransform::Create(); + dt->setSrc("source"); + dt->setDisplay(display.c_str()); + + // Build once using the view's current name. + dt->setView(view.c_str()); + OCIO::OpRcPtrVec currentOps; + OCIO_CHECK_NO_THROW(OCIO::BuildDisplayOps(currentOps, *config, config->getCurrentContext(), + *dt, OCIO::TRANSFORM_DIR_FORWARD)); + OCIO_CHECK_NO_THROW(currentOps.validate()); + OCIO_REQUIRE_EQUAL(currentOps.size(), 7); // (includes gpu allocation no-ops) + + // Build again using only the view transform's old, alias-only name as the "view". The result + // must be identical. Note that the view has a look, so this also verifies that the resolved + // view name is used to look up the view's looks, not just its color space and view transform. + dt->setView(oldViewTransformName.c_str()); + OCIO::OpRcPtrVec aliasedOps; + OCIO_CHECK_NO_THROW(OCIO::BuildDisplayOps(aliasedOps, *config, config->getCurrentContext(), + *dt, OCIO::TRANSFORM_DIR_FORWARD)); + OCIO_CHECK_NO_THROW(aliasedOps.validate()); + OCIO_REQUIRE_EQUAL(aliasedOps.size(), currentOps.size()); + + for (size_t i = 0; i < aliasedOps.size(); ++i) + { + auto opA = OCIO_DYNAMIC_POINTER_CAST(aliasedOps[i]); + auto opB = OCIO_DYNAMIC_POINTER_CAST(currentOps[i]); + OCIO_CHECK_ASSERT(*opA->data() == *opB->data()); + } + + // A view name that cannot be resolved at all -- not an existing view, and not a view + // transform or named transform name or alias used by this display either -- must still + // throw, referencing the name the caller actually provided. + dt->setView("not a view"); + OCIO::OpRcPtrVec badOps; + OCIO_CHECK_THROW_WHAT(OCIO::BuildDisplayOps(badOps, *config, config->getCurrentContext(), + *dt, OCIO::TRANSFORM_DIR_FORWARD), + OCIO::Exception, + "DisplayViewTransform error. The display 'sRGB - Display' does not " + "have view 'not a view'."); +} + +OCIO_ADD_TEST(DisplayViewTransform, context_variables_with_resolved_display_view) +{ + // Validate that CollectContextVariables resolves the (display, view) pair the same way + // BuildDisplayOps does, so that the context variables of the display color space are found + // even when the view uses or when the display name is aliased. + + constexpr const char * OCIO_CONFIG{ R"( +ocio_profile_version: 2.6 + +use_display_view_aliases: true + +environment: { FILE: cdl_test1.cc } + +roles: + default: raw + aces_interchange: raw + cie_xyz_d65_interchange: sRGB - Display + color_timing: raw + compositing_log: raw + scene_linear: source + +file_rules: + - ! {name: Default, colorspace: default} + +shared_views: + - ! {name: view, view_transform: display_vt, display_colorspace: } + +displays: + sRGB - Display: + - ! [view] + - ! {name: plain_view, colorspace: source} + +view_transforms: + - ! + name: display_vt + to_scene_reference: ! {offset: [0.3, 0.1, 0.1, 0]} + +display_colorspaces: + - ! + name: sRGB - Display + aliases: [sRGB] + to_display_reference: ! {src: $FILE} + +colorspaces: + - ! + name: raw + isdata: true + + - ! + name: source + allocation: uniform +)" }; + + std::istringstream is; + is.str(OCIO_CONFIG); + + OCIO::ConfigRcPtr cfg; + OCIO_CHECK_NO_THROW(cfg = OCIO::Config::CreateFromStream(is)->createEditableCopy()); + cfg->setSearchPath(OCIO::GetTestFilesDir().c_str()); + OCIO_CHECK_NO_THROW(cfg->validate()); + + OCIO::ContextRcPtr usedContextVars = OCIO::Context::Create(); + + auto dt = OCIO::DisplayViewTransform::Create(); + dt->setSrc("source"); + + // A view with no context variables anywhere: neither the source nor the color space it + // targets uses one. Returns false. + dt->setDisplay("sRGB - Display"); + dt->setView("plain_view"); + OCIO_CHECK_ASSERT(!CollectContextVariables(*cfg, *cfg->getCurrentContext(), *dt, + usedContextVars)); + + // The shared view's display color space is , so finding the context + // variable requires resolving that to the display color space named after the display. + // The context variable is found: returns true. + dt->setView("view"); + OCIO_CHECK_ASSERT(CollectContextVariables(*cfg, *cfg->getCurrentContext(), *dt, + usedContextVars)); + + // The display's old, alias-only name is resolved too, so the same context variable is found. + // The context variable is found: returns true. + dt->setDisplay("sRGB"); + OCIO_CHECK_ASSERT(CollectContextVariables(*cfg, *cfg->getCurrentContext(), *dt, + usedContextVars)); +} diff --git a/tests/python/ConfigTest.py b/tests/python/ConfigTest.py index 62c2c7d043..f0992fd1f4 100644 --- a/tests/python/ConfigTest.py +++ b/tests/python/ConfigTest.py @@ -798,6 +798,129 @@ def test_canonical_name(self): self.assertEqual(cfg.getCanonicalName('Alias1'), 'nt1') self.assertEqual(cfg.getCanonicalName('Test1'), 'nt1') + def test_use_display_view_aliases(self): + # Test the getUseDisplayViewAliases/setUseDisplayViewAliases methods, and that they gate + # the getCanonicalDisplayName/getCanonicalViewName fallbacks. + + cfg = OCIO.Config() + self.assertFalse(cfg.getUseDisplayViewAliases()) + + cfg.setUseDisplayViewAliases(True) + self.assertTrue(cfg.getUseDisplayViewAliases()) + cfg.setUseDisplayViewAliases(False) + self.assertFalse(cfg.getUseDisplayViewAliases()) + + # Build a config where a display and a view transform have both been renamed, keeping + # their old names alive as aliases. + cfg.setVersion(2, 6) + + dcs = OCIO.ColorSpace( + referenceSpace=OCIO.REFERENCE_SPACE_DISPLAY, + name='sRGB - Display', + aliases=['sRGB']) + dcs.setTransform(OCIO.MatrixTransform(), OCIO.COLORSPACE_DIR_FROM_REFERENCE) + cfg.addColorSpace(dcs) + + vt = OCIO.ViewTransform( + referenceSpace=OCIO.REFERENCE_SPACE_SCENE, + name='vt_new', + aliases=['vt_old']) + vt.setTransform(OCIO.MatrixTransform(), OCIO.VIEWTRANSFORM_DIR_FROM_REFERENCE) + cfg.addViewTransform(vt) + + cfg.addDisplayView('sRGB - Display', 'view', viewTransform='vt_new', + displayColorSpaceName='sRGB - Display') + + # The fallback is disabled by default: only exact matches resolve. + self.assertEqual(cfg.getCanonicalDisplayName('sRGB - Display'), 'sRGB - Display') + self.assertEqual(cfg.getCanonicalDisplayName('sRGB'), '') + self.assertEqual(cfg.getCanonicalViewName('sRGB - Display', 'view'), 'view') + self.assertEqual(cfg.getCanonicalViewName('sRGB - Display', 'vt_old'), '') + + # Once enabled, both the display and view transform aliases resolve. + cfg.setUseDisplayViewAliases(True) + self.assertEqual(cfg.getCanonicalDisplayName('sRGB'), 'sRGB - Display') + self.assertEqual(cfg.getCanonicalViewName('sRGB - Display', 'vt_old'), 'view') + + def test_display_description(self): + # Test that getDisplayDescription borrows the description of the display's associated + # display color space: an exact name match always works, but matching only via one of + # the color space's aliases requires getUseDisplayViewAliases. + + cfg = OCIO.Config() + cfg.setVersion(2, 6) + + dcs = OCIO.ColorSpace( + referenceSpace=OCIO.REFERENCE_SPACE_DISPLAY, + name='sRGB - Display', + aliases=['sRGB'], + description='The sRGB display.') + dcs.setTransform(OCIO.MatrixTransform(), OCIO.COLORSPACE_DIR_FROM_REFERENCE) + cfg.addColorSpace(dcs) + + self.assertFalse(cfg.getUseDisplayViewAliases()) + self.assertEqual(cfg.getDisplayDescription('sRGB - Display'), 'The sRGB display.') + self.assertEqual(cfg.getDisplayDescription('sRGB'), '') + + cfg.setUseDisplayViewAliases(True) + self.assertEqual(cfg.getDisplayDescription('sRGB'), 'The sRGB display.') + + self.assertEqual(cfg.getDisplayDescription('does not exist'), '') + + def test_resolved_display_view_color_space_name(self): + # Test that getResolvedDisplayViewColorSpaceName returns the color space actually used by + # a (display, view) pair, rather than the colorspace attribute as written in the config. + + cfg = OCIO.Config() + cfg.setVersion(2, 6) + + cfg.addColorSpace(OCIO.ColorSpace(name='raw')) + + dcs = OCIO.ColorSpace( + referenceSpace=OCIO.REFERENCE_SPACE_DISPLAY, + name='sRGB - Display', + aliases=['sRGB']) + dcs.setTransform(OCIO.MatrixTransform(), OCIO.COLORSPACE_DIR_FROM_REFERENCE) + cfg.addColorSpace(dcs) + + vt = OCIO.ViewTransform( + referenceSpace=OCIO.REFERENCE_SPACE_SCENE, + name='vt_new', + aliases=['vt_old']) + vt.setTransform(OCIO.MatrixTransform(), OCIO.VIEWTRANSFORM_DIR_FROM_REFERENCE) + cfg.addViewTransform(vt) + + cfg.addDisplayView('sRGB - Display', 'view1', 'raw') + cfg.addDisplayView('sRGB - Display', 'view2', viewTransform='vt_old', + displayColorSpaceName='') + + # A plain view behaves like getDisplayViewColorSpaceName. + self.assertEqual( + cfg.getResolvedDisplayViewColorSpaceName('sRGB - Display', 'view1'), 'raw') + + # resolves to the display's own color space, without needing the + # display/view alias fallback to be enabled. + self.assertFalse(cfg.getUseDisplayViewAliases()) + self.assertEqual( + cfg.getDisplayViewColorSpaceName('sRGB - Display', 'view2'), '') + self.assertEqual( + cfg.getResolvedDisplayViewColorSpaceName('sRGB - Display', 'view2'), 'sRGB - Display') + + # Resolving an out-of-date display or view name is opt-in. + self.assertEqual(cfg.getResolvedDisplayViewColorSpaceName('sRGB', 'view1'), '') + self.assertEqual(cfg.getResolvedDisplayViewColorSpaceName('sRGB - Display', 'vt_new'), '') + + cfg.setUseDisplayViewAliases(True) + self.assertEqual(cfg.getResolvedDisplayViewColorSpaceName('sRGB', 'view1'), 'raw') + self.assertEqual( + cfg.getResolvedDisplayViewColorSpaceName('sRGB', 'vt_new'), 'sRGB - Display') + + # Nonexistent display or view. + self.assertEqual( + cfg.getResolvedDisplayViewColorSpaceName('sRGB - Display', 'not a view'), '') + self.assertEqual(cfg.getResolvedDisplayViewColorSpaceName('not a display', 'view1'), '') + self.assertEqual(cfg.getResolvedDisplayViewColorSpaceName('', 'view1'), '') + def test_virtual_display(self): # Test platform agnostic virtual display interface. diff --git a/tests/python/ViewTransformTest.py b/tests/python/ViewTransformTest.py index bb9e9e3f31..cb6df5fafe 100644 --- a/tests/python/ViewTransformTest.py +++ b/tests/python/ViewTransformTest.py @@ -61,11 +61,13 @@ def test_copy(self): vt.setTransform(mat, OCIO.VIEWTRANSFORM_DIR_TO_REFERENCE) vt.setTransform(direction=OCIO.VIEWTRANSFORM_DIR_FROM_REFERENCE, transform=mat) vt.addCategory('cat1') + vt.addAlias('alias1') other = copy.deepcopy(vt) self.assertFalse(other is vt) self.assertEqual(other.getName(), vt.getName()) + self.assertEqual(list(other.getAliases()), list(vt.getAliases())) self.assertEqual(other.getFamily(), vt.getFamily()) self.assertEqual(other.getDescription(), vt.getDescription()) self.assertEqual( @@ -85,6 +87,68 @@ def test_name(self): vt.setName('test name') self.assertEqual(vt.getName(), 'test name') + def test_aliases(self): + """ + Test ViewTransform aliases. + """ + + vt = OCIO.ViewTransform() + self.assertEqual(vt.getName(), '') + aliases = vt.getAliases() + self.assertEqual(len(aliases), 0) + + vt.addAlias('alias1') + aliases = vt.getAliases() + self.assertEqual(len(aliases), 1) + self.assertEqual(aliases[0], 'alias1') + self.assertTrue(vt.hasAlias('alias1')) + self.assertTrue(vt.hasAlias('aLiaS1')) + self.assertFalse(vt.hasAlias('alias2')) + + vt.addAlias('alias2') + aliases = vt.getAliases() + self.assertEqual(len(aliases), 2) + self.assertEqual(aliases[0], 'alias1') + self.assertEqual(aliases[1], 'alias2') + self.assertTrue(vt.hasAlias('alias2')) + + # Alias is already there, not added. + + vt.addAlias('Alias2') + aliases = vt.getAliases() + self.assertEqual(len(aliases), 2) + self.assertEqual(aliases[0], 'alias1') + self.assertEqual(aliases[1], 'alias2') + + # Name might remove an alias. + + vt.setName('name') + aliases = vt.getAliases() + self.assertEqual(len(aliases), 2) + + vt.setName('alias2') + aliases = vt.getAliases() + self.assertEqual(len(aliases), 1) + self.assertEqual(aliases[0], 'alias1') + + vt.removeAlias('alias1') + aliases = vt.getAliases() + self.assertEqual(len(aliases), 0) + + vt.addAlias('alias3') + vt.addAlias('alias4') + self.assertEqual(len(vt.getAliases()), 2) + vt.clearAliases() + self.assertEqual(len(vt.getAliases()), 0) + + # Aliases may also be set via the constructor. + + vt = OCIO.ViewTransform(name='vt_name', aliases=['a1', 'a2']) + aliases = vt.getAliases() + self.assertEqual(len(aliases), 2) + self.assertEqual(aliases[0], 'a1') + self.assertEqual(aliases[1], 'a2') + def test_family(self): """ Test get/setFamily.