Files
AnyImage/tests/anyimage/common/test_image_target.py
nan 938d95db0f Add new functionality for image rectification and depth metadata handling
- Implemented a preview tool for rectifying images with interactive aspect controls in `preview.py`.
- Added tests for depth metadata loading to ensure consistent schema usage in `test_depth_metadata.py`.
- Developed comprehensive tests for cutout calibration processes in `test_cutout_calibration.py`, including stability checks and depth calibration.
- Created tests for frame compositing operations in `test_frame_compositing.py`, ensuring correct handling of depth and transparency.
- Introduced material slot inheritance functionality in `material_slots.py` to manage geometry material assignments.
- Built shared shader nodes for material processing in `shader.py`, including roughness and bump mapping setups.
2026-09-12 19:20:06 +08:00

207 lines
9.0 KiB
Python

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 / "foreground.png")
assert tuple(texture.image.size) == (8, 6)
assert other.image is None
@pytest.mark.parametrize("color_space,alpha_mode,filename,shared", [
("sRGB", "PREMUL", "foreground.png", True),
("AgX Base sRGB", "STRAIGHT", "upscale.png", False),
("Filmic sRGB", "STRAIGHT", "foreground.png", False),
("sRGB", "STRAIGHT", "upscale.png", True),
("AgX Base sRGB", "PREMUL", "foreground.png", True),
])
def test_image_actions_preserve_source_color_space_and_alpha(texture, tmp_path, color_space, alpha_mode, filename, 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, filename)
target.apply(tmp_path / filename)
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", "identity"])
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"])
elif change == "identity":
# A reused RNA address must not make a replacement node the original target.
texture["anyimage_identity"] = "replacement"
else:
bpy.data.images.remove(texture.image)
count = len(bpy.data.images)
with pytest.raises(RuntimeError, match="no longer available"):
target.apply(tmp_path / "foreground.png")
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 / "foreground.png")
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("source", ["MOVIE", "SEQUENCE"])
def test_target_rejects_animated_image(texture, source):
texture.image.source = source
with pytest.raises(RuntimeError, match="Only still images"):
targets.ImageEditTarget.capture(texture_context(texture))
@pytest.mark.parametrize("shared", [False, True])
def test_failed_texture_assignment_restores_source(texture, tmp_path, shared):
original = texture.image
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)
result = images.load_image_edit_result(original, write_result(tmp_path))
count = len(bpy.data.images) - 1
class FailingTarget:
failed = False
def __getattr__(self, name):
return getattr(texture, name)
@property
def image(self):
return texture.image
@image.setter
def image(self, value):
texture.image = value
if not self.failed:
self.failed = True
raise RuntimeError("Assignment failed")
with patch.object(targets, "has_other_image_user", return_value=shared):
with pytest.raises(RuntimeError, match="Assignment failed"):
targets.replace_texture_image(FailingTarget(), 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 len(bpy.data.images) == count