Add tests for clipboard image functionality and mesh plane asset creation

- Implemented a comprehensive test suite for the clipboard image module, covering various functionalities including operator registration, context handling, and image processing.
- Created a new mesh plane asset with customizable parameters and integrated it into the geometry node system.
- Developed a validation script to ensure the correct behavior of the mesh plane asset, including input handling and output verification.
This commit is contained in:
2026-08-22 09:49:00 +08:00
parent af5603045c
commit 988332548e
49 changed files with 4419 additions and 291 deletions

View File

@@ -90,8 +90,12 @@ class AddonTest(unittest.TestCase):
cls.depth_build = importlib.import_module(
"anyimage.operators.depth_plane.build"
)
cls.mesh_plane = importlib.import_module(
"anyimage.operators.mesh_plane"
)
cls.image_background = importlib.import_module("anyimage.operators.background")
cls.image_mesh = importlib.import_module("anyimage.operators.surface")
cls.image_lasso = importlib.import_module("anyimage.operators.image_lasso")
cls.balloon = importlib.import_module("anyimage.operators.balloon.operator")
cls.balloon_geometry = importlib.import_module(
"anyimage.operators.balloon.geometry"
@@ -514,65 +518,230 @@ class AddonTest(unittest.TestCase):
{"models": ["MOGE2_VITS_NORMAL", "BEN2_BASE"]},
)
def test_image_menu_offers_required_model_repair_when_missing(self):
calls = []
class Layout:
operator_context = ""
enabled = True
def __init__(self, enabled=True):
self.enabled = enabled
def row(self):
return Layout(self.enabled)
def operator(self, identifier, **_options):
calls.append((identifier, self.enabled))
return SimpleNamespace()
def separator(self):
calls.append(("separator", self.enabled))
menu = self.addon.AnyImageImageMenu()
menu.layout = Layout()
def test_ai_status_checks_environment_and_required_model_files(self):
properties = self.addon.properties
with (
patch.object(self.addon.runtime, "environment_ready", return_value=True),
patch.object(
self.addon.runtime,
"environment_ready",
return_value=True,
),
patch(
"anyimage.properties.required_models_ready",
return_value=False,
properties,
"model_catalog",
return_value=(
{"key": "MOGE2_VITS_NORMAL", "ready": True},
{"key": "BEN2_BASE", "ready": False},
),
),
):
menu.draw(None)
status = properties.ai_status()
self.assertTrue(status["environment_ready"])
self.assertEqual(status["missing_models"], ("BEN2_BASE",))
self.assertFalse(status["ready"])
self.assertEqual(properties.ai_setup_label(status), "Download Required Models…")
self.assertEqual(
calls[0],
("anyimage.activate_balloon_tool", True),
with (
patch.object(self.addon.runtime, "environment_ready", return_value=False),
patch.object(
properties,
"model_catalog",
return_value=tuple(
{"key": key, "ready": True}
for key in properties.REQUIRED_MODEL_KEYS
),
),
):
status = properties.ai_status()
self.assertFalse(status["ready"])
self.assertEqual(properties.ai_setup_label(status), "Set Up AI Environment…")
def test_setup_ai_environment_selects_the_needed_install_step(self):
calls = []
ops = SimpleNamespace(
anyimage=SimpleNamespace(
install_environment=lambda *_args: calls.append("official")
or {"FINISHED"},
install_environment_mirror=lambda *_args: calls.append("mirror")
or {"FINISHED"},
download_required_models=lambda *_args: calls.append("models")
or {"FINISHED"},
)
)
operator = self.model_operators.SetupAIEnvironment()
with patch.object(self.fake_bpy, "ops", ops, create=True):
operator.use_mirror = True
with patch.object(
self.model_operators,
"ai_status",
return_value={
"environment_ready": False,
"missing_models": ("MOGE2_VITS_NORMAL", "BEN2_BASE"),
"ready": False,
},
):
self.assertEqual(operator.execute(None), {"FINISHED"})
with patch.object(
self.model_operators,
"ai_status",
return_value={
"environment_ready": True,
"missing_models": ("BEN2_BASE",),
"ready": False,
},
):
self.assertEqual(operator.execute(None), {"FINISHED"})
self.assertEqual(calls, ["mirror", "models"])
def test_ai_environment_ui_types_are_registered(self):
self.assertIn(self.model_operators.SetupAIEnvironment, self.addon.CLASSES)
self.assertIn(
("anyimage.download_required_models", True),
calls,
self.model_operators.OpenAIEnvironmentSettings,
self.addon.CLASSES,
)
self.assertIn(("anyimage.convert_to_depth_plane", False), calls)
self.assertIn(("anyimage.convert_to_surface", False), calls)
self.assertIn(("anyimage.remove_image_background", False), calls)
panel = importlib.import_module("anyimage.panel")
self.assertFalse(hasattr(panel, "ModelsPanel"))
def test_image_menu_keeps_disabled_features_when_environment_is_missing(self):
def test_mesh_plane_operator_is_registered_with_material_options(self):
operator = self.mesh_plane.ConvertToMeshPlane
self.assertIn(operator, self.addon.CLASSES)
self.assertEqual(operator.bl_idname, "anyimage.convert_to_mesh_plane")
self.assertFalse(operator.__annotations__["shadeless"]["default"])
self.assertFalse(operator.__annotations__["keep_original"]["default"])
self.assertIn("invoke", operator.__dict__)
self.assertNotIn("draw", operator.__dict__)
self.assertIn("REGISTER", operator.bl_options)
self.assertIn("UNDO", operator.bl_options)
image_empty = SimpleNamespace(
type="EMPTY",
empty_display_type="IMAGE",
data=object(),
)
mesh_plane = SimpleNamespace(
type="MESH",
modifiers=[
SimpleNamespace(node_group=SimpleNamespace(name="O Mesh Plane"))
],
)
plain_mesh = SimpleNamespace(type="MESH", modifiers=[])
self.assertTrue(operator.poll(SimpleNamespace(object=image_empty)))
self.assertTrue(operator.poll(SimpleNamespace(object=mesh_plane)))
self.assertFalse(operator.poll(SimpleNamespace(object=plain_mesh)))
def test_mesh_plane_redo_keeps_the_original_world_matrix(self):
operator = self.mesh_plane.ConvertToMeshPlane()
operator.shadeless = False
operator.keep_original = True
operator.source_object_name = ""
original_matrix = (
(1.0, 0.0, 0.0, 3.0),
(0.0, 1.0, 0.0, -2.0),
(0.0, 0.0, 1.0, 7.0),
(0.0, 0.0, 0.0, 1.0),
)
source = SimpleNamespace(
name="Source Image",
type="EMPTY",
empty_display_type="IMAGE",
data=object(),
matrix_world=original_matrix,
)
context = SimpleNamespace(object=source)
matrices = []
def capture_build(*_args, **kwargs):
matrices.append(kwargs["matrix_world"])
with (
patch.object(
self.mesh_plane.bpy.data.objects,
"get",
return_value=source,
),
patch.object(
self.mesh_plane,
"restore_matrix",
side_effect=lambda values: tuple(values),
),
patch.object(
self.mesh_plane,
"build_mesh_plane",
side_effect=capture_build,
),
):
self.assertEqual(operator.invoke(context, None), {"FINISHED"})
source.matrix_world = (
(1.0, 0.0, 0.0, 0.0),
(0.0, 1.0, 0.0, 0.0),
(0.0, 0.0, 1.0, 0.0),
(0.0, 0.0, 0.0, 1.0),
)
self.assertEqual(operator.execute(context), {"FINISHED"})
expected = self.mesh_plane.flatten_matrix(original_matrix)
self.assertEqual(matrices, [expected, expected])
def test_mesh_plane_uses_a_single_quad_and_geometry_nodes(self):
import inspect
mesh_source = inspect.getsource(self.mesh_plane.create_mesh_plane_mesh)
processing_source = inspect.getsource(
self.mesh_plane.add_mesh_plane_processing
)
self.assertIn("((0, 1, 2, 3),)", mesh_source)
self.assertIn('("Subdivide", max(0, int(subdivisions)))', processing_source)
self.assertIn('("Thickness", max(0.0, float(thickness)))', processing_source)
self.assertIn('("Material", material)', processing_source)
def test_image_menu_keeps_ai_features_clickable_and_offers_models(self):
calls = []
labels = {}
class Layout:
operator_context = ""
enabled = True
def __init__(self, enabled=True):
self.enabled = enabled
def operator(self, identifier, **options):
calls.append((identifier, self.enabled))
labels[identifier] = options.get("text")
return SimpleNamespace()
def row(self):
return Layout(self.enabled)
def separator(self):
calls.append(("separator", self.enabled))
menu = self.addon.AnyImageImageMenu()
menu.layout = Layout()
with patch(
"anyimage.properties.ai_status",
return_value={
"environment_ready": True,
"missing_models": ("BEN2_BASE",),
"ready": False,
},
):
menu.draw(None)
self.assertEqual(
calls,
[
("anyimage.convert_to_mesh_plane", True),
("anyimage.convert_to_depth_plane", True),
("anyimage.convert_to_surface", True),
("anyimage.remove_image_background", True),
("separator", True),
("anyimage.setup_ai_environment", True),
],
)
self.assertEqual(
labels["anyimage.setup_ai_environment"],
"Download Required Models…",
)
def test_image_menu_opens_environment_settings_when_ai_is_ready(self):
calls = []
class Layout:
operator_context = ""
enabled = True
def operator(self, identifier, **_options):
calls.append((identifier, self.enabled))
@@ -583,29 +752,18 @@ class AddonTest(unittest.TestCase):
menu = self.addon.AnyImageImageMenu()
menu.layout = Layout()
with (
patch.object(
self.addon.runtime,
"environment_ready",
return_value=False,
),
patch(
"anyimage.properties.required_models_ready",
return_value=False,
),
with patch(
"anyimage.properties.ai_status",
return_value={
"environment_ready": True,
"missing_models": (),
"ready": True,
},
):
menu.draw(None)
self.assertEqual(
calls[0],
("anyimage.activate_balloon_tool", True),
)
self.assertIn(("anyimage.install_environment", True), calls)
self.assertIn(("anyimage.install_environment_mirror", True), calls)
self.assertNotIn(("anyimage.download_required_models", True), calls)
self.assertIn(("anyimage.convert_to_depth_plane", False), calls)
self.assertIn(("anyimage.convert_to_surface", False), calls)
self.assertIn(("anyimage.remove_image_background", False), calls)
self.assertIn(("anyimage.open_ai_environment_settings", True), calls)
self.assertNotIn(("anyimage.setup_ai_environment", True), calls)
def test_balloon_contour_is_smooth_and_evenly_resampled(self):
import numpy as np
@@ -621,6 +779,157 @@ class AddonTest(unittest.TestCase):
self.assertGreater(self.balloon_geometry.polygon_area(contour), 0.0)
self.assertLess(float(lengths.std() / lengths.mean()), 0.15)
def test_image_lasso_clears_rgba_outside_the_selection(self):
import numpy as np
top_down = np.full((3, 3, 4), (0.2, 0.4, 0.6, 1.0), dtype=np.float32)
source = np.flipud(top_down).ravel()
pixels, bounds = self.image_lasso.extract_lasso_pixels(
source,
(3, 3),
((0, 0), (3, 0), (0, 3)),
)
result = np.flipud(np.asarray(pixels).reshape((3, 3, 4)))
self.assertEqual(bounds, (0, 0, 3, 3))
np.testing.assert_allclose(result[2, 2], (0.0, 0.0, 0.0, 0.0))
self.assertEqual(float(result[0, 0, 3]), 1.0)
def test_image_lasso_clears_hidden_rgb_from_source_transparency(self):
import numpy as np
top_down = np.full((2, 2, 4), (0.2, 0.4, 0.6, 1.0), dtype=np.float32)
top_down[0, 0, 3] = 0.0
pixels, _bounds = self.image_lasso.extract_lasso_pixels(
np.flipud(top_down).ravel(),
(2, 2),
((0, 0), (2, 0), (2, 2), (0, 2)),
)
result = np.flipud(np.asarray(pixels).reshape((2, 2, 4)))
np.testing.assert_allclose(result[0, 0], (0.0, 0.0, 0.0, 0.0))
def test_image_lasso_keeps_out_of_image_canvas_transparent(self):
import numpy as np
top_down = np.ones((2, 2, 4), dtype=np.float32)
pixels, bounds = self.image_lasso.extract_lasso_pixels(
np.flipud(top_down).ravel(),
(2, 2),
((-1, -1), (2, -1), (2, 2), (-1, 2)),
)
result = np.flipud(np.asarray(pixels).reshape((3, 3, 4)))
self.assertEqual(bounds, (-1, -1, 2, 2))
self.assertEqual(float(result[0, 0, 3]), 0.0)
self.assertEqual(float(result[1, 1, 3]), 1.0)
def test_image_lasso_rejects_a_fully_out_of_image_region(self):
import numpy as np
with self.assertRaisesRegex(ValueError, "does not overlap"):
self.image_lasso.extract_lasso_pixels(
np.ones(2 * 2 * 4, dtype=np.float32),
(2, 2),
((3, 3), (4, 3), (4, 4), (3, 4)),
)
def test_image_lasso_crops_to_visible_alpha_with_pixel_padding(self):
import numpy as np
rgba = np.zeros((4, 5, 4), dtype=np.float32)
rgba[1:3, 2:4] = (0.2, 0.4, 0.6, 1.0)
pixels, bounds = self.image_lasso.crop_pixels_to_alpha(
np.flipud(rgba).ravel(),
(10, 20, 15, 24),
padding=1,
)
result = np.flipud(np.asarray(pixels).reshape((4, 4, 4)))
self.assertEqual(bounds, (11, 20, 15, 24))
self.assertEqual(result.shape, (4, 4, 4))
self.assertTrue(np.all(result[[0, -1], :, 3] == 0.0))
self.assertTrue(np.all(result[:, [0, -1], 3] == 0.0))
self.assertTrue(np.all(result[1:3, 1:3, 3] == 1.0))
def test_image_lasso_tool_and_operators_are_registered(self):
self.assertIn(self.image_lasso.ExtractImageLasso, self.addon.CLASSES)
self.assertIn(self.image_lasso.AnalyzeImageLasso, self.addon.CLASSES)
self.assertIn(self.image_lasso.ActivateImageLassoTool, self.addon.CLASSES)
self.assertNotIn(self.image_lasso.ImageLassoTool, self.addon.CLASSES)
self.assertNotIn(self.image_lasso.ImageBoxTool, self.addon.CLASSES)
self.assertEqual(
self.image_lasso.ImageLassoTool.bl_operator,
self.image_lasso.ExtractImageLasso.bl_idname,
)
self.assertEqual(
self.image_lasso.ImageLassoTool.bl_icon,
"ops.sculpt.lasso_mask",
)
self.assertEqual(
self.image_lasso.ImageBoxTool.bl_icon,
"ops.sculpt.border_mask",
)
self.assertEqual(
self.image_lasso.ImageBoxTool.bl_keymap[0][2]["properties"],
[("selection_shape", "BOX")],
)
self.assertEqual(
self.image_lasso.ImageLassoTool.bl_keymap[0][2]["properties"],
[("selection_shape", "LASSO")],
)
self.assertNotIn("bl_cursor", self.image_lasso.ImageBoxTool.__dict__)
self.assertNotIn("bl_cursor", self.image_lasso.ImageLassoTool.__dict__)
self.assertEqual(
self.image_lasso.ExtractImageLasso.__annotations__["selection_shape"][
"default"
],
"BOX",
)
operator = self.image_lasso.ExtractImageLasso
self.assertIn("keep_original", operator.__annotations__)
self.assertFalse(operator.__annotations__["keep_original"]["default"])
self.assertIn("padding", operator.__annotations__)
self.assertEqual(operator.__annotations__["padding"]["default"], 0)
settings = self.addon.properties.AnyImageSettings.__annotations__
self.assertFalse(settings["image_lasso_keep_original"]["default"])
self.assertEqual(settings["image_lasso_padding"]["default"], 0)
def test_image_box_builds_a_rectangular_screen_path(self):
self.assertEqual(
self.image_lasso.box_screen_path(((2, 3), (8, 9))),
((2, 3), (8, 3), (8, 9), (2, 9)),
)
self.assertEqual(
self.image_lasso.box_screen_path(((2, 3), (3, 9))),
(),
)
def test_image_extract_selection_uses_white_dashed_segments(self):
segments = self.image_lasso.dashed_line_segments(
((0, 0), (10, 0)), dash_length=3.0, gap_length=2.0
)
self.assertEqual(
segments,
((0.0, 0.0), (3.0, 0.0), (5.0, 0.0), (8.0, 0.0)),
)
def test_image_extract_activation_defaults_to_box(self):
import inspect
source = inspect.getsource(self.image_lasso.ActivateImageLassoTool.execute)
self.assertIn("name=ImageBoxTool.bl_idname", source)
def test_keep_original_restores_the_source_empty_selection(self):
import inspect
execute_source = inspect.getsource(self.image_lasso.ExtractImageLasso.execute)
response_source = inspect.getsource(self.image_lasso.AnalyzeImageLasso.response)
expected = "source_object if self.keep_original else target"
self.assertIn(expected, execute_source)
self.assertIn(expected, response_source)
def test_balloon_interior_points_have_uniform_spacing(self):
import numpy as np
@@ -909,6 +1218,10 @@ class AddonTest(unittest.TestCase):
response_source = inspect.getsource(self.balloon.AnalyzeBalloon.response)
tool_source = inspect.getsource(self.balloon.BalloonTool.draw_settings)
self.assertIn("bpy.ops.anyimage.analyze_balloon", execute_source)
self.assertIn("ready = ai_ready()", invoke_source)
self.assertIn("status = ai_status()", tool_source)
self.assertIn('"anyimage.setup_ai_environment"', tool_source)
self.assertIn("ai_setup_label(status)", tool_source)
self.assertIn("prepare_image_input(", execute_source)
self.assertIn(
"settings.balloon_contour_analysis",
@@ -1360,7 +1673,7 @@ class AddonTest(unittest.TestCase):
self.assertIn(expected, original_depth_source)
self.assertIn(expected, depth_source)
def test_image_operators_require_environment_and_required_models(self):
def test_image_operators_stay_clickable_until_server_is_busy(self):
context = SimpleNamespace(
object=SimpleNamespace(data=SimpleNamespace()),
)
@@ -1370,31 +1683,13 @@ class AddonTest(unittest.TestCase):
(self.image_background, self.image_background.RemoveImageBackground),
)
states = (
(False, True, False),
(True, False, False),
(True, True, True),
)
for environment_ready, models_ready, expected in states:
for busy, expected in ((False, True), (True, False)):
with (
self.subTest(
environment_ready=environment_ready,
models_ready=models_ready,
expected=expected,
),
patch.object(
self.addon.runtime,
"environment_ready",
return_value=environment_ready,
),
self.subTest(busy=busy),
patch.object(
self.addon.runtime,
"server_busy",
return_value=False,
),
patch(
"anyimage.properties.required_models_ready",
return_value=models_ready,
return_value=busy,
),
):
for module, operator in operators:
@@ -1500,6 +1795,14 @@ class AddonTest(unittest.TestCase):
self.assertEqual(attribute, "server")
def test_runtime_registers_system_operators_and_status_bar(self):
self.assertIn(
self.addon.clipboard_image.PasteClipboardImage,
self.addon.CLASSES,
)
self.assertIn(
self.addon.clipboard_image.TrackNativeCopy,
self.addon.CLASSES,
)
self.addon.register()
system_classes = self.addon.runtime.operator_classes()
@@ -1507,12 +1810,26 @@ class AddonTest(unittest.TestCase):
self.assertEqual(
self.registered_tools,
[
(
self.image_lasso.ImageBoxTool,
{
"group": True,
"separator": True,
},
),
(
self.image_lasso.ImageLassoTool,
{
"after": {self.image_lasso.ImageBoxTool.bl_idname},
"separator": False,
},
),
(
self.balloon.BalloonTool,
{
"separator": False,
},
)
),
],
)
self.assertEqual(len(self.status_bar_draws), 1)

View File

@@ -0,0 +1,892 @@
import importlib
import struct
import sys
import tempfile
import unittest
from pathlib import Path
from types import ModuleType, SimpleNamespace
from unittest.mock import Mock, patch
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT / "src"))
class ClipboardImageTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.temporary_directory = tempfile.TemporaryDirectory()
cls.remove_anyimage_modules()
fake_bpy = ModuleType("bpy")
fake_bpy.app = SimpleNamespace(version=(5, 1, 0))
fake_bpy.props = SimpleNamespace(
BoolProperty=lambda **options: options,
CollectionProperty=lambda **options: options,
EnumProperty=lambda **options: options,
FloatProperty=lambda **options: options,
FloatVectorProperty=lambda **options: options,
IntProperty=lambda **options: options,
PointerProperty=lambda **options: options,
StringProperty=lambda **options: options,
)
fake_bpy.types = SimpleNamespace(
GizmoGroup=object,
Image=object,
Menu=object,
Operator=object,
Panel=object,
PropertyGroup=object,
Scene=object,
TOPBAR_MT_file_defaults=SimpleNamespace(),
)
fake_bpy.utils = SimpleNamespace()
fake_bpy.context = SimpleNamespace(
window_manager=SimpleNamespace(
keyconfigs=SimpleNamespace(addon=None),
)
)
fake_bpy.data = SimpleNamespace()
sys.modules["bpy"] = fake_bpy
cls.fake_bpy = fake_bpy
cls.addon = ModuleType("anyimage")
cls.addon.__path__ = [str(PROJECT_ROOT / "src" / "anyimage")]
sys.modules["anyimage"] = cls.addon
cls.addon.clipboard_image = importlib.import_module(
"anyimage.clipboard_image"
)
cls.actions = cls.addon.clipboard_image.actions
cls.clipboard = cls.addon.clipboard_image.clipboard
@classmethod
def tearDownClass(cls):
sys.modules.pop("bpy", None)
cls.remove_anyimage_modules()
cls.temporary_directory.cleanup()
def test_operators_use_anyimage_prefix_and_plain_properties(self):
clipboard_image = self.addon.clipboard_image
self.assertEqual(
clipboard_image.PasteClipboardImage.bl_idname,
"anyimage.paste_clipboard_image",
)
self.assertEqual(
clipboard_image.TrackNativeCopy.bl_idname,
"anyimage.track_native_copy",
)
for name in clipboard_image.PasteClipboardImage.__annotations__:
self.assertFalse(name.startswith("anyimage_"), name)
self.assertFalse(name.startswith("o_"), name)
def test_registers_and_removes_clipboard_shortcuts(self):
class KeyMapItems:
def __init__(self):
self.items = []
def new(self, operator_id, event_type, value, **modifiers):
item = SimpleNamespace(
idname=operator_id,
type=event_type,
value=value,
**modifiers,
)
self.items.append(item)
return item
def remove(self, item):
self.items.remove(item)
class KeyMaps:
def __init__(self):
self.items = []
def new(self, name, space_type):
keymap = SimpleNamespace(
name=name,
space_type=space_type,
keymap_items=KeyMapItems(),
)
self.items.append(keymap)
return keymap
keymaps = KeyMaps()
window_manager = SimpleNamespace(
keyconfigs=SimpleNamespace(
addon=SimpleNamespace(keymaps=keymaps),
),
)
clipboard_image = self.addon.clipboard_image
with patch.object(
self.fake_bpy.context,
"window_manager",
window_manager,
):
clipboard_image.register_keymaps()
items = [
item
for keymap in keymaps.items
for item in keymap.keymap_items.items
]
self.assertEqual(
[item.idname for item in items],
[
"anyimage.paste_clipboard_image",
"anyimage.track_native_copy",
"anyimage.paste_clipboard_image",
"anyimage.track_native_copy",
],
)
modifier = "oskey" if sys.platform == "darwin" else "ctrl"
self.assertTrue(all(getattr(item, modifier) for item in items))
clipboard_image.unregister_keymaps()
self.assertTrue(
all(not keymap.keymap_items.items for keymap in keymaps.items)
)
def test_routes_supported_contexts(self):
contexts = (
(self.make_context("VIEW_3D"), "PLANE"),
(
self.make_context("VIEW_3D", mode="SCULPT"),
"TOOL_TEXTURE",
),
(
self.make_context("VIEW_3D", mode="PAINT_VERTEX"),
"TOOL_TEXTURE",
),
(
self.make_context("VIEW_3D", mode="PAINT_TEXTURE"),
"TOOL_TEXTURE",
),
(
self.make_context(
"NODE_EDITOR",
tree_type="ShaderNodeTree",
),
"NODE",
),
(
self.make_context(
"NODE_EDITOR",
tree_type="CompositorNodeTree",
),
"NODE",
),
(
self.make_context(
"NODE_EDITOR",
tree_type="GeometryNodeTree",
),
"NODE",
),
)
for context, expected_target in contexts:
with self.subTest(expected_target=expected_target):
self.assertEqual(
self.actions.target_for_context(context),
expected_target,
)
def test_rejects_unsupported_node_tree(self):
context = self.make_context(
"NODE_EDITOR",
tree_type="TextureNodeTree",
)
self.assertIsNone(self.actions.target_for_context(context))
def test_world_shader_uses_environment_texture(self):
world_space = SimpleNamespace(
tree_type="ShaderNodeTree",
shader_type="WORLD",
)
material_space = SimpleNamespace(
tree_type="ShaderNodeTree",
shader_type="OBJECT",
)
self.assertEqual(
self.actions._node_type_for_space(world_space),
"ShaderNodeTexEnvironment",
)
self.assertEqual(
self.actions._node_type_for_space(material_space),
"ShaderNodeTexImage",
)
def test_pinned_world_tree_uses_environment_texture(self):
world_space = SimpleNamespace(
tree_type="ShaderNodeTree",
shader_type="OBJECT",
id=SimpleNamespace(
bl_rna=SimpleNamespace(identifier="World"),
),
)
self.assertEqual(
self.actions._node_type_for_space(world_space),
"ShaderNodeTexEnvironment",
)
def test_node_location_matches_native_ui_scale_and_anchor_offset(self):
view2d = SimpleNamespace(
region_to_view=Mock(return_value=(200.0, 100.0)),
)
context = SimpleNamespace(
region=SimpleNamespace(view2d=view2d),
preferences=SimpleNamespace(
system=SimpleNamespace(ui_scale=2.0),
),
space_data=SimpleNamespace(cursor_location=(1.0, 2.0)),
)
event = SimpleNamespace(mouse_region_x=80, mouse_region_y=60)
location = self.actions._node_location(context, event)
self.assertEqual(location, (85.0, 55.0))
view2d.region_to_view.assert_called_once_with(80, 60)
def test_node_location_without_event_uses_node_cursor(self):
context = SimpleNamespace(
space_data=SimpleNamespace(cursor_location=(12.0, -8.0)),
)
self.assertEqual(
self.actions._node_location(context, None),
(12.0, -8.0),
)
def test_view3d_location_projects_mouse_at_cursor_depth(self):
projected_location = (4.0, 5.0, 6.0)
project = Mock(return_value=projected_location)
bpy_extras = ModuleType("bpy_extras")
view3d_utils = ModuleType("bpy_extras.view3d_utils")
view3d_utils.region_2d_to_location_3d = project
bpy_extras.view3d_utils = view3d_utils
cursor_location = Mock()
context = SimpleNamespace(
scene=SimpleNamespace(
cursor=SimpleNamespace(location=cursor_location),
),
region=object(),
region_data=object(),
)
event = SimpleNamespace(mouse_region_x=120, mouse_region_y=75)
with patch.dict(
sys.modules,
{
"bpy_extras": bpy_extras,
"bpy_extras.view3d_utils": view3d_utils,
},
):
result = self.actions._view3d_location(context, event)
self.assertIs(result, projected_location)
project.assert_called_once_with(
context.region,
context.region_data,
(120, 75),
cursor_location,
)
def test_view3d_location_without_event_copies_cursor_location(self):
copied_location = object()
cursor_location = SimpleNamespace(
copy=Mock(return_value=copied_location),
)
context = SimpleNamespace(
scene=SimpleNamespace(
cursor=SimpleNamespace(location=cursor_location),
),
)
result = self.actions._view3d_location(context)
self.assertIs(result, copied_location)
cursor_location.copy.assert_called_once_with()
def test_preserves_image_aspect_ratio_for_plane(self):
self.assertEqual(
self.actions._plane_half_dimensions(1920, 1080),
(1.0, 0.5625),
)
self.assertEqual(
self.actions._plane_half_dimensions(800, 1200),
(2 / 3, 1.0),
)
def test_plane_creation_delegates_to_mesh_plane_geometry_nodes(self):
import inspect
source = inspect.getsource(self.actions.add_image_plane)
self.assertIn("create_mesh_plane_object", source)
self.assertNotIn("_plane_grid", source)
self.assertNotIn("_block_grid", source)
def test_plane_import_defaults_to_reference_image(self):
options = (
self.addon.clipboard_image.PasteClipboardImage
.__annotations__["import_as"]
)
self.assertEqual(options["default"], "REFERENCE")
self.assertEqual(
[item[0] for item in options["items"]],
["PLANE", "REFERENCE"],
)
def test_plane_shadeless_is_disabled_by_default(self):
options = (
self.addon.clipboard_image.PasteClipboardImage
.__annotations__["shadeless"]
)
self.assertFalse(options["default"])
annotations = self.addon.clipboard_image.PasteClipboardImage.__annotations__
self.assertNotIn("subdivisions", annotations)
self.assertNotIn("thickness", annotations)
self.assertNotIn("unshaded", annotations)
def test_native_copy_takes_priority_while_clipboard_is_unchanged(self):
clipboard_image = self.addon.clipboard_image
original_token = clipboard_image._native_copy_token
try:
clipboard_image._native_copy_token = 41
with patch.object(
clipboard_image,
"clipboard_change_token",
return_value=41,
):
self.assertTrue(clipboard_image._should_defer_to_native_paste())
finally:
clipboard_image._native_copy_token = original_token
def test_new_system_clipboard_content_restores_image_paste(self):
clipboard_image = self.addon.clipboard_image
original_token = clipboard_image._native_copy_token
try:
clipboard_image._native_copy_token = 41
with patch.object(
clipboard_image,
"clipboard_change_token",
return_value=42,
):
self.assertFalse(clipboard_image._should_defer_to_native_paste())
finally:
clipboard_image._native_copy_token = original_token
def test_native_copy_tracker_passes_through(self):
clipboard_image = self.addon.clipboard_image
original_token = clipboard_image._native_copy_token
try:
with patch.object(
clipboard_image,
"clipboard_change_token",
return_value=73,
):
result = clipboard_image.TrackNativeCopy().execute(None)
self.assertEqual(result, {"PASS_THROUGH"})
self.assertEqual(clipboard_image._native_copy_token, 73)
finally:
clipboard_image._native_copy_token = original_token
def test_shadeless_nodes_mix_emission_with_transparency(self):
created_nodes = {}
def create_node(node_type):
node = SimpleNamespace(location=None, inputs={}, outputs={})
if node_type == "ShaderNodeBsdfTransparent":
node.outputs["BSDF"] = object()
elif node_type == "ShaderNodeEmission":
node.inputs["Color"] = object()
node.outputs["Emission"] = object()
else:
node.inputs = [object(), object(), object()]
node.outputs["Shader"] = object()
created_nodes[node_type] = node
return node
nodes = SimpleNamespace(new=Mock(side_effect=create_node))
links = SimpleNamespace(new=Mock())
image_node = SimpleNamespace(
outputs={"Color": object(), "Alpha": object()},
)
output_node = SimpleNamespace(inputs={"Surface": object()})
self.actions._create_shadeless_nodes(
nodes,
links,
image_node,
output_node,
)
mix_node = created_nodes["ShaderNodeMixShader"]
links.new.assert_any_call(image_node.outputs["Alpha"], mix_node.inputs[0])
links.new.assert_any_call(
created_nodes["ShaderNodeEmission"].outputs["Emission"],
mix_node.inputs[2],
)
self.assertEqual(links.new.call_count, 5)
def test_reference_import_routes_to_image_empty(self):
context = self.make_context("VIEW_3D")
image = object()
expected_reference = object()
expected_location = (1.0, 2.0, 3.0)
original_add_reference_image = self.actions.add_reference_image
self.actions.add_reference_image = (
lambda received_context, received_image, location=None: expected_reference
if (
received_context is context
and received_image is image
and location == expected_location
)
else None
)
try:
result = self.actions.paste_image(
context,
image,
location=expected_location,
import_as="REFERENCE",
)
finally:
self.actions.add_reference_image = original_add_reference_image
self.assertIs(result, expected_reference)
def test_only_import_as_and_shadeless_are_drawn_for_plane_paste(self):
operator = self.addon.clipboard_image.PasteClipboardImage()
drawn_properties = []
operator.layout = SimpleNamespace(
prop=lambda owner, name: drawn_properties.append((owner, name)),
)
operator.paste_target = "PLANE"
operator.import_as = "PLANE"
operator.draw(None)
operator.import_as = "REFERENCE"
operator.draw(None)
operator.paste_target = "NODE"
operator.draw(None)
self.assertEqual(
drawn_properties,
[
(operator, "import_as"),
(operator, "shadeless"),
(operator, "import_as"),
],
)
def test_redo_reuses_captured_node_location(self):
operator = self.addon.clipboard_image.PasteClipboardImage()
operator.paste_target = "NODE"
operator.location = (10.0, 20.0, 0.0)
operator.location_set = True
operator.import_as = "REFERENCE"
operator.shadeless = False
operator.report = Mock()
context = self.make_context(
"NODE_EDITOR",
tree_type="ShaderNodeTree",
)
image = SimpleNamespace(name="Clipboard Image")
clipboard_image = self.addon.clipboard_image
with (
patch.object(
clipboard_image,
"_should_defer_to_native_paste",
return_value=False,
),
patch.object(
clipboard_image,
"read_clipboard_image",
return_value=(b"image", ".png"),
),
patch.object(
clipboard_image,
"acquire_packed_image",
return_value=(image, True),
),
patch.object(clipboard_image, "paste_image") as paste_image,
):
result = operator.execute(context)
self.assertEqual(result, {"FINISHED"})
paste_image.assert_called_once_with(
context,
image,
None,
location=(10.0, 20.0),
import_as="REFERENCE",
shadeless=False,
)
def test_image_plane_requires_object_mode(self):
context = SimpleNamespace(mode="EDIT_MESH")
image = SimpleNamespace(size=(100, 100))
with self.assertRaisesRegex(
self.actions.PasteTargetError,
"Object Mode",
):
self.actions.add_image_plane(context, image)
def test_stencil_dimensions_preserve_scale_and_image_aspect(self):
self.assertEqual(
self.actions._stencil_dimensions(
(1920, 1080),
(400.0, 200.0),
),
(400.0, 225.0),
)
self.assertEqual(
self.actions._stencil_dimensions(
(800, 1200),
(128.0, 256.0),
),
(256.0 * 2 / 3, 256.0),
)
def test_finds_existing_packed_clipboard_image(self):
matching_image = FakeImage(
self.actions.CLIPBOARD_HASH_PROPERTY,
"matching-hash",
packed=True,
)
unpacked_image = FakeImage(
self.actions.CLIPBOARD_HASH_PROPERTY,
"unpacked-hash",
packed=False,
)
original_data = self.actions.bpy.data
self.actions.bpy.data = SimpleNamespace(
images=[matching_image, unpacked_image],
)
try:
self.assertIs(
self.actions._find_packed_clipboard_image("matching-hash"),
matching_image,
)
self.assertIsNone(
self.actions._find_packed_clipboard_image("unpacked-hash")
)
finally:
self.actions.bpy.data = original_data
def test_sets_and_reuses_stencil_brush_texture(self):
textures = FakeTextures()
original_data = self.actions.bpy.data
self.actions.bpy.data = SimpleNamespace(textures=textures)
brush = SimpleNamespace(
is_editable=True,
texture=None,
texture_slot=SimpleNamespace(
texture=None,
map_mode="VIEW_PLANE",
),
stencil_dimension=(256.0, 256.0),
update_tag=lambda: None,
)
context = SimpleNamespace(brush=brush)
image = SimpleNamespace(name="Clipboard Image", size=(1920, 1080))
try:
first_texture = self.actions.set_tool_texture(context, image)
second_texture = self.actions.set_tool_texture(context, image)
finally:
self.actions.bpy.data = original_data
self.assertIs(first_texture, second_texture)
self.assertEqual(len(textures), 1)
self.assertIs(brush.texture, first_texture)
self.assertIs(brush.texture_slot.texture, first_texture)
self.assertEqual(brush.texture_slot.map_mode, "STENCIL")
self.assertEqual(brush.stencil_dimension, (256.0, 144.0))
def test_localizes_read_only_active_brush(self):
local_brush = SimpleNamespace(is_editable=True, library=None)
linked_brush = SimpleNamespace(
is_editable=False,
library=None,
make_local=lambda: local_brush,
)
self.assertIs(
self.actions._ensure_editable_brush(
SimpleNamespace(),
linked_brush,
),
local_brush,
)
def test_copies_and_activates_runtime_library_brush(self):
local_brush = SimpleNamespace(
name="Paint Hard Clipboard",
is_editable=True,
library=None,
)
library_brush = SimpleNamespace(
name="Paint Hard",
is_editable=True,
library=object(),
)
paint_settings = SimpleNamespace(brush=library_brush)
context = SimpleNamespace(
mode="SCULPT",
tool_settings=SimpleNamespace(sculpt=paint_settings),
)
original_ops = getattr(self.actions.bpy, "ops", None)
def save_brush_as(**options):
self.assertEqual(
options["asset_library_reference"],
"LOCAL",
)
self.assertEqual(options["name"], "Paint Hard Clipboard")
paint_settings.brush = local_brush
return {"FINISHED"}
self.actions.bpy.ops = SimpleNamespace(
brush=SimpleNamespace(asset_save_as=save_brush_as),
)
try:
result = self.actions._ensure_editable_brush(
context,
library_brush,
)
finally:
if original_ops is None:
del self.actions.bpy.ops
else:
self.actions.bpy.ops = original_ops
self.assertIs(result, local_brush)
def test_active_brush_prefers_current_mode_settings(self):
correct_brush = object()
context = SimpleNamespace(
brush=object(),
mode="SCULPT",
tool_settings=SimpleNamespace(
sculpt=SimpleNamespace(brush=correct_brush),
),
)
self.assertIs(self.actions._active_brush(context), correct_brush)
def test_vertex_and_texture_paint_use_unbiased_white(self):
for mode in ("PAINT_VERTEX", "PAINT_TEXTURE"):
with self.subTest(mode=mode):
brush = SimpleNamespace(color=(0.2, 0.4, 0.8))
unified_settings = SimpleNamespace(color=(0.8, 0.4, 0.2))
context = SimpleNamespace(
mode=mode,
tool_settings=SimpleNamespace(
**{
self.actions.PAINT_SETTINGS_BY_MODE[mode][0]:
SimpleNamespace(
unified_paint_settings=unified_settings,
)
},
),
)
self.actions._set_unbiased_paint_color(context, brush)
self.assertEqual(brush.color, (1.0, 1.0, 1.0))
self.assertEqual(
unified_settings.color,
(1.0, 1.0, 1.0),
)
def test_sculpt_keeps_brush_color(self):
brush = SimpleNamespace(color=(0.2, 0.4, 0.8))
self.actions._set_unbiased_paint_color(
SimpleNamespace(mode="SCULPT"),
brush,
)
self.assertEqual(brush.color, (0.2, 0.4, 0.8))
def test_wraps_info_dib_as_bmp(self):
dib_header = struct.pack(
"<IiiHHIIiiII",
40,
1,
1,
1,
24,
0,
4,
0,
0,
0,
0,
)
dib_data = dib_header + b"\x00\x00\xff\x00"
bmp_data = self.clipboard.dib_to_bmp(dib_data)
signature, file_size, _, _, pixel_offset = struct.unpack_from(
"<2sIHHI",
bmp_data,
)
self.assertEqual(signature, b"BM")
self.assertEqual(file_size, len(bmp_data))
self.assertEqual(pixel_offset, 54)
self.assertEqual(bmp_data[14:], dib_data)
def test_rejects_truncated_dib(self):
with self.assertRaises(self.clipboard.ClipboardImageError):
self.clipboard.dib_to_bmp(b"\x28\x00\x00\x00")
def test_reads_macos_clipboard_image_as_png(self):
def export_image(_osascript, output_file):
output_file.write_bytes(b"png image")
with patch.object(
self.clipboard.shutil,
"which",
return_value="/usr/bin/osascript",
), patch.object(
self.clipboard,
"_export_macos_clipboard_image",
side_effect=export_image,
):
image_data, suffix = self.clipboard._read_macos_clipboard_image()
self.assertEqual(image_data, b"png image")
self.assertEqual(suffix, ".png")
def test_macos_clipboard_without_image_is_unavailable(self):
with patch.object(
self.clipboard.shutil,
"which",
return_value="/usr/bin/osascript",
), patch.object(
self.clipboard,
"_export_macos_clipboard_image",
):
with self.assertRaises(self.clipboard.ClipboardImageUnavailable):
self.clipboard._read_macos_clipboard_image()
def test_linux_clipboard_prefers_wayland_png(self):
def find_command(command_name):
if command_name == "wl-paste":
return "/usr/bin/wl-paste"
return None
with patch.object(
self.clipboard.shutil,
"which",
side_effect=find_command,
), patch.object(
self.clipboard,
"_run_image_command",
return_value=b"png image",
) as run_command:
image_data, suffix = self.clipboard._read_linux_clipboard_image()
self.assertEqual(image_data, b"png image")
self.assertEqual(suffix, ".png")
run_command.assert_called_once_with(
[
"/usr/bin/wl-paste",
"--no-newline",
"--type",
"image/png",
]
)
def test_linux_clipboard_falls_back_to_xclip(self):
def find_command(command_name):
if command_name == "xclip":
return "/usr/bin/xclip"
return None
with patch.object(
self.clipboard.shutil,
"which",
side_effect=find_command,
), patch.object(
self.clipboard,
"_run_image_command",
return_value=b"png image",
) as run_command:
image_data, suffix = self.clipboard._read_linux_clipboard_image()
self.assertEqual(image_data, b"png image")
self.assertEqual(suffix, ".png")
run_command.assert_called_once_with(
[
"/usr/bin/xclip",
"-selection",
"clipboard",
"-t",
"image/png",
"-o",
]
)
def test_linux_clipboard_requires_supported_command(self):
with patch.object(
self.clipboard.shutil,
"which",
return_value=None,
):
with self.assertRaises(self.clipboard.ClipboardImageUnavailable):
self.clipboard._read_linux_clipboard_image()
@staticmethod
def make_context(area_type, mode="OBJECT", **space_options):
return SimpleNamespace(
area=SimpleNamespace(type=area_type),
mode=mode,
region=SimpleNamespace(type="WINDOW"),
space_data=SimpleNamespace(**space_options),
)
@staticmethod
def remove_anyimage_modules():
for module_name in tuple(sys.modules):
if module_name == "anyimage" or module_name.startswith("anyimage."):
sys.modules.pop(module_name)
class FakeImage:
def __init__(self, property_name, image_hash, packed):
self.properties = {property_name: image_hash}
self.packed_file = object() if packed else None
self.packed_files = ()
def get(self, property_name, default=None):
return self.properties.get(property_name, default)
class FakeTexture(dict):
def __init__(self, name, texture_type):
super().__init__()
self.name = name
self.type = texture_type
self.image = None
class FakeTextures(list):
def new(self, name, type):
texture = FakeTexture(name, type)
self.append(texture)
return texture
def remove(self, texture):
super().remove(texture)
if __name__ == "__main__":
unittest.main()

