From 8b351bdee8cb21ad9159d0b566a3a4ec031d6def Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Thu, 2 Oct 2025 16:14:32 +0200 Subject: [PATCH] Node Wrangler: Fixes for Save Viewer operator ## Fix: Error in Node Wrangler UI when in Sequencer mode Node Wrangler tried to find the current scene compositing node tree, even when there was none. This could happen if working on a sequencer compositing modifier. This commit hides the Save This Image button when not in Scene compositing mode. ## Fix: Error in Node Wrangler's Save Image op when file path is empty If the file path is empty in Save Image operator, it returns `None` and errors out. This commit makes it return `{'CANCELLED'}` early in that case. ## Add webp to list of file formats for Save Viewer ## Fix: Error when Viewer Node image renamed and saved The "Viewer Node" image that contains the current compositing node tree's viewer data has a hardcoded name by default. However it can be renamed by the user, which will make the Save Viewer operator fail. This commit extracts a utility function to get the current viewer image based on its properties, and simply saves that. In addition, this avoids having to change the area type to an image editor to get the current viewer node, and restoring the node editor afterwards. This action did not restore the current node tree if the editor was inside a node group. ## Rename "Save This Image" to "Save Viewer Image" When calling the operator through Menu Search, it is not clear what "Save This Image" refers to. This commit renames the operator to "Save Viewer Image", but keeps the old name for the button label. Pull Request: https://projects.blender.org/blender/blender/pulls/147085 --- scripts/addons_core/node_wrangler/__init__.py | 4 +- .../addons_core/node_wrangler/interface.py | 7 +- .../addons_core/node_wrangler/operators.py | 71 ++++++++++--------- .../addons_core/node_wrangler/utils/nodes.py | 20 +++--- 4 files changed, 56 insertions(+), 46 deletions(-) diff --git a/scripts/addons_core/node_wrangler/__init__.py b/scripts/addons_core/node_wrangler/__init__.py index e04c48b1014..2d5deafe809 100644 --- a/scripts/addons_core/node_wrangler/__init__.py +++ b/scripts/addons_core/node_wrangler/__init__.py @@ -7,8 +7,8 @@ bl_info = { # This is now displayed as the maintainer, so show the foundation. # "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer", # Original Authors "author": "Blender Foundation", - "version": (3, 56), - "blender": (4, 2, 0), + "version": (4, 0, 0), + "blender": (5, 0, 0), "location": "Node Editor Toolbar or Shift-W", "description": "Various tools to enhance and speed up node-based workflow", "warning": "", diff --git a/scripts/addons_core/node_wrangler/interface.py b/scripts/addons_core/node_wrangler/interface.py index aa47103327d..a0494562844 100644 --- a/scripts/addons_core/node_wrangler/interface.py +++ b/scripts/addons_core/node_wrangler/interface.py @@ -440,11 +440,12 @@ def bgreset_menu_func(self, context): def save_viewer_menu_func(self, context): space = context.space_data if (space.type == 'NODE_EDITOR' + and space.tree_type == 'CompositorNodeTree' + and space.node_tree_sub_type == 'SCENE' and space.node_tree is not None and space.node_tree.library is None - and space.tree_type == 'CompositorNodeTree' - and context.scene.compositing_node_group.nodes.active - and context.scene.compositing_node_group.nodes.active.type == "VIEWER"): + and space.edit_tree.nodes.active + and space.edit_tree.nodes.active.type == "VIEWER"): self.layout.operator(operators.NWSaveViewer.bl_idname, icon='FILE_IMAGE') diff --git a/scripts/addons_core/node_wrangler/operators.py b/scripts/addons_core/node_wrangler/operators.py index 7660c11570a..b42372a0eeb 100644 --- a/scripts/addons_core/node_wrangler/operators.py +++ b/scripts/addons_core/node_wrangler/operators.py @@ -34,7 +34,7 @@ from .utils.paths import match_files_to_socket_names, split_into_components from .utils.nodes import (node_mid_pt, autolink, node_at_pos, get_nodes_links, force_update, nw_check, nw_check_not_empty, nw_check_selected, nw_check_active, nw_check_space_type, - nw_check_node_type, nw_check_visible_outputs, nw_check_viewer_node, NWBase, + nw_check_node_type, nw_check_visible_outputs, get_viewer_image, nw_check_viewer_node, NWBase, get_first_enabled_output, is_visible_socket) @@ -2191,7 +2191,7 @@ class NWAddSequence(Operator, NWBase, ImportHelper): class NWSaveViewer(bpy.types.Operator, ExportHelper): """Save the current viewer node to an image file""" bl_idname = "node.nw_save_viewer" - bl_label = "Save This Image" + bl_label = "Save Viewer Image" filepath: StringProperty(subtype="FILE_PATH") filename_ext: EnumProperty( name="Format", @@ -2206,7 +2206,9 @@ class NWSaveViewer(bpy.types.Operator, ExportHelper): ('.dpx', 'DPX', ""), ('.exr', 'OPEN_EXR', ""), ('.hdr', 'HDR', ""), - ('.tif', 'TIFF', "")), + ('.tif', 'TIFF', ""), + ('.webp', 'WEBP', ""), + ), default='.png', ) @@ -2218,36 +2220,39 @@ class NWSaveViewer(bpy.types.Operator, ExportHelper): def execute(self, context): fp = self.filepath - if fp: - formats = { - '.bmp': 'BMP', - '.rgb': 'IRIS', - '.png': 'PNG', - '.jpg': 'JPEG', - '.jpeg': 'JPEG', - '.jp2': 'JPEG2000', - '.tga': 'TARGA', - '.cin': 'CINEON', - '.dpx': 'DPX', - '.exr': 'OPEN_EXR', - '.hdr': 'HDR', - '.tiff': 'TIFF', - '.tif': 'TIFF'} - basename, ext = path.splitext(fp) - image_settings = context.scene.render.image_settings - old_media_type = image_settings.media_type - old_file_format = image_settings.file_format - old_tree_type = context.space_data.tree_type - image_settings.media_type = 'IMAGE' - image_settings.file_format = formats[self.filename_ext] - context.area.type = "IMAGE_EDITOR" - context.area.spaces[0].image = bpy.data.images['Viewer Node'] - context.area.spaces[0].image.save_render(fp) - context.area.type = "NODE_EDITOR" - context.space_data.tree_type = old_tree_type - image_settings.media_type = old_media_type - image_settings.file_format = old_file_format - return {'FINISHED'} + if not fp: + return {'CANCELLED'} + + formats = { + '.bmp': 'BMP', + '.rgb': 'IRIS', + '.png': 'PNG', + '.jpg': 'JPEG', + '.jpeg': 'JPEG', + '.jp2': 'JPEG2000', + '.tga': 'TARGA', + '.cin': 'CINEON', + '.dpx': 'DPX', + '.exr': 'OPEN_EXR', + '.hdr': 'HDR', + '.tiff': 'TIFF', + '.tif': 'TIFF', + '.webp': 'WEBP', + } + image_settings = context.scene.render.image_settings + old_media_type = image_settings.media_type + old_file_format = image_settings.file_format + image_settings.media_type = 'IMAGE' + image_settings.file_format = formats[self.filename_ext] + + try: + get_viewer_image().save_render(fp) + except RuntimeError as e: + self.report({'ERROR'}, rpt_("Could not write image: {}").format(e)) + + image_settings.media_type = old_media_type + image_settings.file_format = old_file_format + return {'FINISHED'} class NWResetNodes(bpy.types.Operator): diff --git a/scripts/addons_core/node_wrangler/utils/nodes.py b/scripts/addons_core/node_wrangler/utils/nodes.py index 8f770de713e..885f33e9cdb 100644 --- a/scripts/addons_core/node_wrangler/utils/nodes.py +++ b/scripts/addons_core/node_wrangler/utils/nodes.py @@ -183,6 +183,14 @@ def get_output_location(tree): return loc_x, loc_y +def get_viewer_image(): + for img in bpy.data.images: + if (img.source == 'VIEWER' + and len(img.render_slots) == 0 + and sum(img.size) > 0): + return img + + def nw_check(cls, context): space = context.space_data if space.type != 'NODE_EDITOR': @@ -258,14 +266,10 @@ def nw_check_visible_outputs(cls, context): def nw_check_viewer_node(cls): - for img in bpy.data.images: - # False if not connected or connected but no image - if (img.source == 'VIEWER' - and len(img.render_slots) == 0 - and sum(img.size) > 0): - return True - cls.poll_message_set("Viewer image not found.") - return False + if get_viewer_image() is None: + cls.poll_message_set("Viewer image not found.") + return False + return True def get_first_enabled_output(node):