import importlib.util import sys import tempfile import unittest from pathlib import Path from types import ModuleType, SimpleNamespace from unittest.mock import patch PROJECT_ROOT = Path(__file__).resolve().parents[1] def load_image_modules(): fake_bpy = ModuleType("bpy") fake_bpy.props = SimpleNamespace( BoolProperty=lambda **options: options, EnumProperty=lambda **options: options, FloatProperty=lambda **options: options, IntProperty=lambda **options: options, StringProperty=lambda **options: options, ) fake_bpy.types = SimpleNamespace(Menu=object, Operator=object) fake_bpy.path = SimpleNamespace(abspath=lambda path: path) fake_bpy.context = SimpleNamespace(scene=None) module_paths = ( ( "anyimage.operators.common.image_data", PROJECT_ROOT / "src" / "anyimage" / "operators" / "common" / "image_data.py", ), ( "anyimage.operators.common.mesh_result", PROJECT_ROOT / "src" / "anyimage" / "operators" / "common" / "mesh_result.py", ), ) packages = { name: ModuleType(name) for name in ( "anyimage", "anyimage.operators", "anyimage.operators.common", "anyimage.operators.convert_to_plane", ) } for package in packages.values(): package.__path__ = [] loaded = [] with patch.dict(sys.modules, {"bpy": fake_bpy, **packages}): for module_name, module_path in module_paths: specification = importlib.util.spec_from_file_location( module_name, module_path, ) module = importlib.util.module_from_spec(specification) sys.modules[module_name] = module specification.loader.exec_module(module) loaded.append(module) namespace = SimpleNamespace() for module in loaded: for name, value in vars(module).items(): if not name.startswith("__"): setattr(namespace, name, value) return namespace class ImageTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.image_depth = load_image_modules() def test_material_asset_path_survives_package_split(self): self.assertEqual( self.image_depth.node_asset_path(), PROJECT_ROOT / "src" / "anyimage" / "assets" / "O_AnyImage.blend", ) def test_material_color_space_matches_scene_view_transform(self): mappings = { "ACES 1.3": "ACES 1.3 sRGB", "ACES 2.0": "ACES 2.0 sRGB", "AgX": "AgX Base sRGB", "Filmic": "Filmic sRGB", "Khronos PBR Neutral": "Khronos PBR Neutral sRGB", "Standard": "sRGB", } for view_transform, expected in mappings.items(): with self.subTest(view_transform=view_transform): scene = SimpleNamespace( view_settings=SimpleNamespace( view_transform=view_transform, ) ) self.assertEqual( self.image_depth.material_color_space_name(scene), expected, ) def test_material_color_space_falls_back_when_unsupported(self): class ColorSpaceSettings: def __init__(self): self._name = "sRGB" @property def name(self): return self._name @name.setter def name(self, value): if value != "sRGB": raise TypeError("unsupported color space") self._name = value image = SimpleNamespace(colorspace_settings=ColorSpaceSettings()) scene = SimpleNamespace( view_settings=SimpleNamespace(view_transform="ACES 2.0") ) result = self.image_depth.set_material_color_space(image, scene) self.assertEqual(result, "sRGB") self.assertEqual(image.colorspace_settings.name, "sRGB") def test_is_animated_image_accepts_movie_and_sequence(self): self.assertTrue( self.image_depth.is_animated_image(SimpleNamespace(source="MOVIE")) ) self.assertTrue( self.image_depth.is_animated_image(SimpleNamespace(source="SEQUENCE")) ) self.assertFalse( self.image_depth.is_animated_image(SimpleNamespace(source="FILE")) ) def test_video_total_frames_reads_movie_duration(self): image = SimpleNamespace(source="MOVIE", frame_duration=97) self.assertEqual(self.image_depth.video_total_frames(image), 97) def test_video_total_frames_counts_sequence_files(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "foreground-0001.png").touch() (root / "foreground-0002.png").touch() image = SimpleNamespace( source="SEQUENCE", filepath=str(root / "foreground-0001.png"), ) self.assertEqual(self.image_depth.video_total_frames(image), 2) def test_video_total_frames_returns_one_for_still_image(self): image = SimpleNamespace(source="FILE", frame_duration=1) self.assertEqual(self.image_depth.video_total_frames(image), 1) def test_default_extract_frames_uses_empty_image_frames(self): source_object = SimpleNamespace( data=SimpleNamespace(source="MOVIE", frame_duration=97), image_user=SimpleNamespace(frame_duration=24), ) self.assertEqual(self.image_depth.default_extract_frames(source_object), 24) def test_default_extract_frames_caps_at_video_total(self): source_object = SimpleNamespace( data=SimpleNamespace(source="MOVIE", frame_duration=97), image_user=SimpleNamespace(frame_duration=200), ) self.assertEqual(self.image_depth.default_extract_frames(source_object), 97) def test_default_extract_frames_falls_back_to_video_total(self): source_object = SimpleNamespace( data=SimpleNamespace(source="MOVIE", frame_duration=97), ) self.assertEqual(self.image_depth.default_extract_frames(source_object), 97) def test_extract_frame_range_honors_offset_within_clip(self): source_object = SimpleNamespace( data=SimpleNamespace(source="MOVIE", frame_duration=97), image_user=SimpleNamespace( frame_start=4, frame_offset=70, frame_duration=24, ), ) self.assertEqual( self.image_depth.extract_frame_range(source_object), (71, 24), ) def test_extract_frame_range_clamps_large_offset_to_clip_tail(self): source_object = SimpleNamespace( data=SimpleNamespace(source="MOVIE", frame_duration=97), image_user=SimpleNamespace( frame_start=1, frame_offset=90, frame_duration=24, ), ) self.assertEqual( self.image_depth.extract_frame_range(source_object), (74, 24), ) def test_extract_frame_range_clamps_negative_offset_to_start(self): source_object = SimpleNamespace( data=SimpleNamespace(source="MOVIE", frame_duration=97), image_user=SimpleNamespace( frame_start=1, frame_offset=-5, frame_duration=24, ), ) self.assertEqual( self.image_depth.extract_frame_range(source_object), (1, 24), ) def test_extract_frame_range_returns_full_clip_for_still(self): source_object = SimpleNamespace( data=SimpleNamespace(source="FILE", frame_duration=1), image_user=SimpleNamespace( frame_start=1, frame_offset=50, frame_duration=24, ), ) self.assertEqual( self.image_depth.extract_frame_range(source_object), (1, 1), ) def test_extract_frame_range_honors_single_frame_setting(self): source_object = SimpleNamespace( data=SimpleNamespace(source="MOVIE", frame_duration=97), image_user=SimpleNamespace( frame_start=1, frame_offset=70, frame_duration=1, ), ) self.assertEqual( self.image_depth.extract_frame_range(source_object), (71, 1), ) def test_configure_image_user_preserves_cyclic_setting(self): image_user = SimpleNamespace( frame_start=0, frame_offset=5, use_auto_refresh=False, use_cyclic=False, frame_duration=1, ) self.image_depth.configure_image_user( image_user, 97, frame_start=4, frame_offset=70, ) self.assertFalse(image_user.use_cyclic) self.assertTrue(image_user.use_auto_refresh) self.assertEqual(image_user.frame_start, 4) self.assertEqual(image_user.frame_offset, 70) self.assertEqual(image_user.frame_duration, 97) self.image_depth.configure_image_user( image_user, use_cyclic=True, ) self.assertTrue(image_user.use_cyclic) def test_image_user_settings_reads_empty_values(self): source_object = SimpleNamespace( image_user=SimpleNamespace( frame_start=4, frame_offset=70, frame_duration=12, ) ) self.assertEqual( self.image_depth.image_user_settings(source_object), (4, 70, 12, False), ) source_object.image_user.use_cyclic = True self.assertEqual( self.image_depth.image_user_settings(source_object), (4, 70, 12, True), ) def test_image_user_settings_defaults_without_user(self): self.assertEqual( self.image_depth.image_user_settings(SimpleNamespace(image_user=None)), (1, 0, 0, False), ) def test_image_base_name_strips_video_extension(self): image = SimpleNamespace(name="微信视频2026-07-23_192756_414.mp4") self.assertEqual( self.image_depth.image_base_name(image), "微信视频2026-07-23_192756_414", ) def test_color_name_strips_video_extension(self): source_object = SimpleNamespace(data=SimpleNamespace(name="clip.mp4")) self.assertEqual( self.image_depth.color_data_name(source_object), "clip_color.png", ) def test_force_sequence_source_keeps_loaded_sequence(self): image = SimpleNamespace(source="SEQUENCE") result = self.image_depth.force_sequence_source(image) self.assertIs(result, image) self.assertEqual(image.source, "SEQUENCE") def test_force_sequence_source_switches_single_image(self): image = SimpleNamespace(source="FILE") result = self.image_depth.force_sequence_source(image) self.assertIs(result, image) self.assertEqual(image.source, "SEQUENCE") def test_force_sequence_source_reports_unloadable_sequence(self): class BlockedImage: @property def source(self): return "FILE" @source.setter def source(self, value): raise RuntimeError("blocked") with self.assertRaisesRegex(RuntimeError, "cannot load"): self.image_depth.force_sequence_source(BlockedImage()) def test_collect_sequence_frames_returns_numbered_files_in_order(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "frame-0001.jpg").touch() (root / "frame-0002.jpg").touch() (root / "frame-0003.jpg").touch() (root / "notes.txt").touch() image = SimpleNamespace( source="SEQUENCE", filepath=str(root / "frame-0002.jpg"), ) frames = self.image_depth.collect_sequence_frames(image) self.assertEqual( [path.name for path in frames], ["frame-0001.jpg", "frame-0002.jpg", "frame-0003.jpg"], ) def test_sorted_frame_paths_uses_numeric_frame_order(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "frame-2.jpg").touch() (root / "frame-10.jpg").touch() (root / "frame-1.jpg").touch() frames = self.image_depth.sorted_frame_paths(root) self.assertEqual( [path.name for path in frames], ["frame-1.jpg", "frame-2.jpg", "frame-10.jpg"], ) def test_depth_data_uses_source_image_name(self): source_object = SimpleNamespace( name="obj_img", data=SimpleNamespace(name="img.png"), ) self.assertEqual( self.image_depth.depth_data_name(source_object), "img_depth.exr", ) source_object.data.name = "img.png.001" self.assertEqual( self.image_depth.depth_data_name(source_object), "img.001_depth.exr", ) def test_moge_normal_name_uses_source_image_name(self): source_object = SimpleNamespace( name="obj_img", data=SimpleNamespace(name="img.png"), ) self.assertEqual( self.image_depth.normal_data_name(source_object), "img_normal.png", ) def test_color_name_uses_source_image_base_name(self): source_object = SimpleNamespace( data=SimpleNamespace(name="img.png"), ) self.assertEqual( self.image_depth.color_data_name(source_object), "img_color.png", ) source_object.data.name = "img.png.001" self.assertEqual( self.image_depth.color_data_name(source_object), "img.001_color.png", ) def test_surface_displacement_is_set_to_displacement_only(self): class Material: surface_displacement_method = "BUMP" material = Material() self.image_depth.set_displacement_only(material) self.assertEqual( material.surface_displacement_method, "DISPLACEMENT", ) def test_image_empty_bounds_keep_image_aspect_and_empty_offset(self): source = SimpleNamespace( data=SimpleNamespace(size=(800, 400)), empty_display_size=2.0, empty_image_offset=(-0.5, -0.5), ) bounds = self.image_depth.image_empty_bounds(source) self.assertEqual(bounds, (-1.0, 1.0, -0.5, 0.5)) if __name__ == "__main__": unittest.main()