Files
AnyImage/tests/anyimage/operators/test_conversion_source_images.py
nan 198fcbf55b
Some checks failed
Test / test (push) Has been cancelled
Initial public release
2026-09-19 21:31:36 +08:00

191 lines
8.9 KiB
Python

"""Verify conversion ownership, source recovery, and persistent color data."""
from types import SimpleNamespace
import bpy
import numpy as np
import pytest
from PIL import Image
from anyimage.common import image as images, material
from anyimage.common.color_image import prepare_material_color_input, cleanup_material_color_input
from anyimage.operators.convert_to_plane import operators, object as conversion, image_plane
@pytest.fixture
def source():
bpy.ops.wm.read_factory_settings(use_empty=True)
image = bpy.data.images.new("Source.png", width=2, height=1, alpha=True)
image.pixels.foreach_set([0.2, 0.4, 0.8, 0.25, 0.6, 0.3, 0.1, 0])
obj = bpy.data.objects.new("Source", None)
obj.empty_display_type = "IMAGE"
obj.data = image
bpy.context.collection.objects.link(obj)
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
yield obj
bpy.ops.wm.read_factory_settings(use_empty=True)
def share_image(source, sharing):
if sharing in {"visible", "hidden", "other_scene", "same_path"}:
other = source.copy()
if sharing == "same_path":
source.data.filepath_raw = "//source.png"
other.data = source.data.copy()
collection = bpy.context.collection
if sharing == "other_scene":
collection = bpy.data.scenes.new("Other scene").collection
collection.objects.link(other)
other.hide_viewport = sharing == "hidden"
elif sharing == "material":
existing = bpy.data.materials.new("Existing")
existing.use_nodes = True
existing.node_tree.nodes.new("ShaderNodeTexImage").image = source.data
def run_conversion(source, entry, monkeypatch):
if entry == "PLANE":
operators.ConvertToPlane.execute(SimpleNamespace(mesh_detail=0), bpy.context)
else:
metadata = {"image_size": (2, 1),
"intrinsics": ((2, 0, 1), (0, 2, 0.5), (0, 0, 1))}
monkeypatch.setattr(conversion, "load_depth_metadata", lambda _: metadata)
def load_depth(*_):
image = bpy.data.images.new("Depth", width=2, height=1, float_buffer=True)
image.colorspace_settings.name = "Non-Color"
image.pixels.foreach_set(np.ones(8, dtype=np.float32))
return image
monkeypatch.setattr(conversion, "load_depth_result_image", load_depth)
monkeypatch.setattr(conversion, "load_normal_result_image", load_depth)
monkeypatch.setattr(conversion, "bpy", SimpleNamespace(data=bpy.data,
ops=SimpleNamespace(ed=SimpleNamespace(undo_push=lambda **_: {"FINISHED"}))))
path = prepare_material_color_input(source.data)
try:
conversion.create_depth_plane_from_result(bpy.context, SimpleNamespace(file=lambda key: key),
SimpleNamespace(source_object_name=source.name, source_identity=str(source.as_pointer()),
image_identity=str(source.data.as_pointer()), color_path=str(path), plane_type=entry, mesh_detail=0))
finally:
cleanup_material_color_input(path)
return bpy.context.object
@pytest.mark.parametrize("entry,sharing", [
("PLANE", "none"),
("PLANE", "material"),
("DEPTH", "visible"),
])
def test_conversion_selects_source_by_empty_identity(source, monkeypatch, entry, sharing):
share_image(source, sharing)
original = source.data
before = images.image_pixels(original)
name, path = original.name, original.filepath_raw
monkeypatch.setattr(material, "configured_material_view_adaptation", lambda: False)
obj = run_conversion(source, entry, monkeypatch)
color = next(n.image for n in obj.data.materials[0].node_tree.nodes if n.type == "TEX_IMAGE")
assert color != original
assert color.name == "Source_color.png"
assert color.filepath_raw != original.filepath_raw
assert original.name == name and original.filepath_raw == path
assert color.colorspace_settings.name == "sRGB" and color.alpha_mode == "PREMUL"
assert original.alpha_mode == "STRAIGHT"
np.testing.assert_array_equal(images.image_pixels(original), before)
np.testing.assert_allclose(images.image_pixels(color), before, atol=1 / 255)
if sharing == "material":
existing = bpy.data.materials["Existing"]
assert next(n.image for n in existing.node_tree.nodes if n.type == "TEX_IMAGE") == original
@pytest.mark.parametrize("shared,failure,entry", [
(False, "material", "PLANE"),
(True, "modifier", "DEPTH"),
(False, "finalize", "PLANE"),
])
def test_conversion_failure_restores_image_and_releases_resources(source, monkeypatch, shared, failure, entry):
if shared:
share_image(source, "visible")
source.data.pack()
source.data.pixels[0] = 0.75
original = source.data
state = images.image_content_state(original)
before = {name: set(getattr(bpy.data, name)) for name in ("images", "objects", "meshes", "materials")}
def fail(*args, **kwargs):
raise RuntimeError("Conversion failed")
monkeypatch.setattr(material, "configured_material_view_adaptation", lambda: True)
if failure == "material":
monkeypatch.setattr(material, "material_node_group", fail)
elif failure == "finalize":
monkeypatch.setattr(conversion, "finalize_object_result", fail)
elif entry == "PLANE":
monkeypatch.setattr(image_plane, "add_image_plane_processing", fail)
else:
monkeypatch.setattr(conversion, "set_modifier_input", fail)
with pytest.raises(RuntimeError, match="Conversion failed"):
run_conversion(source, entry, monkeypatch)
for name, values in before.items():
assert set(getattr(bpy.data, name)) == values
assert source.data == original
assert (original.colorspace_settings.name, original.alpha_mode) == (state["colorspace"], state["alpha_mode"])
assert bytes(original.packed_file.data) == state["packed"]
np.testing.assert_array_equal(original.pixels[:], state["pixels"])
@pytest.mark.parametrize("shared,kind", [(False, "generated"), (True, "file")])
def test_conversion_pixels_survive_library_reload(source, tmp_path, monkeypatch, kind, shared):
if kind in {"file", "dirty"}:
path = tmp_path / "source.png"
Image.fromarray(np.array([[[51, 102, 204, 64], [153, 76, 25, 0]]], np.uint8)).save(path)
source.data = bpy.data.images.load(str(path))
if kind == "dirty":
source.data.pixels[0] = 0.8
elif kind == "packed":
source.data.pack()
source.data.reload()
if shared:
share_image(source, "visible")
expected = images.image_pixels(source.data)
monkeypatch.setattr(material, "configured_material_view_adaptation", lambda: True)
obj = run_conversion(source, "PLANE", monkeypatch)
color = next(n.image for n in obj.data.materials[0].node_tree.nodes if n.type == "TEX_IMAGE")
library = tmp_path / "converted.blend"
bpy.data.libraries.write(str(library), {color})
with bpy.data.libraries.load(str(library)) as (saved, loaded):
loaded.images = saved.images
restored = loaded.images[0]
assert (restored.colorspace_settings.name, restored.alpha_mode) == (color.colorspace_settings.name, color.alpha_mode)
np.testing.assert_allclose(images.image_pixels(restored), expected, atol=1 / 255)
@pytest.mark.parametrize("entry", ["DEPTH", "RELIEF"])
def test_depth_result_rejects_replaced_source_image(source, entry):
operator = SimpleNamespace(source_object_name=source.name, source_identity=str(source.as_pointer()),
image_identity=str(source.data.as_pointer()), plane_type=entry)
source.data = source.data.copy()
with pytest.raises(ValueError, match="source image has changed"):
conversion.create_depth_plane_from_result(bpy.context, None, operator)
@pytest.mark.parametrize("shared", [False, True])
def test_plane_undo_redo_restores_image_configuration(source, monkeypatch, shared):
if shared:
share_image(source, "visible")
original_name = source.data.name
expected = images.image_pixels(source.data)
monkeypatch.setattr(material, "configured_material_view_adaptation", lambda: False)
bpy.context.preferences.edit.use_global_undo = True
bpy.utils.register_class(operators.ConvertToPlane)
try:
bpy.ops.ed.undo_push(message="Before Plane")
assert bpy.ops.anyimage.convert_to_plane("EXEC_DEFAULT", True) == {"FINISHED"}
color_name = "Source_color.png"
assert bpy.data.images[color_name].alpha_mode == "PREMUL"
assert bpy.ops.ed.undo() == {"FINISHED"}
assert bpy.data.objects["Source"].type == "EMPTY"
assert bpy.data.images[original_name].alpha_mode == "STRAIGHT"
np.testing.assert_allclose(images.image_pixels(bpy.data.images[original_name]), expected, atol=1 / 255)
assert bpy.ops.ed.redo() == {"FINISHED"}
assert bpy.data.objects["Source"].type == "MESH"
assert bpy.data.images[color_name].alpha_mode == "PREMUL"
finally:
bpy.utils.unregister_class(operators.ConvertToPlane)