227 lines
9.8 KiB
Python
227 lines
9.8 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 import image as images
|
|
from tests.support.color_image import color_image
|
|
|
|
|
|
@pytest.mark.parametrize("floating", [False, True])
|
|
def test_owned_color_images_preserve_source_and_use_premul(floating):
|
|
original = bpy.data.images.new("Alpha source", width=2, height=2, alpha=True, float_buffer=floating)
|
|
if floating:
|
|
original.alpha_mode = "PREMUL"
|
|
rgba = np.tile(np.array([0.2, 0.4, 0.8, 0.25], dtype=np.float32), (2, 2, 1))
|
|
original.pixels.foreach_set(rgba.ravel())
|
|
source = SimpleNamespace(data=original)
|
|
expected = images.image_rgba(original)
|
|
cropped = color_image(source.data, (0, 0, 2, 2), expected)
|
|
edited = images.create_image_edit_result(original, rgba.ravel(), (2, 2))
|
|
try:
|
|
assert original.alpha_mode == ("PREMUL" if floating else "STRAIGHT")
|
|
np.testing.assert_allclose(images.image_rgba(original), expected)
|
|
assert edited.alpha_mode == original.alpha_mode
|
|
for image in (cropped,):
|
|
assert image.alpha_mode == original.alpha_mode
|
|
np.testing.assert_allclose(images.image_rgba(image), expected, atol=1 / 255)
|
|
finally:
|
|
for image in (cropped, edited, original):
|
|
bpy.data.images.remove(image)
|
|
|
|
|
|
def test_job_loader_preserves_edit_alpha_policy(tmp_path):
|
|
rgba = np.array([[[51, 102, 204, 64]]], dtype=np.uint8)
|
|
Image.fromarray(rgba).save(tmp_path / "foreground.png")
|
|
original = bpy.data.images.new("Job source", width=1, height=1, alpha=True)
|
|
result = images.load_image_edit_result(original, tmp_path / "foreground.png")
|
|
try:
|
|
assert result.alpha_mode == "STRAIGHT"
|
|
assert result.packed_file is not None
|
|
assert original.alpha_mode == "STRAIGHT"
|
|
finally:
|
|
bpy.data.images.remove(result)
|
|
bpy.data.images.remove(original)
|
|
|
|
|
|
def test_business_pixels_ignore_premul_read_interpretation():
|
|
original = bpy.data.images.new("Premul read source", width=1, height=1, alpha=True)
|
|
original.alpha_mode = "PREMUL"
|
|
expected = np.array([0.4, 0.25, 0.1, 0.5], dtype=np.float32)
|
|
result = images.create_image_edit_result(original, expected, (1, 1))
|
|
try:
|
|
result.reload()
|
|
interpreted = np.empty(4, dtype=np.float32)
|
|
result.pixels.foreach_get(interpreted)
|
|
assert interpreted[0] > expected[0]
|
|
np.testing.assert_allclose(images.image_pixels(result), expected, atol=1 / 255)
|
|
assert result.alpha_mode == "PREMUL"
|
|
finally:
|
|
bpy.data.images.remove(result)
|
|
bpy.data.images.remove(original)
|
|
|
|
|
|
@pytest.mark.parametrize("dirty", [False, True])
|
|
def test_business_read_preserves_shared_packed_source(dirty):
|
|
original = bpy.data.images.new("Read integrity", width=1, height=1, alpha=True)
|
|
original.alpha_mode = "PREMUL"
|
|
result = images.create_image_edit_result(original, np.array([0.2, 0.4, 0.8, 0.25], dtype=np.float32), (1, 1))
|
|
owner = bpy.data.objects.new("Read owner", None)
|
|
owner.empty_display_type = "IMAGE"
|
|
owner.data = result
|
|
try:
|
|
result.reload()
|
|
if dirty:
|
|
result.pixels.foreach_set([0.9, 0.1, 0.3, 0.5])
|
|
result.update()
|
|
before = np.array(result.pixels[:])
|
|
packed = bytes(result.packed_file.data)
|
|
state = (result.alpha_mode, result.is_dirty, result.users)
|
|
count = len(bpy.data.images)
|
|
for _ in range(3):
|
|
pixels = images.image_pixels(result)
|
|
np.testing.assert_allclose(pixels, before if dirty else [0.2, 0.4, 0.8, 0.25], atol=1 / 255)
|
|
np.testing.assert_array_equal(result.pixels[:], before)
|
|
assert (result.alpha_mode, result.is_dirty, result.users) == state
|
|
assert bytes(result.packed_file.data) == packed
|
|
assert owner.data == result
|
|
assert len(bpy.data.images) == count
|
|
finally:
|
|
bpy.data.objects.remove(owner)
|
|
bpy.data.images.remove(result)
|
|
bpy.data.images.remove(original)
|
|
|
|
|
|
@pytest.mark.parametrize("floating", [False, True])
|
|
def test_generated_premul_reads_current_pixels(floating):
|
|
source = bpy.data.images.new("Generated read", width=1, height=1, alpha=True, float_buffer=floating)
|
|
try:
|
|
source.alpha_mode = "PREMUL"
|
|
source.pixels.foreach_set([0.2, 0.4, 0.8, 0.25])
|
|
before = np.array(source.pixels[:])
|
|
np.testing.assert_array_equal(images.image_pixels(source), before)
|
|
np.testing.assert_array_equal(source.pixels[:], before)
|
|
assert source.alpha_mode == "PREMUL"
|
|
finally:
|
|
bpy.data.images.remove(source)
|
|
|
|
|
|
@pytest.mark.parametrize("packed,floating,dirty", [
|
|
(False, False, False),
|
|
(True, False, True),
|
|
(False, True, True),
|
|
(True, True, False),
|
|
])
|
|
def test_file_read_preserves_byte_and_native_float_cache(tmp_path, packed, floating, dirty):
|
|
path = tmp_path / ("source.exr" if floating else "source.png")
|
|
if floating:
|
|
generated = bpy.data.images.new("EXR source", width=1, height=1, alpha=True, float_buffer=True)
|
|
try:
|
|
generated.pixels.foreach_set([0.2, 0.4, 0.8, 0.25])
|
|
generated.file_format = "OPEN_EXR"
|
|
generated.filepath_raw = str(path)
|
|
generated.save()
|
|
finally:
|
|
bpy.data.images.remove(generated)
|
|
else:
|
|
Image.fromarray(np.array([[[51, 102, 204, 64]]], dtype=np.uint8)).save(path)
|
|
source = bpy.data.images.load(str(path), check_existing=False)
|
|
try:
|
|
if packed:
|
|
source.pack()
|
|
source.alpha_mode = "PREMUL"
|
|
if dirty:
|
|
source.pixels.foreach_set([0.9, 0.1, 0.3, 0.5])
|
|
source.update()
|
|
before = np.array(source.pixels[:])
|
|
assert source.is_dirty == dirty
|
|
assert source.is_float == floating
|
|
expected = before if floating or dirty else np.array([51, 102, 204, 64]) / 255
|
|
count = len(bpy.data.images)
|
|
np.testing.assert_allclose(images.image_pixels(source), expected, atol=1e-7)
|
|
np.testing.assert_array_equal(source.pixels[:], before)
|
|
assert source.is_dirty == dirty and source.alpha_mode == "PREMUL"
|
|
assert len(bpy.data.images) == count
|
|
finally:
|
|
bpy.data.images.remove(source)
|
|
|
|
|
|
def test_isolated_decode_failure_releases_copy():
|
|
class FailedReader:
|
|
@property
|
|
def alpha_mode(self):
|
|
return "PREMUL"
|
|
|
|
@alpha_mode.setter
|
|
def alpha_mode(self, value):
|
|
raise RuntimeError("Decode failed")
|
|
|
|
reader = FailedReader()
|
|
source = SimpleNamespace(alpha_mode="PREMUL", is_dirty=False, is_float=False,
|
|
source="FILE", copy=lambda: reader)
|
|
with patch.object(images, "bpy", SimpleNamespace(data=SimpleNamespace(images=SimpleNamespace(remove=lambda value: removed.append(value))))):
|
|
removed = []
|
|
with pytest.raises(RuntimeError, match="Decode failed"):
|
|
images.image_pixels(source)
|
|
assert removed == [reader]
|
|
assert source.alpha_mode == "PREMUL"
|
|
|
|
|
|
def test_mask_pack_reopen_extend_and_repeat_frame_preserve_hidden_color(tmp_path):
|
|
from anyimage.common.selection import SelectionMask
|
|
from anyimage.operators.mask_tool import apply_alpha_mask
|
|
from anyimage.operators.frame_tool.projection import frame_source_projection
|
|
from anyimage.operators.frame_tool.compositing import composite_frame_pixels
|
|
|
|
source = bpy.data.images.new("Roundtrip source", width=2, height=2, alpha=True)
|
|
rgba = np.tile(np.array([0.2, 0.4, 0.8, 1], dtype=np.float32), (2, 2, 1))
|
|
full = SelectionMask(np.ones((2, 2)), (0, 0, 2, 2))
|
|
result = images.create_image_edit_result(source, apply_alpha_mask(rgba, full, "SUBTRACT"), (2, 2))
|
|
result.use_fake_user = True
|
|
name = result.name
|
|
bpy.data.images.remove(source)
|
|
path = str(tmp_path / "mask.blend")
|
|
bpy.ops.wm.save_as_mainfile(filepath=path)
|
|
bpy.ops.wm.open_mainfile(filepath=path)
|
|
result = bpy.data.images[name]
|
|
try:
|
|
restored = images.image_rgba(result)
|
|
np.testing.assert_allclose(restored[..., :3], rgba[..., :3], atol=1 / 255)
|
|
np.testing.assert_array_equal(restored[..., 3], 0)
|
|
added = images.create_image_edit_result(result, apply_alpha_mask(restored, full, "EXTEND"), (2, 2))
|
|
bpy.data.images.remove(result)
|
|
result = added
|
|
for _ in range(2):
|
|
source = frame_source_projection(np.eye(4), np.eye(4), np.eye(4), (-1, 1, -1, 1), (2, 2), perspective=False)
|
|
rgba_read = images.image_rgba(result)
|
|
source.update(rgba=rgba_read, pixels=images.premultiplied_rgba(images.image_pixels(result), (2, 2)),
|
|
image_size=(2, 2), active=True, name=result.name)
|
|
pixels = composite_frame_pixels((source,), ((0, 2), (2, 2), (2, 0), (0, 0)), (2, 2))
|
|
next_result = images.create_image_edit_result(result, pixels, (2, 2))
|
|
bpy.data.images.remove(result)
|
|
result = next_result
|
|
result.reload()
|
|
assert result.alpha_mode == "STRAIGHT" and result.packed_file is not None
|
|
np.testing.assert_allclose(images.image_rgba(result), rgba, atol=1 / 255)
|
|
finally:
|
|
bpy.data.images.remove(result)
|
|
|
|
|
|
def test_job_loader_reads_only_the_returned_file(tmp_path):
|
|
Image.new("RGBA", (2, 2), (255, 0, 0, 255)).save(tmp_path / "foreground.png")
|
|
Image.new("RGBA", (4, 4), (0, 0, 255, 255)).save(tmp_path / "upscale.png")
|
|
original = bpy.data.images.new("Source", width=1, height=1, alpha=True)
|
|
result = images.load_image_edit_result(original, tmp_path / "upscale.png")
|
|
try:
|
|
assert tuple(result.size) == (4, 4)
|
|
assert result.source == "FILE"
|
|
assert result.packed_file is not None
|
|
with pytest.raises(RuntimeError, match="Color result image does not exist"):
|
|
images.load_image_edit_result(original, tmp_path / "missing.png")
|
|
finally:
|
|
bpy.data.images.remove(result)
|
|
bpy.data.images.remove(original)
|