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
This commit is contained in:
Damien Picard
2025-10-02 16:14:32 +02:00
committed by Nika Kutsniashvili
parent d4e254a552
commit 8b351bdee8
4 changed files with 56 additions and 46 deletions

View File

@@ -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": "",

View File

@@ -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')

View File

@@ -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):

View File

@@ -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):