Add comprehensive tests for edge depth extension, empty image edits, and material view adaptation

- Introduced `test_edge_depth.py` to validate the functionality of edge depth extension, ensuring core alpha preservation, proper depth handling, and edge padding behavior.
- Created `test_empty_image_edits.py` to verify the behavior of image edits in Blender, focusing on shared edits, preservation of image properties, and handling of failed operations.
- Developed `test_material_view_adaptation.py` to test material adaptation preferences, ensuring color space and alpha settings are correctly applied and preserved across various scenarios.
This commit is contained in:
2026-09-06 21:43:47 +08:00
parent 1994d38ccd
commit 7e55af6722
58 changed files with 3607 additions and 200 deletions

View File

@@ -0,0 +1,236 @@
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import bpy
import numpy as np
import pytest
from PIL import Image
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from anyimage import preferences
from anyimage.common import image as images
from anyimage.common import material
from anyimage.operators.clipboard_image import actions as clipboard
from anyimage.operators.convert_to_plane.object import create_color_image
from anyimage.operators.cutout_tool.object import create_cropped_color_image
@pytest.fixture
def color_data():
original_images = set(bpy.data.images)
original_materials = set(bpy.data.materials)
original_objects = set(bpy.data.objects)
yield
for obj in set(bpy.data.objects) - original_objects:
bpy.data.objects.remove(obj, do_unlink=True)
for item in set(bpy.data.materials) - original_materials:
bpy.data.materials.remove(item, do_unlink=True)
for item in set(bpy.data.images) - original_images:
bpy.data.images.remove(item, do_unlink=True)
def view_scene(view):
return SimpleNamespace(view_settings=SimpleNamespace(view_transform=view))
def color_texture(result):
return next(node.image for node in result.node_tree.nodes if node.type == "TEX_IMAGE")
def test_adaptation_preference_defaults_and_reads_current_value():
with patch.object(preferences, "addon_preferences", return_value=None):
assert preferences.configured_material_view_adaptation()
with patch.object(preferences, "addon_preferences", return_value=SimpleNamespace()):
assert preferences.configured_material_view_adaptation()
settings = SimpleNamespace(adapt_material_to_view_transform=False)
with patch.object(preferences, "addon_preferences", return_value=settings):
assert not preferences.configured_material_view_adaptation()
settings.adapt_material_to_view_transform = True
assert preferences.configured_material_view_adaptation()
@pytest.mark.parametrize("enabled", [False, True])
@pytest.mark.parametrize("view,space", [*material.VIEW_TRANSFORM_COLOR_SPACES, ("Standard", "sRGB"), ("Unknown", "sRGB")])
def test_color_space_and_alpha_follow_preference(color_data, enabled, view, space):
image = bpy.data.images.new("Material policy", width=2, height=1, alpha=True)
expected = np.array([0.2, 0.4, 0.8, 0.25, 0.6, 0.3, 0.1, 0], dtype=np.float32)
image.pixels.foreach_set(expected)
settings = SimpleNamespace(adapt_material_to_view_transform=enabled)
# The installed bpy may not include newer view transforms such as ACES 2.0.
try:
image.colorspace_settings.name = space
except TypeError:
space = "sRGB"
image.colorspace_settings.name = "sRGB"
image.pixels.foreach_set(expected)
with patch.object(preferences, "addon_preferences", return_value=settings):
result = material.configure_material_color_image(image, view_scene(view))
selected = space if enabled else "sRGB"
assert result.colorspace_settings.name == selected
assert result.alpha_mode == ("PREMUL" if selected == "sRGB" else "STRAIGHT")
assert settings.adapt_material_to_view_transform == enabled
np.testing.assert_allclose(images.image_pixels(result), expected, atol=1 / 255)
@pytest.mark.parametrize("enabled", [False, True])
@pytest.mark.parametrize("kind", [
"byte", "packed", "file", "dirty",
pytest.param("float", marks=pytest.mark.xfail(
strict=True,
reason="Native float RGB is reinterpreted by Blender after packing and reloading with a display color space",
)),
])
def test_material_pixels_survive_pack_and_library_reload(color_data, tmp_path, enabled, kind):
expected = np.array([[[51, 102, 204, 64], [153, 76, 25, 0]]], dtype=np.uint8)
if kind in {"file", "dirty"}:
path = tmp_path / "source.png"
Image.fromarray(expected).save(path)
source = bpy.data.images.load(str(path), check_existing=False)
if kind == "dirty":
source.pixels.foreach_set((expected.astype(np.float32) / 255).ravel())
else:
source = bpy.data.images.new("Material pixels", width=2, height=1, alpha=True, float_buffer=kind == "float")
source.pixels.foreach_set((expected.astype(np.float32) / 255).ravel())
if kind == "packed":
source.pack()
source.reload()
images.configure_color_alpha(source)
before = images.image_pixels(source)
owner = bpy.data.objects.new("Color source owner", None)
owner.empty_display_type = "IMAGE"
owner.data = source
with patch.object(material, "configured_material_view_adaptation", return_value=enabled):
result = material.configure_material_color_image(source, view_scene("AgX"))
assert result != source
assert source.alpha_mode == "PREMUL"
np.testing.assert_allclose(images.image_pixels(source), before, atol=1 / 255)
np.testing.assert_allclose(images.image_pixels(result), before, atol=1 / 255)
if result.packed_file is None or result.is_dirty:
result.pack()
selected = (result.colorspace_settings.name, result.alpha_mode)
path = tmp_path / "material.blend"
bpy.data.libraries.write(str(path), {result})
with bpy.data.libraries.load(str(path)) as (available, loaded):
loaded.images = available.images
restored = loaded.images[0]
assert (restored.colorspace_settings.name, restored.alpha_mode) == selected
np.testing.assert_allclose(images.image_pixels(restored), before, atol=1 / 255)
edited = images.create_image_edit_result(restored, images.image_pixels(restored), (2, 1))
assert edited.alpha_mode == "PREMUL"
np.testing.assert_allclose(images.image_pixels(edited), before, atol=1 / 255)
@pytest.mark.parametrize("shadeless", [False, True])
@pytest.mark.parametrize("entry", ["plane", "cutout", "depth", "clipboard"])
def test_new_material_uses_current_preference_and_preserves_existing(color_data, entry, shadeless):
source = bpy.data.images.new("Shared source", width=2, height=2, alpha=True)
source.pixels.foreach_set(np.tile(np.array([0.2, 0.4, 0.8, 0.25], dtype=np.float32), 4))
owner = bpy.data.objects.new("Shared Empty", None)
owner.empty_display_type = "IMAGE"
owner.data = source
before = images.image_pixels(source)
scene = view_scene("AgX")
settings = SimpleNamespace(adapt_material_to_view_transform=True)
data_textures = {}
if entry == "depth":
for key in ("displacement_image", "normal_image"):
image = bpy.data.images.new(key, width=2, height=2, float_buffer=True)
image.colorspace_settings.name = "Non-Color"
data_textures[key] = image
data_states = {
key: (image.colorspace_settings.name, image.alpha_mode, images.image_pixels(image))
for key, image in data_textures.items()
}
with patch.object(preferences, "addon_preferences", return_value=settings):
if entry == "clipboard":
first = clipboard._create_image_material(source, shadeless, scene=scene)
else:
if entry == "plane":
color = create_color_image(owner)
elif entry == "cutout":
color = create_cropped_color_image(owner, (0, 0, 2, 2), images.image_rgba(source))
else:
color = source
first = material.create_image_material(owner, color, 0, shadeless=shadeless, scene=scene, **data_textures)
original_color = color_texture(first)
assert original_color.colorspace_settings.name == "AgX Base sRGB"
assert original_color.alpha_mode == "STRAIGHT"
settings.adapt_material_to_view_transform = False
assert original_color.alpha_mode == "STRAIGHT"
if entry == "clipboard":
second = clipboard._create_image_material(original_color, shadeless, scene=scene)
else:
second = material.create_image_material(owner, original_color, 0, shadeless=shadeless, scene=scene)
new_color = color_texture(second)
assert new_color != original_color
assert new_color.colorspace_settings.name == "sRGB"
assert new_color.alpha_mode == "PREMUL"
assert original_color.colorspace_settings.name == "AgX Base sRGB"
assert original_color.alpha_mode == "STRAIGHT"
assert not settings.adapt_material_to_view_transform
assert owner.data == source
assert source.colorspace_settings.name == "sRGB"
assert source.alpha_mode == "STRAIGHT"
for image in (source, original_color, new_color):
np.testing.assert_allclose(images.image_pixels(image), before, atol=1 / 255)
for key, image in data_textures.items():
space, alpha, pixels = data_states[key]
assert image.colorspace_settings.name == space
assert image.alpha_mode == alpha
np.testing.assert_array_equal(images.image_pixels(image), pixels)
def test_edit_results_keep_their_policy_with_adaptation_enabled(color_data, tmp_path):
source = bpy.data.images.new("Editing source", width=1, height=1, alpha=True)
rgba = np.array([0.2, 0.4, 0.8, 0.25], dtype=np.float32)
Image.fromarray(np.array([[[51, 102, 204, 64]]], dtype=np.uint8)).save(tmp_path / "image.0001.png")
with patch.object(preferences, "addon_preferences", return_value=SimpleNamespace(adapt_material_to_view_transform=True)):
edited = images.create_image_edit_result(source, rgba, (1, 1))
loaded = images.load_image_edit_result(SimpleNamespace(data=source), tmp_path)
for result in (edited, loaded):
assert result.colorspace_settings.name == "sRGB"
assert result.alpha_mode == "PREMUL"
np.testing.assert_allclose(images.image_pixels(result), rgba, atol=1 / 255)
assert source.colorspace_settings.name == "sRGB"
assert source.alpha_mode == "STRAIGHT"
@pytest.mark.parametrize("supported", [{"sRGB", "Non-Color"}, {"Non-Color"}])
def test_unavailable_color_space_falls_back_or_preserves_original(supported):
class ColorSpace:
value = "Non-Color"
@property
def name(self):
return self.value
@name.setter
def name(self, value):
if value not in supported:
raise TypeError("Unsupported color space")
self.value = value
image = SimpleNamespace(colorspace_settings=ColorSpace(), users=0, use_fake_user=False, alpha_mode="STRAIGHT")
with patch.object(material, "configured_material_view_adaptation", return_value=True):
if "sRGB" in supported:
result = material.configure_material_color_image(image, view_scene("AgX"))
assert result.colorspace_settings.name == "sRGB"
assert result.alpha_mode == "PREMUL"
else:
with pytest.raises(RuntimeError, match="material color space"):
material.configure_material_color_image(image, view_scene("AgX"))
assert image.colorspace_settings.name == "Non-Color"
assert image.alpha_mode == "STRAIGHT"
def test_missing_scene_uses_srgb(color_data):
image = bpy.data.images.new("Missing scene", width=1, height=1)
with patch.object(material, "bpy", SimpleNamespace(context=SimpleNamespace(scene=None))), patch.object(
material, "configured_material_view_adaptation", return_value=True
):
result = material.configure_material_color_image(image)
assert result.colorspace_settings.name == "sRGB"
assert result.alpha_mode == "PREMUL"