diff --git a/client/ayon_nuke/api/__init__.py b/client/ayon_nuke/api/__init__.py index 77c7a4d..4c83288 100644 --- a/client/ayon_nuke/api/__init__.py +++ b/client/ayon_nuke/api/__init__.py @@ -46,7 +46,7 @@ create_write_node, link_knobs ) -from .utils import ( +from .colorspace import ( colorspace_exists_on_node, get_colorspace_list ) diff --git a/client/ayon_nuke/api/colorspace.py b/client/ayon_nuke/api/colorspace.py new file mode 100644 index 0000000..1b98b1d --- /dev/null +++ b/client/ayon_nuke/api/colorspace.py @@ -0,0 +1,339 @@ +""" +Nuke Colorspace related methods +""" + +import re + +from ayon_core.lib import ( + Logger, + StringTemplate, +) + +from .constants import COLOR_VALUE_SEPARATOR + +import nuke + +log = Logger.get_logger(__name__) + + +_DISPLAY_AND_VIEW_COLORSPACES_CACHE = {} +_COLORSPACES_CACHE = {} + + +def get_display_and_view_colorspaces(root_node): + """Get all possible display and view colorspaces + + This is stored in class variable to avoid multiple calls. + + Args: + root_node (nuke.Node): root node + + Returns: + list: all possible display and view colorspaces + """ + script_name = nuke.root().name() + if _DISPLAY_AND_VIEW_COLORSPACES_CACHE.get(script_name) is None: + colorspace_knob = root_node["monitorLut"] + colorspaces = nuke.getColorspaceList(colorspace_knob) + _DISPLAY_AND_VIEW_COLORSPACES_CACHE[script_name] = colorspaces + + return _DISPLAY_AND_VIEW_COLORSPACES_CACHE[script_name] + + +def get_colorspace_list(colorspace_knob, node=None): + """Get available colorspace profile names + + Args: + colorspace_knob (nuke.Knob): nuke knob object + node (Optional[nuke.Node]): nuke node for caching differentiation + + Returns: + list: list of strings names of profiles + """ + results = [] + + # making sure any node is provided + node = node or nuke.root() + # unique script based identifier + script_name = nuke.root().name() + node_name = node.fullName() + identifier_key = f"{script_name}_{node_name}" + + if _COLORSPACES_CACHE.get(identifier_key) is None: + # This pattern is to match with roles which uses an indentation and + # parentheses with original colorspace. The value returned from the + # colorspace is the string before the indentation, so we'll need to + # convert the values to match with value returned from the knob, + # ei. knob.value(). + pattern = r".*\t.* \(.*\)" + for colorspace in nuke.getColorspaceList(colorspace_knob): + match = re.search(pattern, colorspace) + if match: + results.append(colorspace.split("\t", 1)[0]) + else: + results.append(colorspace) + + _COLORSPACES_CACHE[identifier_key] = results + + return _COLORSPACES_CACHE[identifier_key] + + +def colorspace_exists_on_node(node, colorspace_name): + """ Check if colorspace exists on node + + Look through all options in the colorspace knob, and see if we have an + exact match to one of the items. + + Args: + node (nuke.Node): nuke node object + colorspace_name (str): color profile name + + Returns: + bool: True if exists + """ + node_knob_keys = node.knobs().keys() + + if "colorspace" in node_knob_keys: + colorspace_knob = node["colorspace"] + elif "floatLut" in node_knob_keys: + colorspace_knob = node["floatLut"] + else: + log.warning(f"Node '{node.name()}' does not have colorspace knob") + return False + + return colorspace_name in get_colorspace_list(colorspace_knob, node) + + +def create_viewer_profile_string(viewer, display=None, path_like=False): + """Convert viewer and display to string + + Args: + viewer (str): viewer name + display (Optional[str]): display name + path_like (Optional[bool]): if True, return path like string + + Returns: + str: viewer config string + """ + if not display: + return viewer + + if path_like: + return "{}/{}".format(display, viewer) + return "{} ({})".format(viewer, display) + + +def get_formatted_display_and_view( + view_profile, formatting_data, root_node=None): + """Format display and view profile into string. + + This method is formatting a display and view profile. It is iterating + over all possible combinations of display and view colorspaces. Those + could be separated by COLOR_VALUE_SEPARATOR defined in constants. + + If Anatomy template tokens are used but formatting data is not provided, + it will try any other available variants in next position of the separator. + + This method also validate that the formatted display and view profile is + available in currently run nuke session ocio config. + + Example: + >>> from ayon_nuke.api.colorspace import get_formatted_display_and_view + >>> view_profile = { + ... "view": "{context};sRGB", + ... "display": "{project_code};ACES" + ... } + >>> formatting_data = { + ... "context": "01sh010", + ... "project_code": "proj01" + ...} + >>> display_and_view = get_formatted_display_and_view( + ... view_profile, formatting_data) + >>> print(display_and_view) + "01sh010 (proj01)" + + + Args: + view_profile (dict): view and display profile + formatting_data (dict): formatting data + root_node (Optional[nuke.Node]): root node + + Returns: + str: formatted display and view profile string + ex: "sRGB (ACES)" + """ + if not root_node: + root_node = nuke.root() + + views = view_profile["view"].split(COLOR_VALUE_SEPARATOR) + + # display could be optional in case nuke_default ocio config is used + displays = [] + if view_profile["display"]: + displays = view_profile["display"].split(COLOR_VALUE_SEPARATOR) + + # generate all possible combination of display/view + display_views = [] + for view in views: + # display could be optional in case nuke_default ocio config is used + if not displays: + display_views.append(view.strip()) + continue + + for display in displays: + display_views.append( + create_viewer_profile_string( + view.strip(), display.strip(), path_like=False + ) + ) + + for dv_item in display_views: + # format any template tokens used in the string + dv_item_resolved = StringTemplate(dv_item).format_strict( + formatting_data) + log.debug("Resolved display and view: `{}`".format(dv_item_resolved)) + + # making sure formatted colorspace exists in running session + if dv_item_resolved in get_display_and_view_colorspaces(root_node): + return dv_item_resolved + + +def get_formatted_display_and_view_as_dict( + view_profile, formatting_data, root_node=None): + """Format display and view profile into dict. + + This method is formatting a display and view profile. It is iterating + over all possible combinations of display and view colorspaces. Those + could be separated by COLOR_VALUE_SEPARATOR defined in constants. + + If Anatomy template tokens are used but formatting data is not provided, + it will try any other available variants in next position of the separator. + + This method also validate that the formatted display and view profile is + available in currently run nuke session ocio config. + + Example: + >>> from ayon_nuke.api.colorspace import get_formatted_display_and_view_as_dict # noqa + >>> view_profile = { + ... "view": "{context};sRGB", + ... "display": "{project_code};ACES" + ... } + >>> formatting_data = { + ... "context": "01sh010", + ... "project_code": "proj01" + ...} + >>> display_and_view = get_formatted_display_and_view_as_dict( + ... view_profile, formatting_data) + >>> print(display_and_view) + {"view": "01sh010", "display": "proj01"} + + + Args: + view_profile (dict): view and display profile + formatting_data (dict): formatting data + root_node (Optional[nuke.Node]): root node + + Returns: + dict: formatted display and view profile in dict + ex: {"view": "sRGB", "display": "ACES"} + """ + if not root_node: + root_node = nuke.root() + + views = view_profile["view"].split(COLOR_VALUE_SEPARATOR) + + # display could be optional in case nuke_default ocio config is used + displays = [] + if view_profile["display"]: + displays = view_profile["display"].split(COLOR_VALUE_SEPARATOR) + + # generate all possible combination of display/view + display_views = [] + for view in views: + # display could be optional in case nuke_default ocio config is used + if not displays: + display_views.append({"view": view.strip(), "display": None}) + continue + + for display in displays: + display_views.append( + {"view": view.strip(), "display": display.strip()}) + + root_display_and_view = get_display_and_view_colorspaces(root_node) + for dv_item in display_views: + # format any template tokens used in the string + view = StringTemplate.format_strict_template( + dv_item["view"], formatting_data + ) + # for config without displays - nuke_default + test_string = view + display = dv_item["display"] + if display: + display = StringTemplate.format_strict_template( + display, formatting_data + ) + test_string = create_viewer_profile_string( + view, display, path_like=False + ) + + log.debug(f"Resolved View: '{view}' Display: '{display}'") + + # Make sure formatted colorspace exists in running ocio config session + if test_string in root_display_and_view: + return { + "view": view, + "display": display, + } + + +def get_formatted_colorspace( + colorspace_name, formatting_data, root_node=None): + """Format colorspace profile name into string. + + This method is formatting colorspace profile name. It is iterating + over all possible combinations of input string which could be separated + by COLOR_VALUE_SEPARATOR defined in constants. + + If Anatomy template tokens are used but formatting data is not provided, + it will try any other available variants in next position of the separator. + + This method also validate that the formatted colorspace profile name is + available in currently run nuke session ocio config. + + Example: + >>> from ayon_nuke.api.colorspace import get_formatted_colorspace + >>> colorspace_name = "{project_code}_{context};ACES - ACEScg" + >>> formatting_data = { + ... "context": "01sh010", + ... "project_code": "proj01" + ...} + >>> new_colorspace_name = get_formatted_colorspace( + ... colorspace_name, formatting_data) + >>> print(new_colorspace_name) + "proj01_01sh010" + + + Args: + colorspace_name (str): colorspace profile name + formatting_data (dict): formatting data + root_node (Optional[nuke.Node]): root node + + Returns: + str: formatted colorspace profile string + ex: "ACES - ACEScg" + """ + if not root_node: + root_node = nuke.root() + + colorspaces = colorspace_name.split(COLOR_VALUE_SEPARATOR) + + # iterate via all found colorspaces + for citem in colorspaces: + # format any template tokens used in the string + citem_resolved = StringTemplate(citem.strip()).format_strict( + formatting_data) + log.debug("Resolved colorspace: `{}`".format(citem_resolved)) + + # making sure formatted colorspace exists in running session + if colorspace_exists_on_node(root_node, citem_resolved): + return citem_resolved diff --git a/client/ayon_nuke/api/constants.py b/client/ayon_nuke/api/constants.py index fa39626..805decb 100644 --- a/client/ayon_nuke/api/constants.py +++ b/client/ayon_nuke/api/constants.py @@ -8,3 +8,5 @@ "invalid": "0xff0000ff", "not_found": "0xffff00ff", } + +COLOR_VALUE_SEPARATOR = ";" diff --git a/client/ayon_nuke/api/lib.py b/client/ayon_nuke/api/lib.py index 66acfa7..5af72bd 100644 --- a/client/ayon_nuke/api/lib.py +++ b/client/ayon_nuke/api/lib.py @@ -58,6 +58,8 @@ from .workio import save_file from .utils import get_node_outputs +from .colorspace import get_formatted_display_and_view + log = Logger.get_logger(__name__) MENU_LABEL = os.getenv("AYON_MENU_LABEL") or "AYON" @@ -749,6 +751,7 @@ def get_imageio_node_override_setting( return knobs_settings +# TODO: move into ./colorspace.py def get_imageio_input_colorspace(filename): ''' Get input file colorspace based on regex in settings. ''' @@ -1427,6 +1430,7 @@ def get_nodes(self, nodes=None, nodes_filter=None): for filter in nodes_filter: return [n for n in self._nodes if filter in n.Class()] + # TODO: move into ./colorspace.py def set_viewers_colorspace(self, imageio_nuke): ''' Adds correct colorspace to viewer @@ -1439,11 +1443,11 @@ def set_viewers_colorspace(self, imageio_nuke): "wipe_position", "monitorOutOutputTransform" ] - viewer_process = self._display_and_view_formatted( - imageio_nuke["viewer"] + viewer_process = get_formatted_display_and_view( + imageio_nuke["viewer"], self.formatting_data, self._root_node ) - output_transform = self._display_and_view_formatted( - imageio_nuke["monitor"] + output_transform = get_formatted_display_and_view( + imageio_nuke["monitor"], self.formatting_data, self._root_node ) erased_viewers = [] for v in nuke.allNodes(filter="Viewer"): @@ -1481,21 +1485,7 @@ def set_viewers_colorspace(self, imageio_nuke): "Attention! Viewer nodes {} were erased." "It had wrong color profile".format(erased_viewers)) - def _display_and_view_formatted(self, view_profile): - """ Format display and view profile string - - Args: - view_profile (dict): view and display profile - - Returns: - str: formatted display and view profile string - """ - display_view = create_viewer_profile_string( - view_profile["view"], view_profile["display"], path_like=False - ) - # format any template tokens used in the string - return StringTemplate(display_view).format_strict(self.formatting_data) - + # TODO: move into ./colorspace.py def set_root_colorspace(self, imageio_host): ''' Adds correct colorspace to root @@ -1755,6 +1745,7 @@ def _replace_ocio_path_with_env_var(self, config_data): return new_path + # TODO: move into ./colorspace.py def set_writes_colorspace(self): ''' Adds correct colorspace to write node dict @@ -1832,6 +1823,7 @@ def set_writes_colorspace(self): set_node_knobs_from_settings( write_node, nuke_imageio_writes["knobs"]) + # TODO: move into ./colorspace.py def set_reads_colorspace(self, read_clrs_inputs): """ Setting colorspace to Read nodes @@ -1879,6 +1871,7 @@ def set_reads_colorspace(self, read_clrs_inputs): nname, knobs["to"])) + # TODO: move into ./colorspace.py def set_colorspace(self): ''' Setting colorspace following presets ''' diff --git a/client/ayon_nuke/api/plugin.py b/client/ayon_nuke/api/plugin.py index eb9a926..e6291e1 100644 --- a/client/ayon_nuke/api/plugin.py +++ b/client/ayon_nuke/api/plugin.py @@ -41,13 +41,17 @@ get_node_data, get_view_process_node, get_filenames_without_hash, - link_knobs + get_work_default_directory, + link_knobs, ) from .pipeline import ( list_instances, remove_instance ) -from ayon_nuke.api.lib import get_work_default_directory +from .colorspace import ( + get_formatted_display_and_view_as_dict, + get_formatted_colorspace +) def _collect_and_cache_nodes(creator): @@ -969,25 +973,27 @@ def generate_mov(self, farm=False, delete=True, **kwargs): if baking_colorspace["type"] == "display_view": display_view = baking_colorspace["display_view"] - message = "OCIODisplay... '{}'" - node = nuke.createNode("OCIODisplay") + display_view_f = get_formatted_display_and_view_as_dict( + display_view, self.formatting_data + ) + + if not display_view_f: + raise ValueError( + "Invalid display and view profile: " + f"'{display_view}'" + ) # assign display and view - display = display_view["display"] - view = display_view["view"] + display = display_view_f["display"] + view = display_view_f["view"] + + message = "OCIODisplay... '{}'" + node = nuke.createNode("OCIODisplay") # display could not be set in nuke_default config if display: - # format display string with anatomy data - display = StringTemplate(display).format_strict( - self.formatting_data - ) node["display"].setValue(display) - # format view string with anatomy data - view = StringTemplate(view).format_strict( - self.formatting_data) - # assign viewer node["view"].setValue(view) if config_data: @@ -1001,8 +1007,13 @@ def generate_mov(self, farm=False, delete=True, **kwargs): elif baking_colorspace["type"] == "colorspace": baking_colorspace = baking_colorspace["colorspace"] # format colorspace string with anatomy data - baking_colorspace = StringTemplate( - baking_colorspace).format_strict(self.formatting_data) + baking_colorspace = get_formatted_colorspace( + baking_colorspace, self.formatting_data + ) + if not baking_colorspace: + raise ValueError( + f"Invalid baking color space: '{baking_colorspace}'" + ) node = nuke.createNode("OCIOColorSpace") message = "OCIOColorSpace... '{}'" # no need to set input colorspace since it is driven by diff --git a/client/ayon_nuke/api/utils.py b/client/ayon_nuke/api/utils.py index 646bb0e..dc21537 100644 --- a/client/ayon_nuke/api/utils.py +++ b/client/ayon_nuke/api/utils.py @@ -1,5 +1,4 @@ import os -import re import nuke @@ -93,55 +92,6 @@ def bake_gizmos_recursively(in_group=None): bake_gizmos_recursively(node) -def colorspace_exists_on_node(node, colorspace_name): - """ Check if colorspace exists on node - - Look through all options in the colorspace knob, and see if we have an - exact match to one of the items. - - Args: - node (nuke.Node): nuke node object - colorspace_name (str): color profile name - - Returns: - bool: True if exists - """ - try: - colorspace_knob = node['colorspace'] - except ValueError: - # knob is not available on input node - return False - - return colorspace_name in get_colorspace_list(colorspace_knob) - - -def get_colorspace_list(colorspace_knob): - """Get available colorspace profile names - - Args: - colorspace_knob (nuke.Knob): nuke knob object - - Returns: - list: list of strings names of profiles - """ - results = [] - - # This pattern is to match with roles which uses an indentation and - # parentheses with original colorspace. The value returned from the - # colorspace is the string before the indentation, so we'll need to - # convert the values to match with value returned from the knob, - # ei. knob.value(). - pattern = r".*\t.* \(.*\)" - for colorspace in nuke.getColorspaceList(colorspace_knob): - match = re.search(pattern, colorspace) - if match: - results.append(colorspace.split("\t", 1)[0]) - else: - results.append(colorspace) - - return results - - def is_headless(): """ Returns: diff --git a/server/settings/common.py b/server/settings/common.py index 2ddbc3c..16a7c0f 100644 --- a/server/settings/common.py +++ b/server/settings/common.py @@ -147,15 +147,23 @@ class DisplayAndViewProfileModel(BaseSettingsModel): display: str = SettingsField( "", title="Display", - description="What display to use", + description=( + "What display to use. Anatomy context tokens can " + "be used to dynamically set the value. And also fallback can " + "be defined via ';' (semicolon) separator. \n" + "Example: \n'{project[code]} ; ACES'.\n" + "Note that we are stripping the spaces around the separator." + ), ) - view: str = SettingsField( "", title="View", description=( "What view to use. Anatomy context tokens can " - "be used to dynamically set the value." + "be used to dynamically set the value. And also fallback can " + "be defined via ';' separator. \nExample: \n" + "'{project[code]}_{parent}_{folder[name]} ; sRGB'.\n" + "Note that we are stripping the spaces around the separator." ), ) diff --git a/server/settings/imageio.py b/server/settings/imageio.py index 22798b2..89d469e 100644 --- a/server/settings/imageio.py +++ b/server/settings/imageio.py @@ -146,14 +146,23 @@ class ViewProcessModel(BaseSettingsModel): display: str = SettingsField( "", title="Display", - description="What display to use", + description=( + "What display to use. Anatomy context tokens can " + "be used to dynamically set the value. And also fallback can " + "be defined via ';' (semicolon) separator. \n" + "Example: \n'{project[code]} ; ACES'.\n" + "Note that we are stripping the spaces around the separator." + ), ) view: str = SettingsField( "", title="View", description=( "What view to use. Anatomy context tokens can " - "be used to dynamically set the value." + "be used to dynamically set the value. And also fallback can " + "be defined via ';' separator. \nExample: \n" + "'{project[code]}_{parent}_{folder[name]} ; sRGB'.\n" + "Note that we are stripping the spaces around the separator." ), ) @@ -164,14 +173,23 @@ class MonitorProcessModel(BaseSettingsModel): display: str = SettingsField( "", title="Display", - description="What display to use", + description=( + "What display to use. Anatomy context tokens can " + "be used to dynamically set the value. And also fallback can " + "be defined via ';' (semicolon) separator. \n" + "Example: \n'{project[code]} ; ACES'.\n" + "Note that we are stripping the spaces around the separator." + ), ) view: str = SettingsField( "", title="View", description=( "What view to use. Anatomy context tokens can " - "be used to dynamically set the value." + "be used to dynamically set the value. And also fallback can " + "be defined via ';' separator. \nExample: \n" + "'{project[code]}_{parent}_{folder[name]} ; sRGB'.\n" + "Note that we are stripping the spaces around the separator." ), )