diff --git a/docs/guides/authoring/displays_views.rst b/docs/guides/authoring/displays_views.rst index 1cdc8c51bf..6953710378 100644 --- a/docs/guides/authoring/displays_views.rst +++ b/docs/guides/authoring/displays_views.rst @@ -89,6 +89,11 @@ The keys allowed with a View are: and the '-' character to apply in reverse. See :ref:`config-looks` * ``rule``: The viewing rule to be used with this View. See :ref:`config-viewing-rules` * ``description``: A description string for this View. +* ``aliases``: Alternative names that can be used to refer to this View. Unlike display + aliases, resolving a view by one of its aliases is always active (it does not depend + on ``use_display_aliases``). An alias must not collide with the name or an alias of + another view used by the same display (whether display-defined or a referenced + shared view). Requires ``ocio_profile_version`` 2.6 or higher. Note that a View may use either the colorspace key or it may use both the view_transform and dispay_colorspace keys. No other combinations @@ -132,6 +137,27 @@ A View Transform may use the following keys: .. TODO: Good spot for an example in a future revision. + +``use_display_aliases`` +^^^^^^^^^^^^^^^^^^^^^^^ + +Optional. Activates aliases for display names. + +By default, the arguments to DisplayViewTransform must be the exact display name found +in the display section of the config. However, if ``ocio_profile_version`` is 2.6 or +higher, ``use_display_aliases`` may be set to true. This allows a display to be +referred to by the name or aliases of its corresponding display ColorSpace. This +config-level attribute defaults to false and must be omitted from the config file if +its value is not "true". + +Note that this does not affect resolving a view by one of its ``aliases`` (see the View +keys above), which is always active regardless of this setting. + +.. code-block:: yaml + + use_display_aliases: true + + ``default_view_transform`` ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/releases/ocio_2_6.rst b/docs/releases/ocio_2_6.rst index d2c023bde2..035a69f85c 100644 --- a/docs/releases/ocio_2_6.rst +++ b/docs/releases/ocio_2_6.rst @@ -15,6 +15,81 @@ calendar year 2027. New Feature Guide ================= +Display and View Aliases +************************ + +For Config Authors +++++++++++++++++++ + +Config authors may now define alias names for displays and views that will be recognized +in a ``DisplayViewTransform`` as equivalent to the canonical names. Similar to color space +aliases, this allows config authors to evolve naming of display and views over time while +still providing backwards compatibility for the older names. + +Display aliasing is opt-in and the config author must set the new config-level attribute +``use_display_aliases: true``. With that enabled, the name or aliases of the display color +space for the display will be considered synonyms for that display. + +View aliasing is allowed via a new ``aliases`` attribute on a view or shared view. These are +always active, independent of whether ``use_display_aliases`` is enabled. Similar to other +Yaml lists, these are separated by a comma. Names that contain an embedded comma are +enclosed in quotes to prevent it from being used as a separator. + +Please note that the ``active_displays`` and ``active_views`` lists must use the canonical names +rather than aliases. Similarly, view aliases in a shared view may not be used when referring to +the shared view in a display's views. + +For virtual displays, aliases may be used with shared views but are +not suppored for display-defined virtual views. + +As an example, in the following config file excerpt, "srgb_rec709_display" could be used as +a display alias and "aces2_sdr_view" could be used as a view alias when creating a +``DisplayViewTransform``. + +.. code-block:: yaml + + use_display_aliases: true + + shared_views: + - ! {name: ACES 2.0 - SDR, view_transform: ACES 2.0 - SDR, + display_colorspace: , aliases: [aces2_sdr_view]} + + displays: + sRGB - Display: + - ! [ACES 2.0 - SDR] + + display_colorspaces: + - ! + name: sRGB - Display + aliases: [srgb_rec709_display] + +For Developers +++++++++++++++ + +If application code is currently calling ``Config::getDisplayViewColorSpaceName``, you will +probably want to change that to ``Config::getResolvedDisplayViewColorSpaceName`` so that it +will handle aliases. Note that this resolves the ```` token, as well. + +Any existing calls to ``DisplayViewTransform`` should automatically work with aliases, without +any changes. + +The new functions ``Config::getCanonicalDisplayName`` and ``Config::getCanonicalViewName`` may +be used to convert aliases back to the primary name used in the config. + + +Display Descriptions +******************** + +For Developers +++++++++++++++ + +On a related note, the new ``Config::getDisplayDescription`` allows applications to get a +description for a display. This is sourced from the description attribute of the display +color space that implements the display. (Views already have a description attribute +available for config authors to set.) This enables applications to provide tool-tips or +similar help text for both displays and views. + + New Fixed Function Transforms ***************************** diff --git a/include/OpenColorIO/OpenColorIO.h b/include/OpenColorIO/OpenColorIO.h index ad294fc805..ee2cbefeed 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 resolved to their canonical names for comparison. */ bool isColorSpaceUsed(const char * name) const noexcept; @@ -863,6 +862,17 @@ class OCIOEXPORT Config void addSharedView(const char * view, const char * viewTransformName, const char * colorSpaceName, const char * looks, const char * ruleName, const char * description); + /** + * \brief As above, but also sets the view's aliases. + * + * Will throw if view or colorSpaceName are null or empty, or if an alias collides with the + * name or an alias of another shared view. + */ + void addSharedView(const char * view, const char * viewTransformName, + const char * colorSpaceName, const char * looks, + const char * ruleName, const char * description, + const std::vector & aliases); + /// Remove a shared view. Will throw if the view does not exist. void removeSharedView(const char * view); @@ -918,6 +928,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. @@ -927,6 +939,28 @@ class OCIOEXPORT Config /// Returns the description attribute of a (display, view) pair. const char * getDisplayViewDescription(const char * display, const char * view) const noexcept; + /** + * \brief Get the number of aliases of a (display, view) pair. If display is null or + * empty, config shared views are used. + */ + int getNumDisplayViewAliases(const char * display, const char * view) const noexcept; + + /** + * \brief Get an alias of a (display, view) pair, by index. If display is null or empty, + * config shared views are used. + * + * Returns "" if the (display, view) pair does not exist or index is out of range. + */ + const char * getDisplayViewAlias(const char * display, const char * view, + int index) const noexcept; + + /** + * \brief Convenience method to check whether a (display, view) pair has a specific alias. + * If display is null or empty, config shared views are used. + */ + bool hasDisplayViewAlias(const char * display, const char * view, + const char * alias) const noexcept; + /** * \brief Determine if a display and view exist. * @@ -961,6 +995,20 @@ class OCIOEXPORT Config const char * colorSpaceName, const char * looks, const char * ruleName, const char * description); + /** + * \brief As above, but also sets the view's aliases. + * + * Will throw if: + * * Display, view or colorSpace are null or empty. + * * Display already has a shared view with the same name. + * * An alias collides with the name or an alias of another view in this display, whether + * display-defined or a shared view referenced by this display. + */ + void addDisplayView(const char * display, const char * view, const char * viewTransformName, + const char * colorSpaceName, const char * looks, + const char * ruleName, const char * description, + const std::vector & aliases); + /** * \brief Add a (reference to a) shared view to a display. * @@ -985,6 +1033,83 @@ class OCIOEXPORT Config /// Clear all the displays. void clearDisplays(); + /** + * Methods that involve resolving display and view aliases. + * + */ + + /** + * \brief This property on the Config object allows config authors to use aliases for + * display names. This feature is off by default. + * + * Corresponds to the "use_display_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 getUseDisplayAliases() const noexcept; + void setUseDisplayAliases(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::getUseDisplayAliases 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, this looks for + * a view used by the display (whether display-defined or a referenced shared view, active + * or inactive) that has viewName as one of its aliases (see \ref Config::addDisplayView + * and \ref Config::addSharedView). + * + * 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, + * via \ref Config::getCanonicalDisplayName and \ref Config::getCanonicalViewName. And + * unlike that function, the displayName may not be empty. + * + * 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::getUseDisplayAliases 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. * @@ -1114,6 +1239,9 @@ class OCIOEXPORT Config * the config file as well as any modifications made by the client app. These functions * only get and set what is in the config object and do not take into account the override * and thus may not represent the actual user experience. + * + * Display aliases may not be used in the active list, use \ref Config::getCanonicalDisplayName + * to convert any aliases to their canonical name. */ /// Set all active displays at once as a comma or colon delimited string. This replaces any /// previous contents of the list. @@ -1152,6 +1280,9 @@ class OCIOEXPORT Config * the config file as well as any modifications made by the client app. These functions * only get and set what is in the config object and do not take into account the override * and thus may not represent the actual user experience. + * + * View aliases may not be used in the active list, use \ref Config::getCanonicalViewName + * to convert any aliases to their canonical name. */ /// Set all active views at once as a comma or colon delimited string. This replaces any /// previous contents of the list. @@ -2555,6 +2686,7 @@ class OCIOEXPORT ViewTransform ViewTransformRcPtr createEditableCopy() const; const char * getName() const noexcept; + /// \see ColorSpace::setName void setName(const char * name) noexcept; /// \see ColorSpace::getFamily @@ -2563,6 +2695,7 @@ class OCIOEXPORT ViewTransform 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 ea9ac00298..edca6c2ea7 100644 --- a/src/OpenColorIO/Config.cpp +++ b/src/OpenColorIO/Config.cpp @@ -321,6 +321,7 @@ class Config::Impl // Misc std::vector m_defaultLumaCoefs; bool m_strictParsing; + bool m_useDisplayAliases{ 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_useDisplayAliases = rhs.m_useDisplayAliases; m_validation = rhs.m_validation; m_validationtext = rhs.m_validationtext; @@ -733,7 +735,32 @@ class Config::Impl m_validationtext = os.str(); throw Exception(m_validationtext.c_str()); } - else if (checkUseDisplayName) + else + { + // Does the shared view's name or an alias collide with the name or an alias of a + // display-defined view in this display? This is normally already prevented by + // addDisplayView and addDisplaySharedView at the point a view is added to (or + // linked to) the display. However, addSharedView may replace an already-linked + // shared view's aliases afterwards, and that call does not check if any other + // displays currently reference it, so this check acts as the defensive backstop + // for that case. + for (const auto & view : viewsOfDisplay) + { + const std::string collision = sharedViewIt->FindNamingCollision(view); + if (!collision.empty()) + { + std::ostringstream os; + os << "Config failed view validation. The display '" << display << "' "; + os << "contains a shared view '" << sharedViewIt->m_name << "' whose name "; + os << "or alias '" << collision << "' collides with the name or alias of "; + os << "the view '" << view.m_name << "' in this display."; + m_validationtext = os.str(); + throw Exception(m_validationtext.c_str()); + } + } + } + + if (checkUseDisplayName) { const auto view = *sharedViewIt; if (!view.m_viewTransform.empty() && view.useDisplayNameForColorspace()) @@ -2674,6 +2701,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 +2730,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 +2743,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 +2754,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 +2769,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 +2782,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 +2800,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 +2814,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; } @@ -3371,6 +3414,14 @@ bool Config::isViewShared(const char * dispName, const char * viewName) const void Config::addSharedView(const char * view, const char * viewTransform, const char * colorSpace, const char * looks, const char * rule, const char * description) +{ + addSharedView(view, viewTransform, colorSpace, looks, rule, description, {}); +} + +void Config::addSharedView(const char * view, const char * viewTransform, + const char * colorSpace, const char * looks, + const char * rule, const char * description, + const std::vector & aliases) { if (!view || !*view) { @@ -3385,7 +3436,35 @@ void Config::addSharedView(const char * view, const char * viewTransform, } ViewVec & views = getImpl()->m_sharedViews; - AddView(views, view, viewTransform, colorSpace, looks, rule, description); + + const View candidate(view, viewTransform, colorSpace, looks, rule, description, aliases); + + // Keep shared views unambiguous among themselves (independent of which displays end up + // referencing them), by checking the candidate's name/aliases against every sibling shared + // view (other than the one being replaced, if this call is updating an existing one). + // + // Note: this does not check the candidate against the views of displays that may already + // reference an existing shared view of this name. That case (redefining an already-linked + // shared view's aliases so that they collide with that display's own views) is instead + // caught defensively by validateSharedView. + for (const auto & existing : views) + { + if (StringUtils::Compare(existing.m_name, view)) + { + continue; + } + const std::string collision = candidate.FindNamingCollision(existing); + if (!collision.empty()) + { + std::ostringstream os; + os << "Shared view '" << view << "' could not be added to config: '" << collision; + os << "' is already used as the name or alias of the shared view '"; + os << existing.m_name << "'."; + throw Exception(os.str().c_str()); + } + } + + AddView(views, view, viewTransform, colorSpace, looks, rule, description, aliases); getImpl()->m_displayCache.clear(); @@ -3608,6 +3687,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); @@ -3638,6 +3719,30 @@ const char * Config::getDisplayViewDescription(const char * display, const char return viewPtr ? viewPtr->m_description.c_str() : ""; } +int Config::getNumDisplayViewAliases(const char * display, const char * view) const noexcept +{ + const View * viewPtr = getImpl()->getView(display, view); + return viewPtr ? static_cast(viewPtr->m_aliases.size()) : 0; +} + +const char * Config::getDisplayViewAlias(const char * display, const char * view, + int index) const noexcept +{ + const View * viewPtr = getImpl()->getView(display, view); + if (!viewPtr || index < 0 || static_cast(index) >= viewPtr->m_aliases.size()) + { + return ""; + } + return viewPtr->m_aliases[index].c_str(); +} + +bool Config::hasDisplayViewAlias(const char * display, const char * view, + const char * alias) const noexcept +{ + const View * viewPtr = getImpl()->getView(display, view); + return viewPtr && viewPtr->hasAlias(alias); +} + bool Config::hasView(const char * dispName, const char * viewName) const { // This returns null if either the display or view doesn't exist. @@ -3687,6 +3792,27 @@ void Config::addDisplaySharedView(const char * display, const char * sharedView) throw Exception(os.str().c_str()); } + // If the shared view is already defined, make sure its aliases don't collide with this + // display's own views either (a plain name collision is already excluded above, since + // shared view names and display-defined view names share one namespace per display). + const auto sharedViewIt = FindView(getImpl()->m_sharedViews, sharedView); + if (sharedViewIt != getImpl()->m_sharedViews.end()) + { + for (const auto & existing : existingViews) + { + const std::string collision = sharedViewIt->FindNamingCollision(existing); + if (!collision.empty()) + { + std::ostringstream os; + os << "Shared view '" << sharedView << "' could not be added to display '"; + os << display << "': '" << collision; + os << "' is already used as the name or alias of the view '"; + os << existing.m_name << "' in this display."; + throw Exception(os.str().c_str()); + } + } + } + StringUtils::StringVec & views = iter->second.m_sharedViews; if (StringUtils::Contain(views, sharedView)) { @@ -3707,12 +3833,20 @@ void Config::addDisplaySharedView(const char * display, const char * sharedView) void Config::addDisplayView(const char * display, const char * view, const char * colorSpace, const char * looks) { - addDisplayView(display, view, nullptr, colorSpace, looks, nullptr, nullptr); + addDisplayView(display, view, nullptr, colorSpace, looks, nullptr, nullptr, {}); } void Config::addDisplayView(const char * display, const char * view, const char * viewTransform, const char * colorSpace, const char * looks, const char * rule, const char * description) +{ + addDisplayView(display, view, viewTransform, colorSpace, looks, rule, description, {}); +} + +void Config::addDisplayView(const char * display, const char * view, const char * viewTransform, + const char * colorSpace, const char * looks, + const char * rule, const char * description, + const std::vector & aliases) { if (!display || !*display) { @@ -3738,7 +3872,7 @@ void Config::addDisplayView(const char * display, const char * view, const char getImpl()->m_displays[curSize].first = display; getImpl()->m_displays[curSize].second.m_views.push_back(View(view, viewTransform, colorSpace, looks, rule, - description)); + description, aliases)); getImpl()->m_displayCache.clear(); } else @@ -3751,8 +3885,48 @@ void Config::addDisplayView(const char * display, const char * view, const char throw Exception(os.str().c_str()); } + const View candidate(view, viewTransform, colorSpace, looks, rule, description, aliases); + + // Check the candidate's name/aliases against sibling views in this display (other than + // the one being replaced, if this call is updating an existing view by that name). + for (const auto & existing : iter->second.m_views) + { + if (StringUtils::Compare(existing.m_name, view)) + { + continue; + } + const std::string collision = candidate.FindNamingCollision(existing); + if (!collision.empty()) + { + std::ostringstream os; + os << "View '" << view << "' could not be added to display '" << display; + os << "': '" << collision << "' is already used as the name or alias of the "; + os << "view '" << existing.m_name << "' in this display."; + throw Exception(os.str().c_str()); + } + } + + // Check the candidate's name/aliases against the shared views this display references. + for (const auto & sharedViewName : iter->second.m_sharedViews) + { + const auto sharedViewIt = FindView(getImpl()->m_sharedViews, sharedViewName); + if (sharedViewIt == getImpl()->m_sharedViews.end()) + { + continue; + } + const std::string collision = candidate.FindNamingCollision(*sharedViewIt); + if (!collision.empty()) + { + std::ostringstream os; + os << "View '" << view << "' could not be added to display '" << display; + os << "': '" << collision << "' is already used as the name or alias of the "; + os << "shared view '" << sharedViewIt->m_name << "' referenced by this display."; + throw Exception(os.str().c_str()); + } + } + ViewVec & views = iter->second.m_views; - AddView(views, view, viewTransform, colorSpace, looks, rule, description); + AddView(views, view, viewTransform, colorSpace, looks, rule, description, aliases); } AutoMutex lock(getImpl()->m_cacheidMutex); @@ -3823,6 +3997,258 @@ void Config::clearDisplays() getImpl()->resetCacheIDs(); } +/////////////////////////////////////////////////////////////////////////// +// Unlike the above functions, these are set up to work with display +// and view aliases. + +bool Config::getUseDisplayAliases() const noexcept +{ + return getImpl()->m_useDisplayAliases; +} + +void Config::setUseDisplayAliases(bool enabled) noexcept +{ + getImpl()->m_useDisplayAliases = enabled; + + AutoMutex lock(getImpl()->m_cacheidMutex); + getImpl()->resetCacheIDs(); +} + +namespace +{ + +// A display might have a display color space with a matching name purely by coincidence. +// This checks whether one of the display's views (display-defined or shared, active or +// inactive) actually resolves, directly or via , to the given color space. +bool DisplayUsesColorSpace(const Config & config, const ViewPtrVec & views, + const ConstColorSpaceRcPtr & cs) +{ + for (const auto * view : views) + { + // Only requiring one (rather than all) the display's views to match since sometimes a + // display will have utility views such as "Raw" that don't rely on a display color space. + + if (view->useDisplayNameForColorspace()) + { + // The display_colorspace is . + return true; + } + ConstColorSpaceRcPtr viewCs = config.getColorSpace(view->m_colorspace.c_str()); + if (viewCs && StringUtils::Compare(viewCs->getName(), cs->getName())) + { + // The canonical name of the view's display_colorspace (or colorspace) + // equals that of cs. + return true; + } + } + return false; +} + +} // namespace + +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_useDisplayAliases) + { + 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 ""; + } + + // A candidate display found by name match alone isn't enough: it must also actually use + // this color space, i.e. have a view whose display_colorspace is or + // that resolves (by name or alias) to this same color space. Otherwise, a display that + // merely happens to share a name with an unrelated color space would incorrectly match. + auto usesColorSpace = [this, &cs](DisplayMap::const_iterator candidateIter) -> bool + { + // Consider both display-defined views and shared views used by this display, and both + // active and inactive views. + return DisplayUsesColorSpace(*this, getImpl()->getViews(candidateIter->second), cs); + }; + + iter = FindDisplay(getImpl()->m_displays, cs->getName()); + if (iter != getImpl()->m_displays.end() && usesColorSpace(iter)) + { + // 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() && usesColorSpace(iter)) + { + // 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. + // + // The reason to allow this match is because resolves via + // getColorSpace(displanName), which works via color space aliases, independent + // of display aliases. Therefore, configs may already contain display color + // spaces that are set up to only match the display name via their aliases. + // In addition, this allows for renaming a display but keeping the display + // color space name unchanged, if that were desired for some reason. + // + // See the aliased_display_name test in Config_tests.cpp. + 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 ""; + } + + // If the color space was found via its aliases, exit now if display alias support is off. + if (!StringUtils::Compare(cs->getName(), display) && !getImpl()->m_useDisplayAliases) + { + return ""; + } + + // Check that there is an actual display with this name. + const char * canonicalDisplay = getCanonicalDisplayName(cs->getName()); + if (!canonicalDisplay || !*canonicalDisplay) + { + return ""; + } + + // Confirm that the display has a view that references this display color space. + // This rules out matches that coincidentally share the same name but are unrelated. + DisplayMap::const_iterator iter = FindDisplay(getImpl()->m_displays, canonicalDisplay); + if (iter == getImpl()->m_displays.end() || + !DisplayUsesColorSpace(*this, getImpl()->getViews(iter->second), cs)) + { + 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. (That fallback is gated + // by m_useDisplayAliases; view alias resolution below is not, since view aliases are + // an explicit attribute of the view rather than a heuristic fallback.) + 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; + } + + // Otherwise, look for a view used by this display (whether display-defined or a + // referenced shared view, active or inactive) that has viewName as an alias. + DisplayMap::const_iterator iter = FindDisplay(getImpl()->m_displays, resolvedDisplay); + if (iter == getImpl()->m_displays.end()) + { + // The requested display does not exist. + return ""; + } + + const ViewPtrVec views = getImpl()->getViews(iter->second); + for (const auto * candidate : views) + { + if (candidate->hasAlias(viewName)) + { + return candidate->m_name.c_str(); + } + } + + return ""; +} + +/////////////////////////////////////////////////////////////////////////// + bool Config::hasVirtualView(const char * viewName) const { const char * cs = getVirtualDisplayViewColorSpaceName(viewName); @@ -4140,6 +4566,8 @@ int Config::instantiateDisplayFromICCProfile(const char * ICCProfileFilepath) return getImpl()->instantiateDisplay("", monitorDescription, ICCProfileFilepath); } +/////////////////////////////////////////////////////////////////////////// + void Config::setActiveDisplays(const char * displays) { getImpl()->m_activeDisplays.clear(); @@ -4340,6 +4768,8 @@ int Config::getNumActiveViews() const return static_cast(getImpl()->m_activeViews.size()); } +/////////////////////////////////////////////////////////////////////////// + int Config::getNumDisplaysAll() const noexcept { return static_cast(getImpl()->m_displays.size()); @@ -5133,15 +5563,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 +5572,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) { @@ -6010,6 +6455,51 @@ void Config::Impl::checkVersionConsistency() const } } + // Check for use_display_aliases. + + if (hexVersion < 0x02060000 && m_useDisplayAliases) + { + throw Exception("Config failed validation: use_display_aliases is true and config " + "version is less than 2.6."); + } + + // Check for view aliases. + + if (hexVersion < 0x02060000) + { + auto checkViewAliases = [](const ViewVec & views) -> const View * + { + for (const auto & view : views) + { + if (!view.m_aliases.empty()) + { + return &view; + } + } + return nullptr; + }; + + if (const View * view = checkViewAliases(m_sharedViews)) + { + std::ostringstream os; + os << "Config failed validation. The shared view '" << view->m_name << "' "; + os << "has aliases and config version is less than 2.6."; + throw Exception(os.str().c_str()); + } + // Note: the virtual display's views are not checked here, since there is currently no + // way to set aliases on them (addVirtualDisplayView has no aliases argument). + for (const auto & display : m_displays) + { + if (const View * view = checkViewAliases(display.second.m_views)) + { + std::ostringstream os; + os << "Config failed validation. The view '" << view->m_name << "' in display '"; + os << display.first << "' has aliases and config version is less than 2.6."; + throw Exception(os.str().c_str()); + } + } + } + // Check for new Look properties. if (hexVersion < 0x02050000) diff --git a/src/OpenColorIO/Display.cpp b/src/OpenColorIO/Display.cpp index aebc37fb08..05f0d4984f 100644 --- a/src/OpenColorIO/Display.cpp +++ b/src/OpenColorIO/Display.cpp @@ -50,7 +50,8 @@ ViewVec::iterator FindView(ViewVec & vec, const std::string & name) void AddView(ViewVec & views, const char * name, const char * viewTransform, const char * displayColorSpace, const char * looks, - const char * rule, const char * description) + const char * rule, const char * description, + const StringUtils::StringVec & aliases) { if (displayColorSpace && 0 == Platform::Strcasecmp(displayColorSpace, OCIO_VIEW_USE_DISPLAY_NAME)) { @@ -59,7 +60,8 @@ void AddView(ViewVec & views, const char * name, const char * viewTransform, auto view = FindView(views, name); if (view == views.end()) { - views.push_back(View(name, viewTransform, displayColorSpace, looks, rule, description)); + views.push_back(View(name, viewTransform, displayColorSpace, looks, rule, description, + aliases)); } else { @@ -68,6 +70,7 @@ void AddView(ViewVec & views, const char * name, const char * viewTransform, (*view).m_looks = looks ? looks : ""; (*view).m_rule = rule ? rule : ""; (*view).m_description = description ? description : ""; + (*view).m_aliases = aliases; } } diff --git a/src/OpenColorIO/Display.h b/src/OpenColorIO/Display.h index b2ffd85b50..8b129a3301 100644 --- a/src/OpenColorIO/Display.h +++ b/src/OpenColorIO/Display.h @@ -28,6 +28,7 @@ struct View std::string m_looks; // Might be empty. std::string m_rule; // Might be empty. std::string m_description; // Might be empty. + StringUtils::StringVec m_aliases; // Might be empty. View() = default; @@ -36,13 +37,15 @@ struct View const char * colorspace, const char * looks, const char * rule, - const char * description) + const char * description, + const StringUtils::StringVec & aliases = StringUtils::StringVec()) : m_name(name ? name : "") , m_viewTransform(viewTransform ? viewTransform : "") , m_colorspace(colorspace ? colorspace : "") , m_looks(looks ? looks : "") , m_rule(rule ? rule : "") , m_description(description ? description : "") + , m_aliases(aliases) { } // Make sure that csname is not null. @@ -54,6 +57,31 @@ struct View { return UseDisplayName(m_colorspace.c_str()); } + + bool hasAlias(const char * alias) const + { + if (!alias || !*alias) return false; + return StringUtils::Contain(m_aliases, alias); + } + + // Returns the string (name or alias) of 'this' that collides with the name or an alias of + // 'other', or an empty string if the two views' identities do not overlap. Comparisons are + // case-insensitive. Used to keep view names/aliases unambiguous within whatever scope two + // views can both be resolved in (e.g. the same display, or the config's shared views). + std::string FindNamingCollision(const View & other) const + { + if (StringUtils::Compare(m_name, other.m_name)) return m_name; + for (const auto & alias : m_aliases) + { + if (StringUtils::Compare(alias, other.m_name)) return alias; + } + if (other.hasAlias(m_name.c_str())) return m_name; + for (const auto & alias : m_aliases) + { + if (other.hasAlias(alias.c_str())) return alias; + } + return std::string(); + } }; typedef std::vector ViewVec; @@ -63,7 +91,8 @@ ViewVec::iterator FindView(ViewVec & vec, const std::string & name); void AddView(ViewVec & views, const char * name, const char * viewTransform, const char * displayColorSpace, const char * looks, - const char * rule, const char * description); + const char * rule, const char * description, + const StringUtils::StringVec & aliases = StringUtils::StringVec()); // Display can be part of the list of displays (DisplayMap) of a config. struct Display diff --git a/src/OpenColorIO/OCIOYaml.cpp b/src/OpenColorIO/OCIOYaml.cpp index 9010dcb606..8209bf773b 100644 --- a/src/OpenColorIO/OCIOYaml.cpp +++ b/src/OpenColorIO/OCIOYaml.cpp @@ -453,6 +453,13 @@ inline void load(const YAML::Node& node, View& v) { load(iter->second, v.m_description); } + else if (key == "aliases") + { + // This uses load(const YAML::Node & node, StringUtils::StringVec & x), so the + // Yaml parser handles unquoting view names with embedded quotes, symmetric with + // the save function below, rather than using SplitStringEnvStyle. + load(iter->second, v.m_aliases); + } else { LogUnknownKeyWarning(node, iter->first); @@ -500,6 +507,12 @@ inline void save(YAML::Emitter& out, const View & view) { out << YAML::Key << "rule" << YAML::Value << view.m_rule; } + if (!view.m_aliases.empty()) + { + // The Yaml parser automatically quotes view name aliases that contain commas, so + // they are not confused with separators. No need to use JoinStringEnvStyle here. + out << YAML::Key << "aliases" << YAML::Value << view.m_aliases; + } saveDescription(out, view.m_description.c_str()); out << YAML::EndMap; } @@ -4670,7 +4683,7 @@ inline void load(const YAML::Node& node, ConfigRcPtr & config, const char* filen config->addSharedView(view.m_name.c_str(), view.m_viewTransform.c_str(), view.m_colorspace.c_str(), view.m_looks.c_str(), view.m_rule.c_str(), - view.m_description.c_str()); + view.m_description.c_str(), view.m_aliases); } } else if (key == "displays") @@ -4700,7 +4713,7 @@ inline void load(const YAML::Node& node, ConfigRcPtr & config, const char* filen config->addDisplayView(display.c_str(), view.m_name.c_str(), view.m_viewTransform.c_str(), view.m_colorspace.c_str(), view.m_looks.c_str(), view.m_rule.c_str(), - view.m_description.c_str()); + view.m_description.c_str(), view.m_aliases); } else if (node.Tag() == "Views") { @@ -4729,6 +4742,11 @@ inline void load(const YAML::Node& node, ConfigRcPtr & config, const char* filen { View view; load(val, view); + if (!view.m_aliases.empty()) + { + throwValueError(node.Tag(), iter->first, + "Aliases are not supported for virtual display views."); + } config->addVirtualDisplayView(view.m_name.c_str(), view.m_viewTransform.c_str(), view.m_colorspace.c_str(), @@ -4754,6 +4772,11 @@ inline void load(const YAML::Node& node, ConfigRcPtr & config, const char* filen } } } + else if (key == "use_display_aliases") + { + load(iter->second, boolval); + config->setUseDisplayAliases(boolval); + } else if(key == "active_displays") { StringUtils::StringVec display; @@ -5030,6 +5053,21 @@ inline void load(const YAML::Node& node, ConfigRcPtr & config, const char* filen } } +// Build the vector for the View struct used below to build up what gets passed to +// save(YAML::Emitter&, const View&). +StringUtils::StringVec GetViewAliasVec(const Config & config, const char * display, + const char * name) +{ + StringUtils::StringVec aliases; + const int numAliases = config.getNumDisplayViewAliases(display, name); + aliases.reserve(numAliases); + for (int i = 0; i < numAliases; i++) + { + aliases.push_back(config.getDisplayViewAlias(display, name, i)); + } + return aliases; +} + inline void save(YAML::Emitter & out, const Config & config) { std::stringstream ss; @@ -5184,7 +5222,8 @@ inline void save(YAML::Emitter & out, const Config & config) config.getDisplayViewColorSpaceName(nullptr, name), config.getDisplayViewLooks(nullptr, name), config.getDisplayViewRule(nullptr, name), - config.getDisplayViewDescription(nullptr, name) }; + config.getDisplayViewDescription(nullptr, name), + GetViewAliasVec(config, nullptr, name) }; save(out, dview); } out << YAML::EndSeq; @@ -5213,7 +5252,8 @@ inline void save(YAML::Emitter & out, const Config & config) config.getDisplayViewColorSpaceName(display, name), config.getDisplayViewLooks(display, name), config.getDisplayViewRule(display, name), - config.getDisplayViewDescription(display, name) }; + config.getDisplayViewDescription(display, name), + GetViewAliasVec(config, display, name) }; save(out, dview); } @@ -5272,6 +5312,12 @@ inline void save(YAML::Emitter & out, const Config & config) out << YAML::Newline; out << YAML::Newline; + + if (config.getUseDisplayAliases()) + { + out << YAML::Key << "use_display_aliases" << YAML::Value << true; + } + out << YAML::Key << "active_displays"; StringUtils::StringVec active_displays; int nDisplays = config.getNumActiveDisplays(); 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..3e416cb9cd 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(); + // A view may have its own aliases. Resolving a view by one of its aliases is always + // active. 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..81feec7255 100644 --- a/src/bindings/python/PyConfig.cpp +++ b/src/bindings/python/PyConfig.cpp @@ -39,6 +39,7 @@ enum ConfigIterator IT_DISPLAY_ALL, IT_DISPLAY_VIEW_TYPE, IT_VIRTUAL_DISPLAY_VIEW, + IT_DISPLAY_VIEW_ALIAS, }; using EnvironmentVarNameIterator = PyIterator; @@ -65,6 +66,8 @@ using ViewForColorSpaceIterator = PyIterator; using ViewForViewTypeIterator = PyIterator; +using DisplayViewAliasIterator = PyIterator; using ActiveDisplaysListIterator = PyIterator; using ActiveViewsListIterator = PyIterator; using LookNameIterator = PyIterator; @@ -146,7 +149,11 @@ void bindPyConfig(py::module & m) py::class_( clsConfig, "ViewForViewTypeIterator"); - auto clsActiveDisplaysListIterator = + auto clsDisplayViewAliasIterator = + py::class_( + clsConfig, "DisplayViewAliasIterator"); + + auto clsActiveDisplaysListIterator = py::class_( clsConfig, "ActiveDisplaysListIterator"); @@ -346,6 +353,10 @@ void bindPyConfig(py::module & m) DOC(Config, isStrictParsingEnabled)) .def("setStrictParsingEnabled", &Config::setStrictParsingEnabled, "enabled"_a, DOC(Config, setStrictParsingEnabled)) + .def("getUseDisplayAliases", &Config::getUseDisplayAliases, + DOC(Config, getUseDisplayAliases)) + .def("setUseDisplayAliases", &Config::setUseDisplayAliases, "enabled"_a, + DOC(Config, setUseDisplayAliases)) .def("setInactiveColorSpaces", &Config::setInactiveColorSpaces, "inactiveColorSpaces"_a, DOC(Config, setInactiveColorSpaces)) .def("getInactiveColorSpaces", &Config::getInactiveColorSpaces, @@ -408,11 +419,13 @@ void bindPyConfig(py::module & m) const char *, const char *, const char *, - const char *)) &Config::addSharedView, - "view"_a, "viewTransformName"_a, "colorSpaceName"_a, + const char *, + const std::vector &)) &Config::addSharedView, + "view"_a, "viewTransformName"_a, "colorSpaceName"_a, "looks"_a = "", - "ruleName"_a = "", - "description"_a = "", + "ruleName"_a = "", + "description"_a = "", + "aliases"_a = std::vector(), DOC(Config, addSharedView)) .def("removeSharedView", &Config::removeSharedView, "view"_a, DOC(Config, removeSharedView)) @@ -432,6 +445,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 +482,18 @@ 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("getDisplayViewAliases", [](ConfigRcPtr & self, + const std::string & display, const std::string & view) + { + return DisplayViewAliasIterator(self, display, view); + }, + "display"_a, "view"_a) + .def("hasDisplayViewAlias", &Config::hasDisplayViewAlias, + "display"_a, "view"_a, "alias"_a, + DOC(Config, hasDisplayViewAlias)) .def("getDisplayViewLooks", &Config::getDisplayViewLooks, "display"_a, "view"_a, DOC(Config, getDisplayViewLooks)) .def("getDisplayViewRule", &Config::getDisplayViewRule, "display"_a, "view"_a, @@ -479,18 +510,20 @@ void bindPyConfig(py::module & m) "display"_a, "view"_a, "colorSpaceName"_a, "looks"_a = "", DOC(Config, addDisplayView)) - .def("addDisplayView", - (void (Config::*)(const char *, - const char *, - const char *, - const char *, + .def("addDisplayView", + (void (Config::*)(const char *, + const char *, + const char *, const char *, const char *, - const char *)) &Config::addDisplayView, - "display"_a, "view"_a, "viewTransform"_a, "displayColorSpaceName"_a, + const char *, + const char *, + const std::vector &)) &Config::addDisplayView, + "display"_a, "view"_a, "viewTransform"_a, "displayColorSpaceName"_a, "looks"_a = "", - "ruleName"_a = "", - "description"_a = "", + "ruleName"_a = "", + "description"_a = "", + "aliases"_a = std::vector(), DOC(Config, addDisplayView)) .def("isViewShared", &Config::isViewShared, "display"_a, "view"_a, DOC(Config, isViewShared)) @@ -1294,6 +1327,29 @@ void bindPyConfig(py::module & m) std::get<1>(it.m_args).c_str(), i); }); + clsDisplayViewAliasIterator + .def("__len__", [](DisplayViewAliasIterator & it) + { return it.m_obj->getNumDisplayViewAliases(std::get<0>(it.m_args).c_str(), + std::get<1>(it.m_args).c_str()); }) + .def("__getitem__", [](DisplayViewAliasIterator & it, int i) + { + it.checkIndex(i, it.m_obj->getNumDisplayViewAliases(std::get<0>(it.m_args).c_str(), + std::get<1>(it.m_args).c_str())); + return it.m_obj->getDisplayViewAlias(std::get<0>(it.m_args).c_str(), + std::get<1>(it.m_args).c_str(), i); + }) + .def("__iter__", [](DisplayViewAliasIterator & it) -> DisplayViewAliasIterator & + { + return it; + }) + .def("__next__", [](DisplayViewAliasIterator & it) + { + int i = it.nextIndex(it.m_obj->getNumDisplayViewAliases(std::get<0>(it.m_args).c_str(), + std::get<1>(it.m_args).c_str())); + return it.m_obj->getDisplayViewAlias(std::get<0>(it.m_args).c_str(), + std::get<1>(it.m_args).c_str(), i); + }); + clsActiveDisplaysListIterator .def("__len__", [](ActiveDisplaysListIterator & it) { return it.m_obj->getNumActiveDisplays(); }) .def("__getitem__", [](ActiveDisplaysListIterator & it, int i) diff --git a/tests/cpu/Config_tests.cpp b/tests/cpu/Config_tests.cpp index 6a029bba2d..ed72a5e17c 100644 --- a/tests/cpu/Config_tests.cpp +++ b/tests/cpu/Config_tests.cpp @@ -6622,7 +6622,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_aliases: true environment: {} @@ -6631,18 +6633,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, + aliases: [oldview2]} - ! {name: view3, colorspace: data_space} + - ! {name: view4, view_transform: vt2, display_colorspace: , + aliases: [oldview4]} view_transforms: - ! name: vt1 from_scene_reference: ! {min_in_value: 0., min_out_value: 0.} + - ! + name: vt2 + from_scene_reference: ! {offset: [0.02, 0.03, 0.04, 0]} + colorspaces: - ! name: raw1 @@ -6678,6 +6690,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"( @@ -6876,10 +6894,61 @@ 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", "oldview2", "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", "oldview2", + 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", "oldview4", + 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 aliasing and ensure the result now throws. (View alias resolution is + // always active, but the display fails to resolve.) + { + OCIO::ConfigRcPtr config1NoFallback = config1->createEditableCopy(); + config1NoFallback->setUseDisplayAliases(false); + OCIO_CHECK_THROW_WHAT(OCIO::Config::GetProcessorFromConfigs( + config2, "test2", "aces2", config1NoFallback, "olddisplayname", "oldview2", "aces1", + OCIO::TRANSFORM_DIR_FORWARD), + OCIO::Exception, + "DisplayViewTransform error. Display 'olddisplayname' not found."); + } // 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); @@ -7343,6 +7412,710 @@ default_view_transform: view_transform OCIO::Exception, "a non-empty color space name is needed"); } +namespace +{ +// Checks that a (display, view) pair's aliases, as returned by Config::getDisplayViewAlias +// match the expected value, preserving order. +void CheckDisplayViewAliases(const OCIO::ConstConfigRcPtr & config, const char * display, + const char * view, const StringUtils::StringVec & expected, + int line) +{ + OCIO_REQUIRE_EQUAL_FROM(config->getNumDisplayViewAliases(display, view), + (int)expected.size(), line); + for (size_t i = 0; i < expected.size(); i++) + { + OCIO_CHECK_EQUAL_FROM(std::string(config->getDisplayViewAlias(display, view, (int)i)), + expected[i], line); + } +} +} // namespace + +OCIO_ADD_TEST(Config, view_aliases) +{ + // Views (display-defined or shared) may have their own aliases, set directly via + // Config::addDisplayView/addSharedView, and queried via Config::getDisplayViewAlias and + // Config::hasDisplayViewAlias. This test covers that machinery: setting aliases, querying + // them, keeping them unambiguous, requiring config version 2.6, and surviving a YAML + // serialize/reload round-trip. See the aliased_view_name test below for how aliases are + // resolved by Config::getCanonicalViewName. + + constexpr const char * SIMPLE_CONFIG{ R"( +ocio_profile_version: 2.6 + +roles: + default: ref1 + aces_interchange: ref1 + color_timing: ref1 + compositing_log: ref1 + scene_linear: ref1 + +shared_views: + - ! {name: sview, colorspace: ref1, aliases: [sv_alias]} + +displays: + display1: + - ! {name: view1, colorspace: ref1, aliases: [v1_alias2, v1_alias]} + - ! {name: view2, colorspace: cs1} + - ! [sview] + display2: + - ! {name: view3, colorspace: ref1, aliases: [v1_alias]} + - ! [sview] + +colorspaces: + - ! + name: ref1 + + - ! + name: cs1 +)" }; + + std::istringstream is; + is.str(SIMPLE_CONFIG); + OCIO::ConstConfigRcPtr config; + OCIO_CHECK_NO_THROW(config = OCIO::Config::CreateFromStream(is)); + + // Test getNumDisplayViewAliases/getDisplayViewAlias return the aliases in view order. + CheckDisplayViewAliases(config, "display1", "view1", { "v1_alias2", "v1_alias" }, __LINE__); + // A view with no aliases, an unknown view, or an unknown display all return none. + CheckDisplayViewAliases(config, "display1", "view2", {}, __LINE__); + CheckDisplayViewAliases(config, "display1", "not_a_view", {}, __LINE__); + CheckDisplayViewAliases(config, "not_a_display", "view1", {}, __LINE__); + + // A null/empty display means look up the view among the config's shared views, following + // the same convention as some of the other getters. + CheckDisplayViewAliases(config, nullptr, "sview", { "sv_alias" }, __LINE__); + CheckDisplayViewAliases(config, "", "sview", { "sv_alias" }, __LINE__); + + // Test hasDisplayViewAlias convenience method. + OCIO_CHECK_ASSERT(config->hasDisplayViewAlias("display1", "view1", "v1_alias")); + OCIO_CHECK_ASSERT(!config->hasDisplayViewAlias("display1", "view1", "not_an_alias")); + OCIO_CHECK_ASSERT(config->hasDisplayViewAlias(nullptr, "sview", "sv_alias")); + + // Aliases only need to be unique among the views used by a single display, so the same + // alias string ("v1_alias") can be reused by an unrelated view in a different display. + OCIO_CHECK_ASSERT(config->hasDisplayViewAlias("display2", "view3", "v1_alias")); + + // Aliases containing a comma must be quoted in the Yaml, e.g., similar to how the + // active_views list works. + { + OCIO::ConfigRcPtr edit = config->createEditableCopy(); + OCIO_CHECK_NO_THROW(edit->addDisplayView("display1", "view4", nullptr, "ref1", "", "", "", + { "a,b", "plain" })); + CheckDisplayViewAliases(edit, "display1", "view4", { "a,b", "plain" }, __LINE__); + OCIO_CHECK_ASSERT(edit->hasDisplayViewAlias("display1", "view4", "a,b")); + OCIO_CHECK_ASSERT(edit->hasDisplayViewAlias("display1", "view4", "plain")); + + // The comma-containing alias survives a YAML serialize / reload round-trip, quoted as + // needed by the YAML emitter so that it stays a single flow-sequence entry rather than + // being split into two. + std::ostringstream os; + edit->serialize(os); + + std::istringstream reloadStream; + reloadStream.str(os.str()); + OCIO::ConstConfigRcPtr reloaded; + OCIO_CHECK_NO_THROW(reloaded = OCIO::Config::CreateFromStream(reloadStream)); + OCIO_CHECK_NO_THROW(reloaded->validate()); + + CheckDisplayViewAliases(reloaded, "display1", "view4", { "a,b", "plain" }, __LINE__); + OCIO_CHECK_ASSERT(reloaded->hasDisplayViewAlias("display1", "view4", "a,b")); + OCIO_CHECK_ASSERT(reloaded->hasDisplayViewAlias("display1", "view4", "plain")); + + // The other, comma-free aliases also survive the round-trip. + CheckDisplayViewAliases(reloaded, "display1", "view1", { "v1_alias2", "v1_alias" }, + __LINE__); + CheckDisplayViewAliases(reloaded, nullptr, "sview", { "sv_alias" }, __LINE__); + } + + // A view's alias must not collide with the name or alias of another view used by the same + // display, whether display-defined or a referenced shared view. This is checked as soon as + // the collision would occur. + { + OCIO::ConfigRcPtr edit = config->createEditableCopy(); + + // Collides with view1's alias in display1. + OCIO_CHECK_THROW_WHAT( + edit->addDisplayView("display1", "view4", nullptr, "ref1", "", "", "", { "v1_alias" }), + OCIO::Exception, "already used as the name or alias"); + + // Collides with the "sview" shared view's alias, which display1 references. + OCIO_CHECK_THROW_WHAT( + edit->addDisplayView("display1", "view4", nullptr, "ref1", "", "", "", { "sv_alias" }), + OCIO::Exception, "already used as the name or alias"); + + // No collision in display2, since display2 does not use view1 or its aliases. + OCIO_CHECK_NO_THROW( + edit->addDisplayView("display2", "view4", nullptr, "ref1", "", "", "", { "v1_alias2" })); + + // A shared view's alias must not collide with another shared view's name or alias, + // regardless of which displays reference either of them (this keeps shared views + // unambiguous when looked up directly, e.g. via a null display argument). + OCIO_CHECK_THROW_WHAT(edit->addSharedView("sview2", nullptr, "ref1", "", "", "", + { "sv_alias" }), + OCIO::Exception, "already used as the name or alias"); + + // Linking an existing shared view to a display is also checked against that display's + // own views. + OCIO_CHECK_NO_THROW(edit->addDisplayView("display3", "view5", "ref1", "")); + OCIO_CHECK_NO_THROW( + edit->addDisplayView("display3", "view5", nullptr, "ref1", "", "", "", { "sv_alias" })); + OCIO_CHECK_THROW_WHAT(edit->addDisplaySharedView("display3", "sview"), OCIO::Exception, + "already used as the name or alias"); + } + + // Defensive backstop: addSharedView can replace an already-linked shared view's aliases, + // and that call has no way to know which displays currently reference it, so this + // particular collision is only caught later, when the config is validated. + { + OCIO::ConfigRcPtr edit = config->createEditableCopy(); + OCIO_CHECK_NO_THROW(edit->addDisplayView("display3", "view5", "ref1", "")); + OCIO_CHECK_NO_THROW(edit->addDisplaySharedView("display3", "sview")); + + // No collision yet: "view5" has no alias, and "sview"'s alias is "sv_alias". + OCIO_CHECK_NO_THROW(edit->validate()); + + // Redefine "sview" so its alias now collides with "view5" in display3. addSharedView + // does not throw, since it only checks against sibling shared views. + OCIO_CHECK_NO_THROW(edit->addSharedView("sview", nullptr, "ref1", "", "", "", { "view5" })); + OCIO_CHECK_THROW_WHAT(edit->validate(), OCIO::Exception, "collides with the name or " + "alias"); + } + + // View aliases require ocio_profile_version 2.6 or higher. + { + constexpr const char * OLD_CONFIG{ R"( +ocio_profile_version: 2.5 + +roles: + default: ref1 + aces_interchange: ref1 + color_timing: ref1 + compositing_log: ref1 + scene_linear: ref1 + +colorspaces: + - ! + name: ref1 + +displays: + disp: + - ! {name: view1, colorspace: ref1} +)" }; + std::istringstream isOld; + isOld.str(OLD_CONFIG); + OCIO::ConfigRcPtr oldConfig; + OCIO_CHECK_NO_THROW( + oldConfig = OCIO::Config::CreateFromStream(isOld)->createEditableCopy()); + OCIO_CHECK_NO_THROW( + oldConfig->addDisplayView("disp", "view1", nullptr, "ref1", "", "", "", { "alias1" })); + OCIO_CHECK_THROW_WHAT(oldConfig->validate(), OCIO::Exception, "less than 2.6"); + } + + // Test aliases in virtual displays. + { + OCIO::ConfigRcPtr edit = config->createEditableCopy(); + + // A virtual display may reference a shared view that has aliases ("sview" has the + // alias "sv_alias"). + OCIO_CHECK_NO_THROW(edit->addVirtualDisplaySharedView("sview")); + OCIO_REQUIRE_EQUAL(edit->getVirtualDisplayNumViews(OCIO::VIEW_SHARED), 1); + OCIO_CHECK_EQUAL(std::string(edit->getVirtualDisplayView(OCIO::VIEW_SHARED, 0)), "sview"); + OCIO_CHECK_ASSERT(edit->hasDisplayViewAlias(nullptr, "sview", "sv_alias")); + OCIO_CHECK_NO_THROW(edit->validate()); + + // The virtual display's reference to a shared view with an alias survives a + // serialize/reload round-trip without throwing. + std::ostringstream osRoundTrip; + edit->serialize(osRoundTrip); + std::istringstream roundTripStream; + roundTripStream.str(osRoundTrip.str()); + OCIO_CHECK_NO_THROW(OCIO::Config::CreateFromStream(roundTripStream)); + + // But aliases on display-defined views on a virtual display are not supported. + // Validate that a hand-edited config file will throw if it tries to use them. + + // Config::addVirtualDisplayView has no aliases argument, so an alias is injected into + // the serialized YAML to exercise the parser's check. + OCIO_CHECK_NO_THROW(edit->addVirtualDisplayView("vview", nullptr, "ref1", "", "", "")); + + std::ostringstream os; + edit->serialize(os); + const std::string configStr = StringUtils::Replace( + os.str(), "name: vview, colorspace: ref1}", + "name: vview, colorspace: ref1, aliases: [vv_alias]}"); + + std::istringstream reloadStream; + reloadStream.str(configStr); + OCIO_CHECK_THROW_WHAT(OCIO::Config::CreateFromStream(reloadStream), OCIO::Exception, + "Aliases are not supported for virtual display views."); + } +} + +OCIO_ADD_TEST(Config, aliased_view_name) +{ + // Test that Config::getCanonicalViewName resolves a view by one of its aliases. Unlike + // display name aliasing (see the aliased_display_name test below), this resolution is + // always active, i.e. it does not depend on Config::getUseDisplayAliases. See the + // view_aliases test above for the machinery of setting and querying view aliases. + + constexpr const char * SIMPLE_CONFIG{ R"( +ocio_profile_version: 2.6 + +roles: + default: ref1 + aces_interchange: ref1 + color_timing: ref1 + compositing_log: ref1 + scene_linear: ref1 + +shared_views: + - ! {name: sview, colorspace: ref1, aliases: [sv_alias]} + +displays: + display1: + - ! {name: view1, colorspace: ref1, aliases: [v1_alias, v1_alias2]} + - ! {name: view2, colorspace: cs1} + - ! [sview] + display2: + - ! {name: view3, colorspace: ref1, aliases: [v1_alias]} + - ! [sview] + act_display: + - ! {name: act_view, colorspace: cs1} + +# The view aliasing works even if the display and/or view are inactive. +active_displays: [act_display] +active_views: [act_view] + +colorspaces: + - ! + name: ref1 + + - ! + name: cs1 +)" }; + + std::istringstream is; + is.str(SIMPLE_CONFIG); + OCIO::ConstConfigRcPtr config; + OCIO_CHECK_NO_THROW(config = OCIO::Config::CreateFromStream(is)); + + // Alias resolution is always active, regardless of getUseDisplayAliases (which only + // gates display name aliasing). + OCIO_CHECK_ASSERT(!config->getUseDisplayAliases()); + + // Resolve a display-defined view by one of its aliases. + OCIO_CHECK_EQUAL(std::string(config->getCanonicalViewName("display1", "v1_alias")), "view1"); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalViewName("display1", "v1_alias2")), "view1"); + + // Resolve a shared view by its alias, through either display that references it. + OCIO_CHECK_EQUAL(std::string(config->getCanonicalViewName("display1", "sv_alias")), "sview"); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalViewName("display2", "sv_alias")), "sview"); + + // Aliases only need to be unique among the views used by a single display, so the same + // alias string ("v1_alias") can be reused by an unrelated view in a different display. + OCIO_CHECK_EQUAL(std::string(config->getCanonicalViewName("display2", "v1_alias")), "view3"); + + // An exact view name always takes priority and needs no alias resolution. + OCIO_CHECK_EQUAL(std::string(config->getCanonicalViewName("display1", "view2")), "view2"); + + // No match at all: unknown alias, unknown display, or empty/null arguments. + OCIO_CHECK_EQUAL(std::string(config->getCanonicalViewName("display1", "not_a_view")), ""); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalViewName("not_a_display", "v1_alias")), ""); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalViewName("display1", "")), ""); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalViewName("display1", nullptr)), ""); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalViewName("", "view1")), ""); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalViewName(nullptr, "view1")), ""); + + // A comma-containing alias also resolves. + { + OCIO::ConfigRcPtr edit = config->createEditableCopy(); + OCIO_CHECK_NO_THROW(edit->addDisplayView("display1", "view4", nullptr, "ref1", "", "", "", + { "a,b", "plain" })); + OCIO_CHECK_EQUAL(std::string(edit->getCanonicalViewName("display1", "a,b")), "view4"); + } + + // Unlike getCanonicalViewName, the other Config methods that take a view name are not meant + // to resolve it against a view alias. These functions are meant to tell exactly what the + // Config object contains, which would become less clear if they resolved aliases. + OCIO_CHECK_ASSERT(config->hasView("display1", "view1")); + OCIO_CHECK_ASSERT(!config->hasView("display1", "v1_alias")); + + OCIO_CHECK_EQUAL(std::string(config->getDisplayViewTransformName("display1", "v1_alias")), ""); + OCIO_CHECK_EQUAL(std::string(config->getDisplayViewColorSpaceName("display1", "v1_alias")), ""); + OCIO_CHECK_EQUAL(std::string(config->getDisplayViewLooks("display1", "v1_alias")), ""); + OCIO_CHECK_EQUAL(std::string(config->getDisplayViewRule("display1", "v1_alias")), ""); + OCIO_CHECK_EQUAL(std::string(config->getDisplayViewDescription("display1", "v1_alias")), ""); + + OCIO::ConfigRcPtr configEdit = config->createEditableCopy(); + OCIO_CHECK_THROW_WHAT(configEdit->removeDisplayView("display1", "v1_alias"), OCIO::Exception, + "Could not find a view named 'v1_alias"); + + // Aliases may not be used in the active_views list. + { + OCIO::ConfigRcPtr edit = config->createEditableCopy(); + OCIO_REQUIRE_EQUAL(edit->getNumViews("display1"), 3); + OCIO_CHECK_NO_THROW(edit->setActiveViews("v1_alias")); + // Has no effect. + OCIO_REQUIRE_EQUAL(edit->getNumViews("display1"), 3); + OCIO_CHECK_EQUAL(std::string(edit->getView("display1", 0)), "view1"); + OCIO_CHECK_EQUAL(std::string(edit->getView("display1", 1)), "view2"); + OCIO_CHECK_EQUAL(std::string(edit->getView("display1", 2)), "sview"); + // Note: Config validation currently does not validate active_views, + // so that is not something that could be tested here. + } +} + +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", "")); + + // 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"); + + // Display aliasing is opt-in and disabled by default. + OCIO_CHECK_ASSERT(!config->getUseDisplayAliases()); + config->setUseDisplayAliases(true); + + // Resolving via the display color space's alias also requires that the display actually use + // that color space in one of its views. So far "sRGB - Display" only has view1, whose color + // space is "raw", so there is no connection to the display color space and the match fails. + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("sRGB")), ""); + + // Add a view that uses the display color space itself, satisfying that requirement. + OCIO_CHECK_NO_THROW(config->addDisplayView("sRGB - Display", "view2", "sRGB - Display", "")); + // Now the alias works. + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("sRGB")), "sRGB - Display"); + + // Remove that view, the alias no longer works. + OCIO_CHECK_NO_THROW(config->removeDisplayView("sRGB - Display", "view2")); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("sRGB")), ""); + + // Now test that a shared view that uses is sufficient to establish + // a connection between the display and a display color space. + auto vt = OCIO::ViewTransform::Create(OCIO::REFERENCE_SPACE_SCENE); + vt->setName("vt1"); + OCIO_CHECK_NO_THROW(vt->setTransform(OCIO::MatrixTransform::Create(), + OCIO::VIEWTRANSFORM_DIR_FROM_REFERENCE)); + OCIO_CHECK_NO_THROW(config->addViewTransform(vt)); + OCIO_CHECK_NO_THROW(config->addSharedView("sview", "vt1", "", "", "", "")); + OCIO_CHECK_NO_THROW(config->addDisplaySharedView("sRGB - Display", "sview")); + // The alias works again. + 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 display color space's own name doesn't match any display, its aliases are tried + // too. Here the name of "other_dcs" 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"); + otherDcs->addAlias("another_alias"); + OCIO_CHECK_NO_THROW(config->addColorSpace(otherDcs)); + + // As above, the name match alone isn't enough: "AliasedDisplay" only has view1, whose + // colorspace is "raw", so it isn't yet found to be using "other_dcs". + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("other_dcs")), ""); + + // Add a view that uses the display color space, satisfying that requirement. Note that + // the display_colorspace may resolve via an alias of the display color space. + OCIO_CHECK_NO_THROW(config->addDisplayView("AliasedDisplay", "view2", "other_alias", "")); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("other_dcs")), "AliasedDisplay"); + + // Any alias of other_dcs will find the AliasedDisplay. + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("another_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); + + // The fallback is opt-in. + config->setUseDisplayAliases(false); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("sRGB")), ""); + config->setUseDisplayAliases(true); + + // The alias resolution still works even if the display being found is not part of the + // active_displays list. + OCIO_CHECK_NO_THROW(config->setActiveDisplays("not_a_real_display")); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("sRGB")), "sRGB - Display"); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("other_dcs")), "AliasedDisplay"); + config->clearActiveDisplays(); + + // It also still works even if the display color space is in the inactive_colorspaces list. + OCIO_CHECK_NO_THROW(config->setInactiveColorSpaces("sRGB - Display, other_dcs")); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("sRGB")), "sRGB - Display"); + OCIO_CHECK_EQUAL(std::string(config->getCanonicalDisplayName("other_dcs")), "AliasedDisplay"); + config->setInactiveColorSpaces(""); + + // 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'"); + + // 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")), ""); +} + +OCIO_ADD_TEST(Config, active_display_alias) +{ + // Aliases may not be used in the active_displays list. + + OCIO::ConstConfigRcPtr config; + OCIO_CHECK_NO_THROW( + config = OCIO::Config::CreateFromBuiltinConfig("cg-config-v4.0.0_aces-v2.0_ocio-v2.5") + ); + OCIO_REQUIRE_ASSERT(config); + + OCIO::ConfigRcPtr edit = config->createEditableCopy(); + edit->setVersion(2, 6); + edit->setUseDisplayAliases(true); + + // "srgb_rec709_display" is an alias of the "sRGB - Display" display color space, and + // resolves to the "sRGB - Display" display. + OCIO_CHECK_EQUAL(std::string(edit->getCanonicalDisplayName("srgb_rec709_display")), + "sRGB - Display"); + + OCIO_REQUIRE_EQUAL(edit->getNumDisplays(), 8); + OCIO_CHECK_NO_THROW(edit->setActiveDisplays("srgb_rec709_display")); + // It has no effect, there are still eight displays. + OCIO_REQUIRE_EQUAL(edit->getNumDisplays(), 8); + OCIO_CHECK_THROW_WHAT(edit->validate(), OCIO::Exception, + "The list of active displays [srgb_rec709_display] from the config " + "file is invalid."); +} + +OCIO_ADD_TEST(Config, display_description) +{ + OCIO::ConfigRcPtr config = OCIO::Config::Create(); + config->setVersion(2, 6); + + auto raw = OCIO::ColorSpace::Create(); + raw->setName("raw"); + config->addColorSpace(raw); + + auto dcs = OCIO::ColorSpace::Create(OCIO::REFERENCE_SPACE_DISPLAY); + dcs->setName("sRGB - Display"); + dcs->addAlias("sRGB"); + dcs->setDescription("The sRGB display."); + config->addColorSpace(dcs); + + OCIO_CHECK_NO_THROW(config->addDisplayView("sRGB - Display", "view1", "raw", "")); + + // The name match alone isn't enough: the display must also actually use that color space + // in one of its views. So far "sRGB - Display" only has view1, whose color space is "raw", + // so there is no connection to the display color space and the match fails, even though it + // is an exact name match. + OCIO_CHECK_ASSERT(!config->getUseDisplayAliases()); + OCIO_CHECK_EQUAL(std::string(config->getDisplayDescription("sRGB - Display")), ""); + + // Add a view that uses the display color space itself, satisfying that requirement. + OCIO_CHECK_NO_THROW(config->addDisplayView("sRGB - Display", "view2", "sRGB - Display", "")); + + // Exact match works even though the fallback is disabled by default. + 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->setUseDisplayAliases(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 and + // even though it is actually used by a display's view (so this fails specifically because + // it isn't display-referred, not merely because it goes unused). + auto scs = OCIO::ColorSpace::Create(OCIO::REFERENCE_SPACE_SCENE); + scs->setName("scene_cs"); + scs->setDescription("A scene color space."); + config->addColorSpace(scs); + OCIO_CHECK_NO_THROW(config->addDisplayView("scene_cs", "view1", "scene_cs", "")); + 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"); + 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 has its + // own alias, "view2_old", so getCanonicalViewName can find it from that old name. + OCIO_CHECK_NO_THROW(config->addDisplayView("sRGB - Display", "view2", "vt_new", + "", "", "", "", { "view2_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 getUseDisplayAliases. + OCIO_CHECK_ASSERT(!config->getUseDisplayAliases()); + OCIO_CHECK_EQUAL(std::string(config->getDisplayViewColorSpaceName("sRGB - Display", "view2")), + ""); + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("sRGB - Display", + "view2")), + "sRGB - Display"); + + // Resolving a view by one of its own aliases is always active, unlike resolving the display + // name, which is opt-in (see getCanonicalDisplayName/getCanonicalViewName). + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("sRGB - Display", + "view2_old")), + "sRGB - Display"); + + // Resolving the display is opt-in. + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("sRGB", "view1")), + ""); + + config->setUseDisplayAliases(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 still resolved from its own alias, and the display may be out of date at + // the same time. + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("sRGB - Display", + "view2_old")), + "sRGB - Display"); + OCIO_CHECK_EQUAL(std::string(config->getResolvedDisplayViewColorSpaceName("sRGB", + "view2_old")), + "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. @@ -7784,6 +8557,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::getUseDisplayAliases, which was off. + // Turning it on must not change any result. + OCIO::ConfigRcPtr editableConfig = config->createEditableCopy(); + OCIO_CHECK_ASSERT(!editableConfig->getUseDisplayAliases()); + OCIO_CHECK_NO_THROW(editableConfig->setUseDisplayAliases(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/apphelpers/LegacyViewingPipeline_tests.cpp b/tests/cpu/apphelpers/LegacyViewingPipeline_tests.cpp index 1c41d9dd9c..34ddb80346 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_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, aliases: [old_view]} + +looks: + - ! + name: look1 + process_space: source + transform: ! {slope: [1.1, 1.2, 1.3]} + +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(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'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_view", 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_view", 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..be61ad0094 100644 --- a/tests/cpu/transforms/DisplayViewTransform_tests.cpp +++ b/tests/cpu/transforms/DisplayViewTransform_tests.cpp @@ -1491,3 +1491,382 @@ 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. Note that this has nothing to do with whether use_display_aliases + // is set. It is simply a consequence of the fact that USE_DISPLAY_NAME is resolved + // via a call to config->getColorSpace(displayName), which handles aliases. + + 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] + # Note that the "sRGB" display needs to be present. This test relies on color space aliases + # rather than display aliases. + 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_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 one of the view's + // aliases) doesn't break a DisplayViewTransform still using the old name as its "view". + + constexpr char CONFIG[]{ R"( +ocio_profile_version: 2.6 + +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, aliases: [old_view]} + +looks: + - ! + name: look1 + process_space: source + transform: ! {slope: [1.1, 1.2, 1.3]} + +view_transforms: + - ! + name: display_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 oldViewName{ "old_view" }; + + OCIO_CHECK_EQUAL(std::string(config->getCanonicalViewName(display.c_str(), + oldViewName.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'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(oldViewName.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 an alias of + // a view 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_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)); + OCIO_CHECK_EQUAL(0, usedContextVars->getNumStringVars()); + + // 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)); + OCIO_CHECK_EQUAL(1, usedContextVars->getNumStringVars()); + OCIO_CHECK_EQUAL(std::string("FILE"), usedContextVars->getStringVarNameByIndex(0)); + + // 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)); + OCIO_CHECK_EQUAL(1, usedContextVars->getNumStringVars()); + OCIO_CHECK_EQUAL(std::string("FILE"), usedContextVars->getStringVarNameByIndex(0)); +} diff --git a/tests/python/ConfigTest.py b/tests/python/ConfigTest.py index 62c2c7d043..48529abae6 100644 --- a/tests/python/ConfigTest.py +++ b/tests/python/ConfigTest.py @@ -798,6 +798,188 @@ def test_canonical_name(self): self.assertEqual(cfg.getCanonicalName('Alias1'), 'nt1') self.assertEqual(cfg.getCanonicalName('Test1'), 'nt1') + def test_use_display_aliases(self): + # Test the getUseDisplayAliases/setUseDisplayAliases methods, and that they gate + # the getCanonicalDisplayName fallback, but not view alias resolution, which is + # always active since a view's aliases are an explicit attribute of the view. + + cfg = OCIO.Config() + self.assertFalse(cfg.getUseDisplayAliases()) + + cfg.setUseDisplayAliases(True) + self.assertTrue(cfg.getUseDisplayAliases()) + cfg.setUseDisplayAliases(False) + self.assertFalse(cfg.getUseDisplayAliases()) + + # Build a config where a display has been renamed, keeping its old name available as + # an alias, and a view that has been renamed, keeping its old name available as one + # of the view's 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) + + cfg.addDisplayView('sRGB - Display', 'view', viewTransform='', + displayColorSpaceName='sRGB - Display', looks='', ruleName='', + description='', aliases=['view_old']) + + # The display alias fallback is disabled by default: only an exact match resolves. + self.assertEqual(cfg.getCanonicalDisplayName('sRGB - Display'), 'sRGB - Display') + self.assertEqual(cfg.getCanonicalDisplayName('sRGB'), '') + + # But view alias resolution is always active, regardless of getUseDisplayAliases. + self.assertEqual(cfg.getCanonicalViewName('sRGB - Display', 'view'), 'view') + self.assertEqual(cfg.getCanonicalViewName('sRGB - Display', 'view_old'), 'view') + + # Once enabled, the display alias resolves too. + cfg.setUseDisplayAliases(True) + self.assertEqual(cfg.getCanonicalDisplayName('sRGB'), 'sRGB - Display') + self.assertEqual(cfg.getCanonicalViewName('sRGB - Display', 'view_old'), 'view') + + def test_display_view_aliases(self): + # Test Config.getDisplayViewAliases and Config.hasDisplayViewAlias, for both a + # display-defined view and a shared view. + + cfg = OCIO.Config() + cfg.setVersion(2, 6) + cfg.addColorSpace(OCIO.ColorSpace(name='raw')) + + cfg.addDisplayView('display1', 'view1', viewTransform='', + displayColorSpaceName='raw', looks='', ruleName='', + description='', aliases=['alias1', 'alias2']) + + self.assertEqual(list(cfg.getDisplayViewAliases('display1', 'view1')), + ['alias1', 'alias2']) + self.assertTrue(cfg.hasDisplayViewAlias('display1', 'view1', 'alias1')) + self.assertTrue(cfg.hasDisplayViewAlias('display1', 'view1', 'ALIAS2')) + self.assertFalse(cfg.hasDisplayViewAlias('display1', 'view1', 'alias3')) + + # A view with no aliases. + cfg.addDisplayView('display1', 'view2', 'raw') + self.assertEqual(list(cfg.getDisplayViewAliases('display1', 'view2')), []) + self.assertFalse(cfg.hasDisplayViewAlias('display1', 'view2', 'alias1')) + + # A shared view, looked up the same way as Config.hasView (an empty display finds it + # among the config's shared views). + cfg.addSharedView('shared1', '', 'raw', aliases=['shared_alias']) + cfg.addDisplaySharedView('display1', 'shared1') + + self.assertEqual(list(cfg.getDisplayViewAliases('', 'shared1')), ['shared_alias']) + self.assertTrue(cfg.hasDisplayViewAlias('', 'shared1', 'shared_alias')) + self.assertFalse(cfg.hasDisplayViewAlias('display1', 'shared1', 'unknown')) + + # Unknown display, view, or display/view combination. + self.assertEqual(list(cfg.getDisplayViewAliases('display1', 'not_a_view')), []) + self.assertFalse(cfg.hasDisplayViewAlias('display1', 'not_a_view', 'alias1')) + self.assertFalse(cfg.hasDisplayViewAlias('not_a_display', 'view1', 'alias1')) + + # An alias may itself contain a comma; since aliases are set as a real list, no + # quoting is needed (unlike the comma-delimited Config.setActiveViews string). + cfg.addDisplayView('display1', 'view3', viewTransform='', + displayColorSpaceName='raw', looks='', ruleName='', + description='', aliases=['alias,with,comma', 'alias4']) + + self.assertTrue(cfg.hasDisplayViewAlias('display1', 'view3', 'alias,with,comma')) + self.assertTrue(cfg.hasDisplayViewAlias('display1', 'view3', 'alias4')) + self.assertFalse(cfg.hasDisplayViewAlias('display1', 'view3', 'alias')) + + self.assertEqual(list(cfg.getDisplayViewAliases('display1', 'view3')), + ['alias,with,comma', 'alias4']) + + 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 getUseDisplayAliases. Either way, the display must + # actually use that color space in one of its views, or the match fails (a display + # color space that merely happens to share a name with an unrelated display doesn't + # match). + + 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'], + description='The sRGB display.') + dcs.setTransform(OCIO.MatrixTransform(), OCIO.COLORSPACE_DIR_FROM_REFERENCE) + cfg.addColorSpace(dcs) + + cfg.addDisplayView('sRGB - Display', 'view1', 'raw') + + # The name match alone isn't enough: so far "sRGB - Display" only has view1, whose + # color space is "raw", so the match fails even though it is an exact name match. + self.assertFalse(cfg.getUseDisplayAliases()) + self.assertEqual(cfg.getDisplayDescription('sRGB - Display'), '') + + # Add a view that uses the display color space itself, satisfying that requirement. + cfg.addDisplayView('sRGB - Display', 'view2', 'sRGB - Display', '') + + self.assertEqual(cfg.getDisplayDescription('sRGB - Display'), 'The sRGB display.') + self.assertEqual(cfg.getDisplayDescription('sRGB'), '') + + cfg.setUseDisplayAliases(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) + + cfg.addDisplayView('sRGB - Display', 'view1', 'raw') + cfg.addDisplayView('sRGB - Display', 'view2', viewTransform='', + displayColorSpaceName='', looks='', ruleName='', + description='', aliases=['view2_old']) + + # 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 alias fallback to be enabled. + self.assertFalse(cfg.getUseDisplayAliases()) + self.assertEqual( + cfg.getDisplayViewColorSpaceName('sRGB - Display', 'view2'), '') + self.assertEqual( + cfg.getResolvedDisplayViewColorSpaceName('sRGB - Display', 'view2'), 'sRGB - Display') + + # Resolving a view by one of its own aliases is always active, unlike resolving an + # out-of-date display name, which requires getUseDisplayAliases. + self.assertEqual(cfg.getResolvedDisplayViewColorSpaceName('sRGB', 'view1'), '') + self.assertEqual( + cfg.getResolvedDisplayViewColorSpaceName('sRGB - Display', 'view2_old'), + 'sRGB - Display') + + cfg.setUseDisplayAliases(True) + self.assertEqual(cfg.getResolvedDisplayViewColorSpaceName('sRGB', 'view1'), 'raw') + self.assertEqual( + cfg.getResolvedDisplayViewColorSpaceName('sRGB', 'view2_old'), '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.