Enhance image editing tests and functionality
- Added parameterized tests for image edit replacement to ensure alpha mode preservation. - Refactored existing tests to utilize new image editing methods and improve clarity. - Introduced new tests for image target functionality, ensuring proper handling of shared textures and context. - Updated various tests to validate alpha mode handling across different scenarios. - Improved undo/redo functionality in image operators to maintain state consistency. - Added support for texture node actions, enhancing integration with Blender's node system. - Ensured that image properties are preserved during operations and that invalid contexts are handled gracefully.
This commit is contained in:
@@ -24,7 +24,8 @@ def test_owned_color_images_preserve_source_and_use_premul(floating):
|
||||
try:
|
||||
assert original.alpha_mode == "STRAIGHT"
|
||||
np.testing.assert_allclose(images.image_rgba(original), expected)
|
||||
for image in (copied, cropped, edited):
|
||||
assert edited.alpha_mode == original.alpha_mode
|
||||
for image in (copied, cropped):
|
||||
assert image.alpha_mode == "PREMUL"
|
||||
np.testing.assert_allclose(images.image_rgba(image), expected, atol=1 / 255)
|
||||
finally:
|
||||
@@ -33,13 +34,14 @@ def test_owned_color_images_preserve_source_and_use_premul(floating):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("loader", [images.load_image_edit_result, images.load_color_result_image])
|
||||
def test_job_color_loader_defaults_to_premul(tmp_path, loader):
|
||||
def test_job_loader_uses_edit_or_material_alpha_policy(tmp_path, loader):
|
||||
rgba = np.array([[[51, 102, 204, 64]]], dtype=np.uint8)
|
||||
Image.fromarray(rgba).save(tmp_path / "image.0001.png")
|
||||
original = bpy.data.images.new("Job source", width=1, height=1, alpha=True)
|
||||
result = loader(SimpleNamespace(data=original), tmp_path)
|
||||
source = original if loader == images.load_image_edit_result else SimpleNamespace(data=original)
|
||||
result = loader(source, tmp_path)
|
||||
try:
|
||||
assert result.alpha_mode == "PREMUL"
|
||||
assert result.alpha_mode == ("STRAIGHT" if loader == images.load_image_edit_result else "PREMUL")
|
||||
assert result.packed_file is not None
|
||||
assert original.alpha_mode == "STRAIGHT"
|
||||
finally:
|
||||
@@ -70,6 +72,7 @@ def test_failed_empty_replacement_restores_blending_and_timing():
|
||||
|
||||
def test_business_pixels_ignore_premul_read_interpretation():
|
||||
original = bpy.data.images.new("Premul read source", width=1, height=1, alpha=True)
|
||||
original.alpha_mode = "PREMUL"
|
||||
expected = np.array([0.4, 0.25, 0.1, 0.5], dtype=np.float32)
|
||||
result = images.create_image_edit_result(original, expected, (1, 1))
|
||||
try:
|
||||
@@ -87,6 +90,7 @@ def test_business_pixels_ignore_premul_read_interpretation():
|
||||
@pytest.mark.parametrize("dirty", [False, True])
|
||||
def test_business_read_preserves_shared_packed_source(dirty):
|
||||
original = bpy.data.images.new("Read integrity", width=1, height=1, alpha=True)
|
||||
original.alpha_mode = "PREMUL"
|
||||
result = images.create_image_edit_result(original, np.array([0.2, 0.4, 0.8, 0.25], dtype=np.float32), (1, 1))
|
||||
owner = bpy.data.objects.new("Read owner", None)
|
||||
owner.empty_display_type = "IMAGE"
|
||||
@@ -219,7 +223,7 @@ def test_mask_pack_reopen_add_and_repeat_frame_preserve_hidden_color(tmp_path):
|
||||
bpy.data.images.remove(result)
|
||||
result = next_result
|
||||
result.reload()
|
||||
assert result.alpha_mode == "PREMUL" and result.packed_file is not None
|
||||
assert result.alpha_mode == "STRAIGHT" and result.packed_file is not None
|
||||
np.testing.assert_allclose(images.image_rgba(result), rgba, atol=1 / 255)
|
||||
finally:
|
||||
bpy.data.images.remove(result)
|
||||
|
||||
@@ -33,6 +33,27 @@ def edit_result(source):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("alpha_mode", ["STRAIGHT", "PREMUL", "CHANNEL_PACKED", "NONE"])
|
||||
@pytest.mark.parametrize("shared", [False, True])
|
||||
@pytest.mark.parametrize("entry", ["pixels", "image.", "upscale."])
|
||||
def test_image_edit_replacement_preserves_alpha_mode(source, tmp_path, alpha_mode, shared, entry):
|
||||
original = source.data
|
||||
original.alpha_mode = alpha_mode
|
||||
if shared:
|
||||
other = create_empty("Other", original)
|
||||
if entry == "pixels":
|
||||
result = edit_result(source)
|
||||
else:
|
||||
Image.new("RGBA", (4, 3), (100, 150, 200, 128)).save(tmp_path / f"{entry}0001.png")
|
||||
result = images.load_image_edit_result(original, tmp_path, frame_prefix=entry)
|
||||
assert result.alpha_mode == alpha_mode
|
||||
images.replace_empty_image(source, result)
|
||||
assert source.data.alpha_mode == alpha_mode
|
||||
assert (source.data == original) == (not shared)
|
||||
if shared:
|
||||
assert other.data == original and original.alpha_mode == alpha_mode
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sharing", ["visible", "hidden", "other_scene"])
|
||||
def test_shared_edit_preserves_other_empty(source, sharing):
|
||||
original = source.data
|
||||
@@ -179,7 +200,7 @@ def test_animated_result_preserves_id_and_reopens(source, tmp_path):
|
||||
for index in (1, 2):
|
||||
Image.new("RGBA", (4, 3), (index * 80, 100, 50, 255)).save(tmp_path / f"image.{index:04d}.png")
|
||||
original.source = "SEQUENCE"
|
||||
result = images.load_image_edit_result(source, tmp_path)
|
||||
result = images.load_image_edit_result(source.data, tmp_path)
|
||||
expected_path = result.filepath_raw
|
||||
expected_packed = bytes(result.packed_file.data) if result.packed_file else None
|
||||
final = images.replace_empty_image(source, result)
|
||||
|
||||
@@ -46,30 +46,30 @@ class ImageDataTest(unittest.TestCase):
|
||||
self.assertEqual(self.modules.image.video_total_frames(image), 1)
|
||||
|
||||
|
||||
def test_default_extract_frames_uses_empty_image_frames(self):
|
||||
def test_extract_frame_count_uses_empty_image_frames(self):
|
||||
source_object = SimpleNamespace(
|
||||
data=SimpleNamespace(source="MOVIE", frame_duration=97),
|
||||
image_user=SimpleNamespace(frame_duration=24),
|
||||
)
|
||||
|
||||
self.assertEqual(self.modules.image.default_extract_frames(source_object), 24)
|
||||
self.assertEqual(self.modules.image.extract_frame_range(source_object.data, getattr(source_object, "image_user", None))[1], 24)
|
||||
|
||||
|
||||
def test_default_extract_frames_caps_at_video_total(self):
|
||||
def test_extract_frame_count_caps_at_video_total(self):
|
||||
source_object = SimpleNamespace(
|
||||
data=SimpleNamespace(source="MOVIE", frame_duration=97),
|
||||
image_user=SimpleNamespace(frame_duration=200),
|
||||
)
|
||||
|
||||
self.assertEqual(self.modules.image.default_extract_frames(source_object), 97)
|
||||
self.assertEqual(self.modules.image.extract_frame_range(source_object.data, getattr(source_object, "image_user", None))[1], 97)
|
||||
|
||||
|
||||
def test_default_extract_frames_falls_back_to_video_total(self):
|
||||
def test_extract_frame_count_falls_back_to_video_total(self):
|
||||
source_object = SimpleNamespace(
|
||||
data=SimpleNamespace(source="MOVIE", frame_duration=97),
|
||||
)
|
||||
|
||||
self.assertEqual(self.modules.image.default_extract_frames(source_object), 97)
|
||||
self.assertEqual(self.modules.image.extract_frame_range(source_object.data, getattr(source_object, "image_user", None))[1], 97)
|
||||
|
||||
|
||||
def test_extract_frame_range_honors_offset_within_clip(self):
|
||||
@@ -83,7 +83,7 @@ class ImageDataTest(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.modules.image.extract_frame_range(source_object),
|
||||
self.modules.image.extract_frame_range(source_object.data, source_object.image_user),
|
||||
(71, 24),
|
||||
)
|
||||
|
||||
@@ -99,7 +99,7 @@ class ImageDataTest(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.modules.image.extract_frame_range(source_object),
|
||||
self.modules.image.extract_frame_range(source_object.data, source_object.image_user),
|
||||
(74, 24),
|
||||
)
|
||||
|
||||
@@ -115,7 +115,7 @@ class ImageDataTest(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.modules.image.extract_frame_range(source_object),
|
||||
self.modules.image.extract_frame_range(source_object.data, source_object.image_user),
|
||||
(1, 24),
|
||||
)
|
||||
|
||||
@@ -131,7 +131,7 @@ class ImageDataTest(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.modules.image.extract_frame_range(source_object),
|
||||
self.modules.image.extract_frame_range(source_object.data, source_object.image_user),
|
||||
(1, 1),
|
||||
)
|
||||
|
||||
@@ -147,7 +147,7 @@ class ImageDataTest(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.modules.image.extract_frame_range(source_object),
|
||||
self.modules.image.extract_frame_range(source_object.data, source_object.image_user),
|
||||
(71, 1),
|
||||
)
|
||||
|
||||
|
||||
222
tests/anyimage/common/test_image_target.py
Normal file
222
tests/anyimage/common/test_image_target.py
Normal file
@@ -0,0 +1,222 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from anyimage.common import image as images
|
||||
from anyimage.common import image_target as targets
|
||||
from tests.support.image_texture import texture, texture_context, write_result
|
||||
|
||||
|
||||
def test_target_uses_edit_tree_and_survives_renaming(texture, tmp_path):
|
||||
context = texture_context(texture, pin=True)
|
||||
target = targets.ImageEditTarget.capture(context)
|
||||
texture.name = "Renamed texture"
|
||||
bpy.data.materials["Texture material"].name = "Renamed material"
|
||||
other = texture.id_data.nodes.new("ShaderNodeTexImage")
|
||||
texture.id_data.nodes.active = other
|
||||
context.space_data.edit_tree = None
|
||||
write_result(tmp_path)
|
||||
target.apply(tmp_path)
|
||||
assert tuple(texture.image.size) == (8, 6)
|
||||
assert other.image is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("color_space,alpha_mode", [
|
||||
("sRGB", "PREMUL"),
|
||||
("AgX Base sRGB", "STRAIGHT"),
|
||||
("Filmic sRGB", "STRAIGHT"),
|
||||
("sRGB", "STRAIGHT"),
|
||||
("AgX Base sRGB", "PREMUL"),
|
||||
])
|
||||
@pytest.mark.parametrize("prefix", ["image.", "upscale."])
|
||||
@pytest.mark.parametrize("shared", [False, True])
|
||||
def test_image_actions_preserve_source_color_space_and_alpha(texture, tmp_path, color_space, alpha_mode, prefix, shared):
|
||||
texture.image.colorspace_settings.name = color_space
|
||||
texture.image.alpha_mode = alpha_mode
|
||||
original = texture.image
|
||||
original_pixels = images.image_pixels(original)
|
||||
if shared:
|
||||
other = texture.id_data.nodes.new("ShaderNodeTexImage")
|
||||
other.image = original
|
||||
target = targets.ImageEditTarget.capture(texture_context(texture))
|
||||
write_result(tmp_path, prefix)
|
||||
target.apply(tmp_path, frame_prefix=prefix)
|
||||
result = texture.image
|
||||
assert result.colorspace_settings.name == color_space
|
||||
assert result.alpha_mode == alpha_mode
|
||||
np.testing.assert_allclose(
|
||||
images.image_pixels(result).reshape(-1, 4),
|
||||
np.tile(np.array([128, 64, 32, 80]) / 255, (48, 1)),
|
||||
atol=1 / 255,
|
||||
)
|
||||
if shared:
|
||||
assert other.image == original
|
||||
assert original.alpha_mode == alpha_mode
|
||||
np.testing.assert_array_equal(images.image_pixels(original), original_pixels)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("invalid", ["world", "light", "compositor", "geometry", "missing_image", "other_node", "no_tree"])
|
||||
def test_invalid_node_context_does_not_fall_back_to_empty(texture, invalid):
|
||||
context = texture_context(texture)
|
||||
context.object = bpy.data.objects.new("Fallback Empty", None)
|
||||
context.object.empty_display_type = "IMAGE"
|
||||
context.object.data = texture.image
|
||||
if invalid == "world":
|
||||
context.space_data.shader_type = "WORLD"
|
||||
elif invalid == "light":
|
||||
context.space_data.id = bpy.data.lights.new("Light", type="POINT")
|
||||
elif invalid in {"compositor", "geometry"}:
|
||||
context.space_data.tree_type = "CompositorNodeTree" if invalid == "compositor" else "GeometryNodeTree"
|
||||
elif invalid == "missing_image":
|
||||
texture.image = None
|
||||
elif invalid == "other_node":
|
||||
texture.id_data.nodes.active = texture.id_data.nodes.get("Principled BSDF")
|
||||
else:
|
||||
context.space_data.edit_tree = None
|
||||
assert targets.image_edit_owner(context) is None
|
||||
with pytest.raises(RuntimeError, match="Select"):
|
||||
targets.ImageEditTarget.capture(context)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("change", ["delete", "recreate", "rebind", "delete_material", "delete_image"])
|
||||
def test_target_rejects_changed_identity_before_loading_result(texture, tmp_path, change):
|
||||
target = targets.ImageEditTarget.capture(texture_context(texture))
|
||||
tree = texture.id_data
|
||||
if change in {"delete", "recreate"}:
|
||||
name = texture.name
|
||||
tree.nodes.remove(texture)
|
||||
if change == "recreate":
|
||||
replacement = tree.nodes.new("ShaderNodeTexImage")
|
||||
replacement.name = name
|
||||
replacement.image = target.image
|
||||
elif change == "rebind":
|
||||
texture.image = bpy.data.images.new("Replacement", width=2, height=2)
|
||||
elif change == "delete_material":
|
||||
bpy.data.materials.remove(bpy.data.materials["Texture material"])
|
||||
else:
|
||||
bpy.data.images.remove(texture.image)
|
||||
count = len(bpy.data.images)
|
||||
with pytest.raises(RuntimeError, match="no longer available"):
|
||||
target.apply(tmp_path)
|
||||
assert len(bpy.data.images) == count
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reference", ["sole", "fake_user", "editor", "same_tree", "other_material", "empty", "brush"])
|
||||
def test_texture_commit_preserves_structure_and_isolates_users(texture, tmp_path, reference):
|
||||
original = texture.image
|
||||
original.colorspace_settings.name = "Non-Color"
|
||||
expected_source = images.image_pixels(original)
|
||||
source_packed = bytes(original.packed_file.data)
|
||||
original.use_fake_user = reference == "fake_user"
|
||||
target = targets.ImageEditTarget.capture(texture_context(texture))
|
||||
# Add users after capture to exercise changes while an AI job is pending.
|
||||
if reference == "same_tree":
|
||||
other = texture.id_data.nodes.new("ShaderNodeTexImage")
|
||||
other.image = original
|
||||
elif reference == "other_material":
|
||||
material = bpy.data.materials.new("Other material")
|
||||
material.use_nodes = True
|
||||
other = material.node_tree.nodes.new("ShaderNodeTexImage")
|
||||
other.image = original
|
||||
elif reference == "empty":
|
||||
other = bpy.data.objects.new("Other Empty", None)
|
||||
other.empty_display_type = "IMAGE"
|
||||
other.data = original
|
||||
elif reference == "brush":
|
||||
other = bpy.data.textures.new("Brush texture", type="IMAGE")
|
||||
other.image = original
|
||||
elif reference == "editor":
|
||||
area = bpy.context.screen.areas[0]
|
||||
area.type = "IMAGE_EDITOR"
|
||||
area.spaces.active.image = original
|
||||
shared = reference in {"same_tree", "other_material", "empty", "brush"}
|
||||
links = [(link.from_socket, link.to_socket) for link in texture.id_data.links]
|
||||
write_result(tmp_path)
|
||||
target.apply(tmp_path)
|
||||
assert (texture.image == original) == (not shared)
|
||||
assert tuple(texture.image.size) == (8, 6)
|
||||
assert texture.image.colorspace_settings.name == "Non-Color"
|
||||
assert texture.image.alpha_mode == original.alpha_mode
|
||||
assert texture.image.packed_file is not None
|
||||
assert tuple(texture.location) == (-400, 100)
|
||||
assert (texture.interpolation, texture.extension, texture.projection) == ("Closest", "CLIP", "BOX")
|
||||
assert [(link.from_socket, link.to_socket) for link in texture.id_data.links] == links
|
||||
assert original.use_fake_user == (reference == "fake_user")
|
||||
if shared:
|
||||
assert (other.data if reference == "empty" else other.image) == original
|
||||
assert bytes(original.packed_file.data) == source_packed
|
||||
np.testing.assert_array_equal(images.image_pixels(original), expected_source)
|
||||
else:
|
||||
assert texture.image.name == "Texture.png"
|
||||
assert len(bpy.data.images) == 1
|
||||
expected = images.image_pixels(texture.image)
|
||||
path = str(tmp_path / "texture.blend")
|
||||
bpy.ops.wm.save_as_mainfile(filepath=path)
|
||||
bpy.ops.wm.open_mainfile(filepath=path)
|
||||
reopened = bpy.data.materials["Texture material"].node_tree.nodes["Source texture"]
|
||||
np.testing.assert_allclose(images.image_pixels(reopened.image), expected, atol=1 / 255)
|
||||
assert len(reopened.id_data.links) == len(links)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shared", [False, True])
|
||||
def test_animated_texture_preserves_playback_and_reopens(texture, tmp_path, shared):
|
||||
write_result(tmp_path, count=4)
|
||||
texture.image.source = "SEQUENCE"
|
||||
texture.image.filepath_raw = str(tmp_path / "image.0001.png")
|
||||
texture.image_user.frame_start = 9
|
||||
texture.image_user.frame_offset = 1
|
||||
texture.image_user.frame_duration = 2
|
||||
texture.image_user.use_cyclic = True
|
||||
context = texture_context(texture)
|
||||
target = targets.ImageEditTarget.capture(context)
|
||||
assert (target.start_frame, target.frame_count) == (2, 2)
|
||||
if shared:
|
||||
other = texture.id_data.nodes.new("ShaderNodeTexImage")
|
||||
other.image = texture.image
|
||||
with patch.object(targets, "prepare_image_input", return_value=(tmp_path, False)):
|
||||
target.prepare(context)
|
||||
assert context.scene.anyimage_settings.video_frames == 2
|
||||
target.apply(tmp_path)
|
||||
assert images.image_user_settings(texture) == (9, 0, 4, True)
|
||||
assert texture.image_user.use_auto_refresh
|
||||
assert texture.image.source == "SEQUENCE"
|
||||
path = str(tmp_path / "animated.blend")
|
||||
bpy.ops.wm.save_as_mainfile(filepath=path)
|
||||
bpy.ops.wm.open_mainfile(filepath=path)
|
||||
reopened = bpy.data.materials["Texture material"].node_tree.nodes["Source texture"]
|
||||
assert images.image_user_settings(reopened) == (9, 0, 4, True)
|
||||
assert len(images.collect_sequence_frames(reopened.image)) == 4
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shared", [False, True])
|
||||
def test_texture_failed_commit_restores_dirty_pixels_and_playback(texture, tmp_path, shared):
|
||||
original = texture.image
|
||||
if shared:
|
||||
other = texture.id_data.nodes.new("ShaderNodeTexImage")
|
||||
other.image = original
|
||||
original.pixels.foreach_set(np.tile([0.1, 0.3, 0.7, 0.5], 12).astype(np.float32))
|
||||
packed = bytes(original.packed_file.data)
|
||||
pixels = images.image_pixels(original)
|
||||
timing = images.image_user_settings(texture)
|
||||
write_result(tmp_path, count=2)
|
||||
result = bpy.data.images.load(str(tmp_path / "image.0001.png"))
|
||||
result.source = "SEQUENCE"
|
||||
count = len(bpy.data.images) - 1
|
||||
|
||||
def fail_configuration(user, *args, **kwargs):
|
||||
user.frame_start = 80
|
||||
user.use_auto_refresh = True
|
||||
raise RuntimeError("Playback failed")
|
||||
|
||||
with patch.object(targets, "configure_image_user", side_effect=fail_configuration):
|
||||
with pytest.raises(RuntimeError, match="Playback failed"):
|
||||
targets.replace_texture_image(texture, result)
|
||||
assert texture.image == original
|
||||
assert tuple(original.size) == (4, 3)
|
||||
assert bytes(original.packed_file.data) == packed
|
||||
np.testing.assert_array_equal(images.image_pixels(original), pixels)
|
||||
assert images.image_user_settings(texture) == timing
|
||||
assert not texture.image_user.use_auto_refresh
|
||||
assert len(bpy.data.images) == count
|
||||
@@ -115,7 +115,7 @@ def test_material_pixels_survive_pack_and_library_reload(color_data, tmp_path, e
|
||||
assert (restored.colorspace_settings.name, restored.alpha_mode) == selected
|
||||
np.testing.assert_allclose(images.image_pixels(restored), before, atol=1 / 255)
|
||||
edited = images.create_image_edit_result(restored, images.image_pixels(restored), (2, 1))
|
||||
assert edited.alpha_mode == "PREMUL"
|
||||
assert edited.alpha_mode == selected[1]
|
||||
np.testing.assert_allclose(images.image_pixels(edited), before, atol=1 / 255)
|
||||
|
||||
|
||||
@@ -179,16 +179,16 @@ def test_new_material_uses_current_preference_and_preserves_existing(color_data,
|
||||
np.testing.assert_array_equal(images.image_pixels(image), pixels)
|
||||
|
||||
|
||||
def test_edit_results_keep_their_policy_with_adaptation_enabled(color_data, tmp_path):
|
||||
def test_edit_results_preserve_source_alpha_with_adaptation_enabled(color_data, tmp_path):
|
||||
source = bpy.data.images.new("Editing source", width=1, height=1, alpha=True)
|
||||
rgba = np.array([0.2, 0.4, 0.8, 0.25], dtype=np.float32)
|
||||
Image.fromarray(np.array([[[51, 102, 204, 64]]], dtype=np.uint8)).save(tmp_path / "image.0001.png")
|
||||
with patch.object(preferences, "addon_preferences", return_value=SimpleNamespace(adapt_material_to_view_transform=True)):
|
||||
edited = images.create_image_edit_result(source, rgba, (1, 1))
|
||||
loaded = images.load_image_edit_result(SimpleNamespace(data=source), tmp_path)
|
||||
loaded = images.load_image_edit_result(source, tmp_path)
|
||||
for result in (edited, loaded):
|
||||
assert result.colorspace_settings.name == "sRGB"
|
||||
assert result.alpha_mode == "PREMUL"
|
||||
assert result.alpha_mode == source.alpha_mode
|
||||
np.testing.assert_allclose(images.image_pixels(result), rgba, atol=1 / 255)
|
||||
assert source.colorspace_settings.name == "sRGB"
|
||||
assert source.alpha_mode == "STRAIGHT"
|
||||
|
||||
@@ -21,7 +21,7 @@ class AiImageOperatorsTest(BlenderTestCase):
|
||||
self.assertEqual(operator_type.bl_options, {"UNDO"})
|
||||
operator = operator_type()
|
||||
operator.input_path = "input.png"
|
||||
operator.source_object_name = "Source"
|
||||
operator._image_target = SimpleNamespace(start_frame=4)
|
||||
operator.model = "SAFMN_REAL_X4"
|
||||
operator.max_input_size = 1024
|
||||
context = SimpleNamespace(
|
||||
@@ -34,7 +34,6 @@ class AiImageOperatorsTest(BlenderTestCase):
|
||||
self.upscale, "require_input_path", return_value=Path("input.png")
|
||||
),
|
||||
patch.object(self.upscale, "production_device", return_value="CUDA"),
|
||||
patch.object(self.upscale, "source_start_frame", return_value=4),
|
||||
):
|
||||
request = operator.request(context)
|
||||
|
||||
@@ -89,63 +88,37 @@ class AiImageOperatorsTest(BlenderTestCase):
|
||||
|
||||
|
||||
def test_remove_background_loads_a_plain_image_edit_result(self):
|
||||
source = SimpleNamespace()
|
||||
result_image = SimpleNamespace()
|
||||
result = SimpleNamespace(directory=Path("result"))
|
||||
operator = self.remove_background.RunBackgroundRemoval()
|
||||
operator._image_target = Mock()
|
||||
ops = SimpleNamespace(
|
||||
ed=SimpleNamespace(undo_push=Mock(return_value={"FINISHED"}))
|
||||
)
|
||||
with (
|
||||
patch.object(self.image_data, "require_image_empty", return_value=source),
|
||||
patch.object(
|
||||
self.image_data,
|
||||
"load_image_edit_result",
|
||||
return_value=result_image,
|
||||
) as load,
|
||||
patch.object(self.image_data, "replace_empty_image") as replace,
|
||||
patch.object(self.remove_background.bpy, "ops", ops, create=True),
|
||||
):
|
||||
self.remove_background.apply_background_response(result, "Source")
|
||||
operator.response(None, result)
|
||||
|
||||
load.assert_called_once_with(source, Path("result"))
|
||||
replace.assert_called_once_with(source, result_image)
|
||||
operator._image_target.apply.assert_called_once_with(Path("result"))
|
||||
ops.ed.undo_push.assert_called_once_with(message="Remove Background")
|
||||
|
||||
|
||||
def test_upscale_loads_a_plain_image_edit_result(self):
|
||||
source_image = SimpleNamespace()
|
||||
source = SimpleNamespace(data=source_image)
|
||||
result_image = SimpleNamespace()
|
||||
result = SimpleNamespace(directory=Path("result"))
|
||||
operator = self.upscale.UpscaleImage()
|
||||
operator.source_object_name = "Source"
|
||||
operator.source_image_name = "Poster.png"
|
||||
operator._image_target = Mock()
|
||||
operator.model = "SAFMN_REAL_X4"
|
||||
images = SimpleNamespace(get=lambda name: source_image if name == "Poster.png" else None)
|
||||
with (
|
||||
patch.object(self.upscale, "require_image_empty", return_value=source),
|
||||
patch.object(
|
||||
self.upscale,
|
||||
"load_image_edit_result",
|
||||
return_value=result_image,
|
||||
) as load,
|
||||
patch.object(self.upscale, "replace_empty_image") as replace,
|
||||
patch.object(
|
||||
self.upscale,
|
||||
"upscale_model_label",
|
||||
return_value="SAFMN Real x4",
|
||||
),
|
||||
patch.object(
|
||||
self.upscale.bpy,
|
||||
"data",
|
||||
SimpleNamespace(images=images),
|
||||
),
|
||||
):
|
||||
message = operator.response(None, result)
|
||||
|
||||
load.assert_called_once_with(
|
||||
source,
|
||||
operator._image_target.apply.assert_called_once_with(
|
||||
Path("result"),
|
||||
frame_prefix="upscale.",
|
||||
)
|
||||
replace.assert_called_once_with(source, result_image)
|
||||
self.assertEqual(message, "Image is upscaled with SAFMN Real x4")
|
||||
|
||||
@@ -126,7 +126,7 @@ def test_packed_frame_mask_add_keeps_boundary_color(background):
|
||||
).reshape(1, 1, 4)
|
||||
np.testing.assert_allclose(restored[0, 0, :3], expected, atol=1 / 255)
|
||||
assert restored[0, 0, 3] == 1
|
||||
assert result.alpha_mode == "PREMUL"
|
||||
assert result.alpha_mode == "STRAIGHT"
|
||||
finally:
|
||||
bpy.data.images.remove(original)
|
||||
bpy.data.images.remove(result)
|
||||
|
||||
@@ -31,7 +31,7 @@ def test_image_edit_result_uses_complete_shared_source_name():
|
||||
|
||||
assert shared.data == source_image
|
||||
assert source_image.alpha_mode == "STRAIGHT"
|
||||
assert result_image.alpha_mode == "PREMUL"
|
||||
assert result_image.alpha_mode == "STRAIGHT"
|
||||
assert source.use_empty_image_alpha
|
||||
assert source.data == result_image
|
||||
assert result_image.name == "Poster.png.001"
|
||||
@@ -92,14 +92,14 @@ def test_mask_frame_mask_add_preserves_packed_hidden_rgb():
|
||||
)
|
||||
result = image_data.create_image_edit_result(source.data, framed, (4, 4))
|
||||
result = image_data.replace_empty_image(source, result)
|
||||
assert result.alpha_mode == "PREMUL"
|
||||
assert result.alpha_mode == "STRAIGHT"
|
||||
assert source.use_empty_image_alpha
|
||||
assert result.packed_file is not None
|
||||
packed = np.asarray(Image.open(BytesIO(result.packed_file.data)).convert("RGBA")) / 255.0
|
||||
np.testing.assert_allclose(packed[..., :3], expected[..., :3], atol=1 / 255)
|
||||
np.testing.assert_array_equal(packed[..., 3], 0)
|
||||
result.reload()
|
||||
assert result.alpha_mode == "PREMUL"
|
||||
assert result.alpha_mode == "STRAIGHT"
|
||||
operator.mode = "ADD"
|
||||
assert edit_mask.EditImageAlpha._apply_mask(
|
||||
operator, SimpleNamespace(), source, selection
|
||||
|
||||
@@ -9,8 +9,10 @@ from anyimage.operators import mask_tool as mask
|
||||
|
||||
|
||||
@pytest.mark.parametrize("gesture", ["LASSO", "BRUSH", "POLYLINE"])
|
||||
def test_mask_commit_is_one_undo_transaction(gesture):
|
||||
@pytest.mark.parametrize("alpha_mode", ["STRAIGHT", "PREMUL"])
|
||||
def test_mask_commit_is_one_undo_transaction(gesture, alpha_mode):
|
||||
source = bpy.data.images.new("Undo source", width=8, height=8, alpha=True)
|
||||
source.alpha_mode = alpha_mode
|
||||
source.pixels.foreach_set(np.ones(8 * 8 * 4, dtype=np.float32))
|
||||
source.pack()
|
||||
owner = bpy.data.objects.new("Mask undo owner", None)
|
||||
@@ -47,11 +49,12 @@ def test_mask_commit_is_one_undo_transaction(gesture):
|
||||
assert bpy.ops.anyimage.edit_image_alpha("EXEC_DEFAULT", True) == {"FINISHED"}
|
||||
create.assert_called_once()
|
||||
result = bpy.data.objects[owner_name].data
|
||||
assert result.packed_file is not None and result.alpha_mode == "PREMUL"
|
||||
assert result.packed_file is not None and result.alpha_mode == alpha_mode
|
||||
assert bpy.data.objects[shared_name].data.name == source_name
|
||||
np.testing.assert_array_equal(image.image_pixels(bpy.data.images[source_name]), before)
|
||||
assert bpy.ops.ed.undo() == {"FINISHED"}
|
||||
assert bpy.data.objects[owner_name].data.name == source_name
|
||||
assert bpy.data.objects[owner_name].data.alpha_mode == alpha_mode
|
||||
np.testing.assert_array_equal(image.image_pixels(bpy.data.objects[owner_name].data), before)
|
||||
finally:
|
||||
bpy.utils.unregister_class(mask.EditImageAlpha)
|
||||
|
||||
225
tests/anyimage/operators/test_texture_node_actions.py
Normal file
225
tests/anyimage/operators/test_texture_node_actions.py
Normal file
@@ -0,0 +1,225 @@
|
||||
from contextlib import ExitStack
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from anyimage import menu
|
||||
from anyimage.common import image as images
|
||||
from anyimage.common.image_target import ImageEditTarget, active_texture_node
|
||||
from anyimage.operators import remove_background, upscale
|
||||
from anyimage.runtime import runtime
|
||||
from tests.support.image_texture import texture, texture_context, write_result
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def shader_editor(texture):
|
||||
mesh = bpy.data.meshes.new("Material owner")
|
||||
obj = bpy.data.objects.new("Material owner", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
obj.data.materials.append(bpy.data.materials["Texture material"])
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D")
|
||||
area.type = "NODE_EDITOR"
|
||||
area.ui_type = "ShaderNodeTree"
|
||||
with bpy.context.temp_override(area=area):
|
||||
yield bpy.context
|
||||
|
||||
|
||||
def test_shader_editor_pin_and_shared_group_target(shader_editor, tmp_path):
|
||||
context = shader_editor
|
||||
node = active_texture_node(context)
|
||||
assert node is not None
|
||||
assert context.space_data.id == bpy.data.materials["Texture material"]
|
||||
context.space_data.pin = True
|
||||
bpy.context.view_layer.objects.active = None
|
||||
assert active_texture_node(context) == node
|
||||
# Use Blender's own grouping operation to exercise nested editor context.
|
||||
for candidate in node.id_data.nodes:
|
||||
candidate.select = candidate == node
|
||||
assert bpy.ops.node.group_make() == {"FINISHED"}
|
||||
nested = active_texture_node(context)
|
||||
assert nested is not None
|
||||
assert context.space_data.edit_tree != context.space_data.node_tree
|
||||
group = nested.id_data
|
||||
second_instance = context.space_data.node_tree.nodes.new("ShaderNodeGroup")
|
||||
second_instance.node_tree = group
|
||||
source = nested.image
|
||||
target = ImageEditTarget.capture(context)
|
||||
write_result(tmp_path)
|
||||
target.apply(tmp_path)
|
||||
assert nested.image == source
|
||||
assert tuple(source.size) == (8, 6)
|
||||
assert second_instance.node_tree.nodes[nested.name].image == source
|
||||
|
||||
|
||||
def test_linked_material_target_is_unavailable(texture, tmp_path):
|
||||
path = str(tmp_path / "linked.blend")
|
||||
bpy.data.libraries.write(path, {bpy.data.materials["Texture material"]})
|
||||
with bpy.data.libraries.load(path, link=True) as (source, destination):
|
||||
destination.materials = source.materials
|
||||
linked = destination.materials[0].node_tree.nodes["Source texture"]
|
||||
context = texture_context(linked)
|
||||
assert active_texture_node(context) is None
|
||||
target = ImageEditTarget(linked, linked.image, linked.id_data)
|
||||
with pytest.raises(RuntimeError, match="no longer available"):
|
||||
target.validate()
|
||||
|
||||
|
||||
def test_texture_menu_uses_node_dimensions_and_ai_settings(texture):
|
||||
context = texture_context(texture)
|
||||
layout = SimpleNamespace(operator=Mock(), separator=Mock(), menu=Mock())
|
||||
with patch("anyimage.properties.ai_status", return_value={"ready": True}):
|
||||
menu.AnyImageTextureNodeMenu.draw(SimpleNamespace(layout=layout), context)
|
||||
assert layout.operator_context == "INVOKE_DEFAULT"
|
||||
assert [call.args[0] for call in layout.operator.call_args_list] == [
|
||||
"anyimage.remove_image_background", "anyimage.upscale_image", "anyimage.open_ai_environment_settings",
|
||||
]
|
||||
assert layout.operator.call_args_list[1].kwargs["text"] == "Upscale (8 × 6)"
|
||||
menu.draw_texture_node_context_menu(SimpleNamespace(layout=layout), context)
|
||||
layout.menu.assert_called_once_with(menu.AnyImageTextureNodeMenu.bl_idname, text="AnyImage", icon="PLUGIN")
|
||||
texture.image = None
|
||||
layout.menu.reset_mock()
|
||||
menu.draw_texture_node_context_menu(SimpleNamespace(layout=layout), context)
|
||||
layout.menu.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("busy,limit,expected", [(False, 2048, True), (True, 2048, False), (False, 4, False)])
|
||||
def test_texture_poll_applies_busy_and_size_limits(texture, busy, limit, expected):
|
||||
context = texture_context(texture)
|
||||
with patch.object(runtime, "server_busy", return_value=busy), patch.object(
|
||||
upscale, "configured_max_ai_input_size", return_value=limit,
|
||||
):
|
||||
assert upscale.UpscaleImage.poll(context) == expected
|
||||
assert remove_background.RemoveImageBackground.poll(context) == (not busy)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("operation,prefix", [(upscale.UpscaleImage, "upscale."), (remove_background.RunBackgroundRemoval, "image.")])
|
||||
@pytest.mark.parametrize("shared", [False, True])
|
||||
def test_texture_action_undo_redo_restores_image_and_binding(texture, tmp_path, operation, prefix, shared):
|
||||
source_name = texture.image.name
|
||||
source_pixels = images.image_pixels(texture.image)
|
||||
if shared:
|
||||
other = texture.id_data.nodes.new("ShaderNodeTexImage")
|
||||
other.image = texture.image
|
||||
other.name = "Other texture"
|
||||
write_result(tmp_path, prefix)
|
||||
|
||||
def execute(operator, context):
|
||||
node = bpy.data.materials["Texture material"].node_tree.nodes["Source texture"]
|
||||
operator._image_target = ImageEditTarget.capture(texture_context(node))
|
||||
if operation == upscale.UpscaleImage:
|
||||
operator.model = "PISA_SR"
|
||||
operator.response(context, SimpleNamespace(directory=tmp_path))
|
||||
return {"FINISHED"}
|
||||
|
||||
bpy.context.preferences.edit.use_global_undo = True
|
||||
with patch.object(operation, "execute", execute), patch.object(
|
||||
operation, "poll", classmethod(lambda cls, context: True), create=True,
|
||||
):
|
||||
bpy.utils.register_class(operation)
|
||||
try:
|
||||
bpy.ops.ed.undo_push(message="Before texture processing")
|
||||
call = getattr(bpy.ops.anyimage, operation.bl_idname.split(".")[1])
|
||||
assert call("EXEC_DEFAULT", True) == {"FINISHED"}
|
||||
result_pixels = images.image_pixels(texture.image)
|
||||
assert tuple(texture.image.size) == (8, 6)
|
||||
assert bpy.ops.ed.undo() == {"FINISHED"}
|
||||
restored = bpy.data.materials["Texture material"].node_tree.nodes["Source texture"]
|
||||
assert restored.image.name == source_name
|
||||
assert tuple(restored.image.size) == (4, 3)
|
||||
np.testing.assert_array_equal(images.image_pixels(restored.image), source_pixels)
|
||||
assert bpy.ops.ed.redo() == {"FINISHED"}
|
||||
redone = bpy.data.materials["Texture material"].node_tree.nodes["Source texture"]
|
||||
assert tuple(redone.image.size) == (8, 6)
|
||||
np.testing.assert_array_equal(images.image_pixels(redone.image), result_pixels)
|
||||
if shared:
|
||||
other = redone.id_data.nodes["Other texture"]
|
||||
np.testing.assert_array_equal(images.image_pixels(other.image), source_pixels)
|
||||
assert other.image != redone.image
|
||||
finally:
|
||||
bpy.utils.unregister_class(operation)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("operation,module,prefix", [
|
||||
(upscale.UpscaleImage, upscale, "upscale."),
|
||||
(remove_background.RunBackgroundRemoval, remove_background, "image."),
|
||||
])
|
||||
@pytest.mark.parametrize("outcome", ["succeeded", "cancelled", "failed", "invalid_target"])
|
||||
def test_texture_job_request_and_completion(texture, tmp_path, operation, module, prefix, outcome):
|
||||
context = texture_context(texture)
|
||||
context.window = object()
|
||||
context.window_manager = SimpleNamespace(
|
||||
progress_begin=Mock(), progress_end=Mock(), event_timer_add=Mock(return_value=object()),
|
||||
event_timer_remove=Mock(), modal_handler_add=Mock(),
|
||||
)
|
||||
write_result(tmp_path, prefix)
|
||||
controller = SimpleNamespace(
|
||||
submit=Mock(return_value={"job_id": "texture-job", "directory": str(tmp_path)}),
|
||||
status=Mock(return_value={"state": "failed" if outcome == "invalid_target" else outcome}),
|
||||
mark_job_complete=Mock(), cancel=Mock(),
|
||||
)
|
||||
original_pixels = images.image_pixels(texture.image)
|
||||
original_execute = operation.execute
|
||||
|
||||
def execute(operator, _context):
|
||||
with ExitStack() as stack:
|
||||
for name in ("require_environment", "require_model"):
|
||||
stack.enter_context(patch.object(module, name))
|
||||
stack.enter_context(patch.object(module, "production_device", return_value="CPU"))
|
||||
stack.enter_context(patch.object(operation, "controller", return_value=controller))
|
||||
stack.enter_context(patch.object(runtime, "update_ui"))
|
||||
stack.enter_context(patch.object(runtime, "redraw_ui"))
|
||||
if operation == upscale.UpscaleImage:
|
||||
stack.enter_context(patch.object(upscale, "configured_upscale_model", return_value="SAFMN_REAL_X4"))
|
||||
stack.enter_context(patch.object(upscale, "configured_max_ai_input_size", return_value=2048))
|
||||
assert original_execute(operator, context) == {"RUNNING_MODAL"}
|
||||
job = runtime.active_job
|
||||
job.start_thread.join(timeout=5)
|
||||
assert not job.start_thread.is_alive()
|
||||
kind, parameters = controller.submit.call_args.args
|
||||
assert kind == operation.job_type
|
||||
assert parameters["start_frame"] == 1
|
||||
assert parameters["device"] == "cpu"
|
||||
assert parameters["model"] == ("SAFMN_REAL_X4" if operation == upscale.UpscaleImage else "BEN2_BASE")
|
||||
input_path = operator.input_path
|
||||
assert operator.delete_input
|
||||
# Selection changes must not redirect completion.
|
||||
other = texture.id_data.nodes.new("ShaderNodeTexImage")
|
||||
texture.id_data.nodes.active = other
|
||||
if outcome == "invalid_target":
|
||||
texture.image = None
|
||||
controller.status.return_value = {"state": "succeeded"}
|
||||
bpy.ops.ed.undo_push(message="Before asynchronous texture result")
|
||||
result = operator.modal(context, SimpleNamespace(type="TIMER"))
|
||||
assert result == ({"FINISHED"} if outcome == "succeeded" else {"CANCELLED"})
|
||||
assert runtime.active_job is None
|
||||
assert operator._image_target is None
|
||||
assert not Path(input_path).exists()
|
||||
assert other.image is None
|
||||
if outcome == "succeeded":
|
||||
assert tuple(texture.image.size) == (8, 6)
|
||||
elif outcome != "invalid_target":
|
||||
np.testing.assert_array_equal(images.image_pixels(texture.image), original_pixels)
|
||||
context.window_manager.event_timer_remove.assert_called_once()
|
||||
return result
|
||||
|
||||
with patch.object(operation, "execute", execute), patch.object(
|
||||
operation, "poll", classmethod(lambda cls, context: True), create=True,
|
||||
):
|
||||
bpy.utils.register_class(operation)
|
||||
try:
|
||||
call = getattr(bpy.ops.anyimage, operation.bl_idname.split(".")[1])
|
||||
if outcome in {"failed", "invalid_target"}:
|
||||
message = "Server task failed" if outcome == "failed" else "no longer available"
|
||||
with pytest.raises(RuntimeError, match=message):
|
||||
call("EXEC_DEFAULT", True)
|
||||
else:
|
||||
assert call("EXEC_DEFAULT", True) == ({"FINISHED"} if outcome == "succeeded" else {"CANCELLED"})
|
||||
finally:
|
||||
runtime.close_active()
|
||||
bpy.utils.unregister_class(operation)
|
||||
@@ -6,6 +6,7 @@ from PIL import Image
|
||||
|
||||
from anyimage.operators.upscale import UpscaleImage
|
||||
from anyimage.common.image import image_pixels
|
||||
from anyimage.common.image_target import ImageEditTarget
|
||||
|
||||
|
||||
def test_upscale_result_replacement_undo_restores_source(tmp_path):
|
||||
@@ -21,8 +22,7 @@ def test_upscale_result_replacement_undo_restores_source(tmp_path):
|
||||
Image.new("RGBA", (16,16), (32,64,96,128)).save(tmp_path / "upscale.0001.png")
|
||||
|
||||
def execute(operator, context):
|
||||
operator.source_object_name = owner_name
|
||||
operator.source_image_name = source_name
|
||||
operator._image_target = ImageEditTarget.capture(SimpleNamespace(object=owner))
|
||||
operator.model = "PISA_SR"
|
||||
operator.response(context, SimpleNamespace(directory=tmp_path))
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -290,8 +290,10 @@ class AddonRegistrationTest(BlenderTestCase):
|
||||
)
|
||||
existing_context_draw = object()
|
||||
existing_outliner_draw = object()
|
||||
existing_node_draw = object()
|
||||
self.context_menu_draws.append(existing_context_draw)
|
||||
self.outliner_menu_draws.append(existing_outliner_draw)
|
||||
self.node_menu_draws.append(existing_node_draw)
|
||||
|
||||
self.assertFalse(any("debug" in cls.bl_idname.lower() for cls in self.anyimage.CLASSES if hasattr(cls, "bl_idname")))
|
||||
settings = self.anyimage.properties.AnyImageSettings.__annotations__
|
||||
@@ -346,6 +348,9 @@ class AddonRegistrationTest(BlenderTestCase):
|
||||
)
|
||||
self.assertFalse(hasattr(self.fake_bpy.types.Image, "anyimage_upscale_source"))
|
||||
|
||||
self.assertEqual(self.node_menu_draws, [self.anyimage.draw_texture_node_context_menu, existing_node_draw])
|
||||
self.assertIn(self.anyimage.AnyImageTextureNodeMenu, self.registered)
|
||||
|
||||
unregister_order = []
|
||||
original_unregister = self.fake_bpy.utils.unregister_class
|
||||
def unregister(cls):
|
||||
@@ -362,4 +367,9 @@ class AddonRegistrationTest(BlenderTestCase):
|
||||
self.assertEqual(self.status_bar_draws, [])
|
||||
self.assertEqual(self.context_menu_draws, [existing_context_draw])
|
||||
self.assertEqual(self.outliner_menu_draws, [existing_outliner_draw])
|
||||
self.assertEqual(self.node_menu_draws, [existing_node_draw])
|
||||
self.anyimage.register()
|
||||
self.assertEqual(self.node_menu_draws, [self.anyimage.draw_texture_node_context_menu, existing_node_draw])
|
||||
self.anyimage.unregister()
|
||||
self.assertEqual(self.node_menu_draws, [existing_node_draw])
|
||||
self.assertFalse(hasattr(self.fake_bpy.types.Image, "anyimage_upscale_source"))
|
||||
|
||||
@@ -96,12 +96,12 @@ class ImageMenuTest(BlenderTestCase):
|
||||
|
||||
def test_image_operators_stay_clickable_until_server_is_busy(self):
|
||||
context = SimpleNamespace(
|
||||
object=SimpleNamespace(data=SimpleNamespace()),
|
||||
object=SimpleNamespace(type="EMPTY", empty_display_type="IMAGE", data=SimpleNamespace()),
|
||||
)
|
||||
operators = (
|
||||
(self.convert_to_plane, self.convert_to_plane.ConvertToDepthPlane),
|
||||
(self.convert_to_plane, self.convert_to_plane.ConvertToReliefPlane),
|
||||
(self.remove_background, self.remove_background.RemoveImageBackground),
|
||||
self.convert_to_plane.ConvertToDepthPlane,
|
||||
self.convert_to_plane.ConvertToReliefPlane,
|
||||
self.remove_background.RemoveImageBackground,
|
||||
)
|
||||
|
||||
for busy, expected in ((False, True), (True, False)):
|
||||
@@ -113,18 +113,6 @@ class ImageMenuTest(BlenderTestCase):
|
||||
return_value=busy,
|
||||
),
|
||||
):
|
||||
for module, operator in operators:
|
||||
with (
|
||||
self.subTest(operator=operator.__name__),
|
||||
patch.object(
|
||||
module,
|
||||
"is_image_empty",
|
||||
return_value=True,
|
||||
),
|
||||
patch.object(
|
||||
module,
|
||||
"video_total_frames",
|
||||
return_value=1,
|
||||
),
|
||||
):
|
||||
for operator in operators:
|
||||
with self.subTest(operator=operator.__name__):
|
||||
self.assertEqual(operator.poll(context), expected)
|
||||
|
||||
@@ -439,6 +439,7 @@ class PreferencesTest(BlenderTestCase):
|
||||
type="EMPTY",
|
||||
empty_display_type="IMAGE",
|
||||
data=source_image,
|
||||
image_user=None,
|
||||
)
|
||||
context = SimpleNamespace(
|
||||
object=source_object,
|
||||
@@ -457,8 +458,8 @@ class PreferencesTest(BlenderTestCase):
|
||||
return_value=1024,
|
||||
),
|
||||
patch.object(
|
||||
self.upscale,
|
||||
"prepare_image_input",
|
||||
self.upscale.ImageEditTarget,
|
||||
"prepare",
|
||||
return_value=(Path("input.png"), False),
|
||||
),
|
||||
patch.object(
|
||||
|
||||
Reference in New Issue
Block a user