import tempfile import unittest from pathlib import Path from types import SimpleNamespace from tests.anyimage.common.support import load_common_modules class ImageDataTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.modules = load_common_modules() def test_is_animated_image_accepts_movie_and_sequence(self): self.assertTrue( self.modules.image.is_animated_image(SimpleNamespace(source="MOVIE")) ) self.assertTrue( self.modules.image.is_animated_image(SimpleNamespace(source="SEQUENCE")) ) self.assertFalse( self.modules.image.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.modules.image.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.modules.image.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.modules.image.video_total_frames(image), 1) 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.extract_frame_range(source_object.data, getattr(source_object, "image_user", None))[1], 24) 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.extract_frame_range(source_object.data, getattr(source_object, "image_user", None))[1], 97) 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.extract_frame_range(source_object.data, getattr(source_object, "image_user", None))[1], 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.modules.image.extract_frame_range(source_object.data, source_object.image_user), (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.modules.image.extract_frame_range(source_object.data, source_object.image_user), (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.modules.image.extract_frame_range(source_object.data, source_object.image_user), (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.modules.image.extract_frame_range(source_object.data, source_object.image_user), (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.modules.image.extract_frame_range(source_object.data, source_object.image_user), (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.modules.image.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.modules.image.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.modules.image.image_user_settings(source_object), (4, 70, 12, False), ) source_object.image_user.use_cyclic = True self.assertEqual( self.modules.image.image_user_settings(source_object), (4, 70, 12, True), ) def test_image_user_settings_defaults_without_user(self): self.assertEqual( self.modules.image.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.modules.image.image_base_name(image), "微信视频2026-07-23_192756_414", ) def test_force_sequence_source_keeps_loaded_sequence(self): image = SimpleNamespace(source="SEQUENCE") result = self.modules.image.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.modules.image.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.modules.image.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.modules.image.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.modules.image.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.modules.image.depth_data_name(source_object), "img_depth.exr", ) source_object.data.name = "img.png.001" self.assertEqual( self.modules.image.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.modules.image.normal_data_name(source_object), "img_normal.png", ) 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.modules.image.image_empty_bounds(source) self.assertEqual(bounds, (-1.0, 1.0, -0.5, 0.5))