190 lines
8.2 KiB
Python
190 lines
8.2 KiB
Python
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
import bpy
|
|
import numpy as np
|
|
import pytest
|
|
from PIL import Image
|
|
|
|
from anyimage.common.hdr_image import HdrBackgroundInput, recognition_rgba, save_hdr_result
|
|
from anyimage.common.image import cleanup_image_input, image_content_state, image_rgba, restore_image_content
|
|
from anyimage.common.image_target import ImageEditTarget
|
|
from anyimage.common import image_target as targets
|
|
from tests.support.image_texture import texture, texture_context
|
|
from tests.support.hdr_image import hdr_texture
|
|
|
|
|
|
@pytest.mark.parametrize("source_kind", ["generated", "file", "packed", "dirty"])
|
|
@pytest.mark.parametrize("owner_kind", ["texture", "empty"])
|
|
@pytest.mark.parametrize("shared", [False, True])
|
|
def test_hdr_commit_keeps_float_pixels_and_transaction(hdr_texture, tmp_path, source_kind, owner_kind, shared):
|
|
texture = hdr_texture
|
|
source = texture.image
|
|
rgba = image_rgba(source).copy()
|
|
if source_kind != "generated":
|
|
path = tmp_path / "source.exr"
|
|
save_hdr_result(rgba, path)
|
|
source = bpy.data.images.load(str(path), check_existing=False)
|
|
texture.image = source
|
|
if source_kind == "packed":
|
|
source.pack()
|
|
if source_kind == "dirty":
|
|
rgba[1, 1, 0] = 19
|
|
source.pixels.foreach_set(np.flipud(rgba).ravel())
|
|
source.use_fake_user = True
|
|
if owner_kind == "empty":
|
|
owner = bpy.data.objects.new("HDR Empty", None)
|
|
owner.empty_display_type = "IMAGE"
|
|
owner.data = source
|
|
bpy.context.scene.collection.objects.link(owner)
|
|
texture.image = None
|
|
context = SimpleNamespace(object=owner, space_data=None)
|
|
else:
|
|
owner = texture
|
|
context = texture_context(texture)
|
|
if shared:
|
|
other = texture.id_data.nodes.new("ShaderNodeTexImage")
|
|
other.image = source
|
|
target = ImageEditTarget.capture(context)
|
|
snapshot = HdrBackgroundInput.prepare(source)
|
|
original_state = image_content_state(source)
|
|
alpha = np.linspace(0.03, 0.91, 12, dtype=np.float32).reshape(3, 4)
|
|
alpha_path = tmp_path / "alpha.npy"
|
|
np.save(alpha_path, alpha)
|
|
try:
|
|
with Image.open(snapshot.path) as preview:
|
|
assert preview.size == (4, 3)
|
|
assert np.asarray(preview)[0, 0, :3].max() == 0
|
|
result = snapshot.apply(target, alpha_path)
|
|
expected = rgba.copy()
|
|
expected[..., 3] *= alpha
|
|
assert result.is_float and result.packed_file and result.alpha_mode == "CHANNEL_PACKED"
|
|
np.testing.assert_allclose(image_rgba(result), expected, atol=1e-7)
|
|
assert (result == source) == (not shared)
|
|
if shared:
|
|
np.testing.assert_array_equal(image_rgba(other.image), rgba)
|
|
else:
|
|
assert result.use_fake_user
|
|
# The encoded result restores the same ID and floats after a source-state restore.
|
|
result_state = image_content_state(result)
|
|
restore_image_content(result, original_state)
|
|
np.testing.assert_allclose(image_rgba(result), rgba, atol=1e-7)
|
|
restore_image_content(result, result_state)
|
|
np.testing.assert_allclose(image_rgba(result), expected, atol=1e-7)
|
|
blend_path = tmp_path / "roundtrip.blend"
|
|
result_name = result.name
|
|
bpy.ops.wm.save_as_mainfile(filepath=str(blend_path))
|
|
bpy.ops.wm.open_mainfile(filepath=str(blend_path))
|
|
restored = bpy.data.images[result_name]
|
|
assert restored.is_float and restored.packed_file
|
|
np.testing.assert_allclose(image_rgba(restored), expected, atol=1e-7)
|
|
finally:
|
|
cleanup_image_input(snapshot.path, True)
|
|
|
|
|
|
@pytest.mark.parametrize("invalid", ["shape", "dtype", "nan", "range", "corrupt", "source", "colorspace", "binding"])
|
|
def test_hdr_rejects_invalid_result_and_source_changes(hdr_texture, tmp_path, invalid):
|
|
target = ImageEditTarget.capture(texture_context(hdr_texture))
|
|
snapshot = HdrBackgroundInput.prepare(target.image)
|
|
alpha = np.ones((3, 4), np.float32)
|
|
if invalid == "shape": alpha = alpha[:1]
|
|
if invalid == "dtype": alpha = alpha.astype(np.float64)
|
|
if invalid == "nan": alpha[0, 0] = np.nan
|
|
if invalid == "range": alpha[0, 0] = 2
|
|
path = tmp_path / "alpha.npy"
|
|
np.save(path, alpha)
|
|
if invalid == "corrupt": path.write_bytes(b"broken")
|
|
if invalid == "source": target.image.pixels[4] = 20
|
|
if invalid == "colorspace": target.image.colorspace_settings.name = "ACEScg"
|
|
if invalid == "binding": hdr_texture.image = bpy.data.images.new("Other", width=1, height=1)
|
|
before = image_rgba(hdr_texture.image).copy()
|
|
count = len(bpy.data.images)
|
|
try:
|
|
with pytest.raises(RuntimeError):
|
|
snapshot.apply(target, path)
|
|
np.testing.assert_array_equal(image_rgba(hdr_texture.image), before)
|
|
assert len(bpy.data.images) == count
|
|
finally:
|
|
cleanup_image_input(snapshot.path, True)
|
|
|
|
|
|
def test_hdr_preview_is_fixed_and_preserves_source(hdr_texture):
|
|
image = hdr_texture.image
|
|
before = image_rgba(image).copy()
|
|
first = HdrBackgroundInput.prepare(image)
|
|
bpy.context.scene.view_settings.view_transform = "Standard"
|
|
bpy.context.scene.view_settings.exposure = 7
|
|
second = HdrBackgroundInput.prepare(image)
|
|
try:
|
|
with Image.open(first.path) as a, Image.open(second.path) as b:
|
|
np.testing.assert_array_equal(np.asarray(a), np.asarray(b))
|
|
np.testing.assert_array_equal(image_rgba(image), before)
|
|
np.testing.assert_array_equal(recognition_rgba(np.zeros((1, 1, 4), np.float32)), [[[0, 0, 0, 1]]])
|
|
finally:
|
|
cleanup_image_input(first.path, True)
|
|
cleanup_image_input(second.path, True)
|
|
|
|
|
|
def test_hdr_rejects_animation_before_export(hdr_texture):
|
|
hdr_texture.image.source = "SEQUENCE"
|
|
with pytest.raises(RuntimeError, match="still images"):
|
|
HdrBackgroundInput.prepare(hdr_texture.image)
|
|
|
|
|
|
def test_hdr_export_failure_releases_blender_resources(hdr_texture, tmp_path):
|
|
images, scenes = len(bpy.data.images), len(bpy.data.scenes)
|
|
blocked = tmp_path / "file"
|
|
blocked.write_bytes(b"not a directory")
|
|
with pytest.raises(RuntimeError):
|
|
save_hdr_result(image_rgba(hdr_texture.image), blocked / "output.exr")
|
|
assert (len(bpy.data.images), len(bpy.data.scenes)) == (images, scenes)
|
|
|
|
|
|
def test_straight_exr_only_changes_alpha(hdr_texture, tmp_path):
|
|
path = tmp_path / "straight.exr"
|
|
encoded = image_rgba(hdr_texture.image).copy()
|
|
save_hdr_result(encoded, path)
|
|
source = bpy.data.images.load(str(path), check_existing=False)
|
|
source.alpha_mode = "STRAIGHT"
|
|
hdr_texture.image = source
|
|
before = image_rgba(source).copy()
|
|
np.testing.assert_allclose(before[..., :3], encoded[..., :3] * encoded[..., 3:4], atol=1e-7)
|
|
snapshot = HdrBackgroundInput.prepare(source)
|
|
alpha_path = tmp_path / "alpha.npy"
|
|
np.save(alpha_path, np.full((3, 4), 0.3, np.float32))
|
|
try:
|
|
result = snapshot.apply(ImageEditTarget.capture(texture_context(hdr_texture)), alpha_path)
|
|
rgba = image_rgba(result)
|
|
np.testing.assert_array_equal(rgba[..., :3], before[..., :3])
|
|
np.testing.assert_allclose(rgba[..., 3], before[..., 3] * 0.3, atol=1e-7)
|
|
finally:
|
|
cleanup_image_input(snapshot.path, True)
|
|
|
|
|
|
def test_hdr_failed_commit_restores_unsaved_source(hdr_texture, tmp_path):
|
|
target = ImageEditTarget.capture(texture_context(hdr_texture))
|
|
snapshot = HdrBackgroundInput.prepare(target.image)
|
|
before = image_rgba(target.image).copy()
|
|
original_source = target.image.source
|
|
alpha_path = tmp_path / "alpha.npy"
|
|
np.save(alpha_path, np.full((3, 4), 0.3, np.float32))
|
|
calls = 0
|
|
|
|
def fail_first(image, state):
|
|
nonlocal calls
|
|
calls += 1
|
|
restore_image_content(image, state)
|
|
if calls == 1:
|
|
raise RuntimeError("Commit failed")
|
|
|
|
count = len(bpy.data.images)
|
|
try:
|
|
with patch.object(targets, "restore_image_content", side_effect=fail_first):
|
|
with pytest.raises(RuntimeError, match="Commit failed"):
|
|
snapshot.apply(target, alpha_path, prepare_undo=True)
|
|
np.testing.assert_array_equal(image_rgba(target.image), before)
|
|
assert target.image.source == original_source
|
|
assert len(bpy.data.images) == count
|
|
finally:
|
|
cleanup_image_input(snapshot.path, True)
|