View File

@@ -60,10 +60,15 @@ class PackTest(unittest.TestCase):
)
self.assertIn("__init__.py", names)
self.assertIn("clipboard_image/__init__.py", names)
self.assertIn("clipboard_image/actions.py", names)
self.assertIn("clipboard_image/clipboard.py", names)
self.assertIn("blender_manifest.toml", names)
self.assertIn("operators/depth_plane/operator.py", names)
self.assertIn("operators/mesh_plane.py", names)
self.assertIn("operators/image.py", names)
self.assertIn("operators/balloon/operator.py", names)
self.assertIn("operators/image_lasso.py", names)
self.assertIn("operators/surface.py", names)
self.assertIn(
"assets/O_AnyImage.blend",
@@ -74,6 +79,7 @@ class PackTest(unittest.TestCase):
self.assertIn("server/geometry/grid_mesh.py", names)
self.assertIn("server/geometry/surface.py", names)
self.assertIn("server/jobs/background.py", names)
self.assertIn("server/jobs/image_lasso.py", names)
self.assertIn("server/models/ben2.py", names)
self.assertIn("server/models/da3.py", names)
self.assertIn("server/models/moge2.py", names)

View File

@@ -220,6 +220,7 @@ class ServerTest(unittest.TestCase):
"download-model",
"download-required-models",
"analyze-balloon",
"analyze-image-lasso",
"remove-background",
"extract-frames",
"generate-depth",
@@ -400,6 +401,46 @@ class ServerTest(unittest.TestCase):
self.assertEqual(tuple(result[0, 0]), (20, 40, 60, 64))
self.assertEqual(tuple(result[3, 3]), (0, 0, 0, 0))
def test_image_lasso_canvas_clears_rgba_outside_the_selection(self):
import numpy as np
from PIL import Image
image_lasso = sys.modules["server.jobs.image_lasso"]
source_path = self.storage_root / "source.png"
pixels = np.full((2, 2, 4), (20, 40, 60, 255), dtype=np.uint8)
Image.fromarray(pixels, mode="RGBA").save(source_path)
canvas, _selection, bounds = image_lasso.create_lasso_canvas(
source_path,
((-1, -1), (2, -1), (-1, 2)),
)
result = np.asarray(canvas)
self.assertEqual(bounds, (-1, -1, 2, 2))
self.assertEqual(result.shape, (3, 3, 4))
self.assertEqual(tuple(result[2, 2]), (0, 0, 0, 0))
self.assertEqual(tuple(result[1, 1]), (20, 40, 60, 255))
def test_image_lasso_crops_to_alpha_with_pixel_padding(self):
import numpy as np
from PIL import Image
image_lasso = sys.modules["server.jobs.image_lasso"]
pixels = np.zeros((4, 5, 4), dtype=np.uint8)
pixels[1:3, 2:4] = (20, 40, 60, 255)
result, bounds = image_lasso.crop_to_alpha(
Image.fromarray(pixels, mode="RGBA"),
(10, 20, 15, 24),
padding=1,
)
result = np.asarray(result)
self.assertEqual(bounds, (11, 20, 15, 24))
self.assertEqual(result.shape, (4, 4, 4))
self.assertTrue(np.all(result[[0, -1], :, 3] == 0))
self.assertTrue(np.all(result[:, [0, -1], 3] == 0))
self.assertTrue(np.all(result[1:3, 1:3, 3] == 255))
def test_balloon_job_sends_the_masked_crop_to_ben2(self):
import numpy as np
from PIL import Image

View File

@@ -506,6 +506,9 @@ class WorkerTest(unittest.TestCase):
def putalpha(self, _alpha):
return None
def paste(self, *_args):
return None
def save(self, path, **options):
save_options.append(options)
Path(path).touch()
@@ -540,7 +543,13 @@ class WorkerTest(unittest.TestCase):
inference_options.append((image, {})) or FakeImage()
)
fake_pil = ModuleType("PIL")
fake_pil.Image = SimpleNamespace(open=lambda _path: FakeImage())
fake_pil.Image = SimpleNamespace(
open=lambda _path: FakeImage(),
new=lambda *_args, **_kwargs: FakeImage(),
)
fake_pil.ImageChops = SimpleNamespace(
multiply=lambda _source, _foreground: FakeImage()
)
with tempfile.TemporaryDirectory() as directory, patch.dict(
sys.modules,
@@ -571,6 +580,42 @@ class WorkerTest(unittest.TestCase):
)
self.assertIn("Background removed in", context.status["message"])
def test_ben2_preserves_input_alpha_and_hides_transparent_rgb(self):
from PIL import Image
onnx_ben2 = load_server_module("models.onnx_ben2")
source_pixels = np.asarray(
(((255, 0, 0, 0), (0, 255, 0, 128)),),
dtype=np.uint8,
)
model_result = Image.new("RGBA", (2, 1), (10, 20, 30, 128))
received = []
def infer(_model, image):
received.append(np.asarray(image.convert("RGB"), dtype=np.uint8))
return model_result
with tempfile.TemporaryDirectory() as directory:
input_path = Path(directory) / "input.png"
Image.fromarray(source_pixels, mode="RGBA").save(input_path)
with (
patch.object(onnx_ben2, "create_session", return_value=object()),
patch.object(onnx_ben2, "infer", side_effect=infer),
):
result, _load_ms, _inference_ms = self.ben2.infer_foreground(
input_path,
"model",
"cpu",
preserve_input_alpha=True,
)
inference_pixels = received[0]
result_pixels = np.asarray(result.convert("RGBA"), dtype=np.uint8)
self.assertEqual(tuple(inference_pixels[0, 0]), (0, 0, 0))
self.assertEqual(tuple(inference_pixels[0, 1]), (0, 128, 0))
self.assertEqual(tuple(result_pixels[0, 0]), (255, 0, 0, 0))
self.assertEqual(tuple(result_pixels[0, 1]), (0, 255, 0, 64))
def test_ben2_background_removal_processes_each_frame(self):
inference_options = []
@@ -593,6 +638,9 @@ class WorkerTest(unittest.TestCase):
def putalpha(self, _alpha):
return None
def paste(self, *_args):
return None
def save(self, path, **options):
Path(path).touch()
@@ -624,7 +672,13 @@ class WorkerTest(unittest.TestCase):
inference_options.append((image, {})) or FakeImage()
)
fake_pil = ModuleType("PIL")
fake_pil.Image = SimpleNamespace(open=lambda _path: FakeImage())
fake_pil.Image = SimpleNamespace(
open=lambda _path: FakeImage(),
new=lambda *_args, **_kwargs: FakeImage(),
)
fake_pil.ImageChops = SimpleNamespace(
multiply=lambda _source, _foreground: FakeImage()
)
with tempfile.TemporaryDirectory() as directory, patch.dict(
sys.modules,