""" 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