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

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)