pyPhotoAlbum/tests/test_rotation_serialization.py
Duncan Tourolle f6ed11b0bc
All checks were successful
Python CI / test (push) Successful in 1m20s
Lint / lint (push) Successful in 1m4s
Tests / test (3.11) (push) Successful in 1m27s
Tests / test (3.12) (push) Successful in 2m25s
Tests / test (3.13) (push) Successful in 2m52s
Tests / test (3.14) (push) Successful in 1m9s
black formatting
2025-11-27 23:07:16 +01:00

190 lines
7.5 KiB
Python
Executable File

"""
Unit tests for photo rotation serialization/deserialization
Tests that rotated photos render correctly after reload
"""
import pytest
import tempfile
import json
from pathlib import Path
from unittest.mock import MagicMock, patch
from PIL import Image
from pyPhotoAlbum.models import ImageData
class TestRotationSerialization:
"""Tests for rotation serialization and deserialization"""
@pytest.fixture
def sample_image(self):
"""Create a sample test image"""
# Create a 400x200 test image (wider than tall)
img = Image.new("RGBA", (400, 200), color=(255, 0, 0, 255))
return img
def test_serialize_rotation_metadata(self):
"""Test that rotation metadata is serialized correctly"""
img_data = ImageData(image_path="test.jpg", x=10, y=20, width=100, height=50)
img_data.pil_rotation_90 = 1 # 90 degree rotation
img_data.image_dimensions = (400, 200) # Original dimensions
# Serialize
data = img_data.serialize()
# Verify rotation is saved
assert data["pil_rotation_90"] == 1
assert data["image_dimensions"] == (400, 200)
assert data["rotation"] == 0 # Visual rotation should be 0
def test_deserialize_rotation_metadata(self):
"""Test that rotation metadata is deserialized correctly"""
data = {
"type": "image",
"position": (10, 20),
"size": (100, 50),
"rotation": 0,
"z_index": 0,
"image_path": "test.jpg",
"crop_info": (0, 0, 1, 1),
"pil_rotation_90": 1,
"image_dimensions": (400, 200),
}
img_data = ImageData()
img_data.deserialize(data)
# Verify rotation is loaded
assert img_data.pil_rotation_90 == 1
assert img_data.image_dimensions == (400, 200)
assert img_data.rotation == 0
def test_image_dimensions_updated_after_rotation(self, sample_image):
"""Test that image_dimensions are updated after rotation is applied"""
# Create ImageData with original dimensions
img_data = ImageData(image_path="test.jpg", x=10, y=20, width=100, height=50)
img_data.pil_rotation_90 = 1 # 90 degree rotation
img_data.image_dimensions = (400, 200) # Original dimensions (width=400, height=200)
# Simulate async image loading with rotation
# Pass the UNROTATED image - _on_async_image_loaded will apply rotation
# After 90° rotation, a 400x200 image becomes 200x400
img_data._on_async_image_loaded(sample_image)
# Verify dimensions are updated to rotated dimensions
assert img_data.image_dimensions == (
200,
400,
), f"Expected rotated dimensions (200, 400), got {img_data.image_dimensions}"
assert img_data._img_width == 200
assert img_data._img_height == 400
def test_image_dimensions_updated_after_180_rotation(self, sample_image):
"""Test that image_dimensions are updated after 180° rotation"""
img_data = ImageData(image_path="test.jpg", x=10, y=20, width=100, height=50)
img_data.pil_rotation_90 = 2 # 180 degree rotation
img_data.image_dimensions = (400, 200) # Original dimensions
# Pass the UNROTATED image - _on_async_image_loaded will apply rotation
# After 180° rotation, dimensions should stay the same
img_data._on_async_image_loaded(sample_image)
# Verify dimensions are updated (same as original for 180°)
assert img_data.image_dimensions == (400, 200)
assert img_data._img_width == 400
assert img_data._img_height == 200
def test_image_dimensions_updated_after_270_rotation(self, sample_image):
"""Test that image_dimensions are updated after 270° rotation"""
img_data = ImageData(image_path="test.jpg", x=10, y=20, width=100, height=50)
img_data.pil_rotation_90 = 3 # 270 degree rotation
img_data.image_dimensions = (400, 200) # Original dimensions
# Pass the UNROTATED image - _on_async_image_loaded will apply rotation
# After 270° rotation, a 400x200 image becomes 200x400
img_data._on_async_image_loaded(sample_image)
# Verify dimensions are updated to rotated dimensions
assert img_data.image_dimensions == (200, 400)
assert img_data._img_width == 200
assert img_data._img_height == 400
def test_serialize_deserialize_roundtrip_with_rotation(self):
"""Test that rotation survives serialize/deserialize roundtrip"""
# Create ImageData with rotation
original = ImageData(image_path="test.jpg", x=10, y=20, width=100, height=50)
original.pil_rotation_90 = 1
original.image_dimensions = (400, 200)
original.rotation = 0
original.z_index = 5
# Serialize
data = original.serialize()
# Deserialize into new object
restored = ImageData()
restored.deserialize(data)
# Verify all fields match
assert restored.pil_rotation_90 == original.pil_rotation_90
assert restored.image_dimensions == original.image_dimensions
assert restored.rotation == original.rotation
assert restored.position == original.position
assert restored.size == original.size
assert restored.z_index == original.z_index
assert restored.image_path == original.image_path
def test_backward_compatibility_visual_rotation_conversion(self):
"""Test that old visual rotations are converted to pil_rotation_90"""
# Old format data with visual rotation
data = {
"type": "image",
"position": (10, 20),
"size": (100, 50),
"rotation": 90, # Old visual rotation
"z_index": 0,
"image_path": "test.jpg",
"crop_info": (0, 0, 1, 1),
"pil_rotation_90": 0, # Not set in old format
"image_dimensions": (400, 200),
}
img_data = ImageData()
img_data.deserialize(data)
# Should convert to pil_rotation_90
assert img_data.pil_rotation_90 == 1
assert img_data.rotation == 0 # Visual rotation reset
def test_dimensions_not_lost_on_reload(self, sample_image):
"""Integration test: dimensions are preserved through save/load cycle"""
# Step 1: Create and "rotate" an image
img1 = ImageData(image_path="test.jpg", x=10, y=20, width=100, height=50)
img1.pil_rotation_90 = 1
img1.image_dimensions = (400, 200)
# Simulate first load and rotation - pass UNROTATED image
img1._on_async_image_loaded(sample_image)
# Verify dimensions after rotation
assert img1.image_dimensions == (200, 400)
# Step 2: Serialize (like saving the project)
saved_data = img1.serialize()
# Step 3: Deserialize (like loading the project)
img2 = ImageData()
img2.deserialize(saved_data)
# Verify rotation metadata is preserved
assert img2.pil_rotation_90 == 1
# Note: image_dimensions from save will be the rotated dimensions
assert img2.image_dimensions == (200, 400)
# Step 4: Simulate reload (async loading happens again) - pass UNROTATED image
img2._on_async_image_loaded(sample_image)
# Verify dimensions are STILL correct after reload
assert img2.image_dimensions == (200, 400), "Dimensions should remain correct after reload"
assert img2._img_width == 200
assert img2._img_height == 400