pyWebLayout/tests/concrete/test_concrete_image.py
Duncan Tourolle 65ab46556f
Some checks failed
Python CI / test (push) Failing after 3m55s
big update with ok rendering
2025-08-27 22:22:54 +02:00

384 lines
15 KiB
Python

"""
Unit tests for pyWebLayout.concrete.image module.
Tests the RenderableImage class for image loading, scaling, and rendering functionality.
"""
import unittest
import os
import tempfile
import numpy as np
from PIL import Image as PILImage, ImageDraw
from unittest.mock import Mock, patch, MagicMock
from pyWebLayout.concrete.image import RenderableImage
from pyWebLayout.abstract.block import Image as AbstractImage
from pyWebLayout.style import Alignment
class TestRenderableImage(unittest.TestCase):
"""Test cases for the RenderableImage class"""
def setUp(self):
"""Set up test fixtures"""
# Create a temporary test image
self.temp_dir = tempfile.mkdtemp()
self.test_image_path = os.path.join(self.temp_dir, "test_image.png")
# Create a simple test image
test_img = PILImage.new('RGB', (100, 80), (255, 0, 0)) # Red image
test_img.save(self.test_image_path)
# Create abstract image objects
self.abstract_image = AbstractImage(self.test_image_path, "Test Image", 100, 80)
self.abstract_image_no_dims = AbstractImage(self.test_image_path, "Test Image")
# Create a canvas and draw object for testing
self.canvas = PILImage.new('RGBA', (400, 300), (255, 255, 255, 255))
self.draw = ImageDraw.Draw(self.canvas)
def tearDown(self):
"""Clean up test fixtures"""
# Clean up temporary files
try:
os.unlink(self.test_image_path)
os.rmdir(self.temp_dir)
except:
pass
def test_renderable_image_initialization_basic(self):
"""Test basic image initialization"""
renderable = RenderableImage(self.abstract_image, self.canvas)
self.assertEqual(renderable._abstract_image, self.abstract_image)
self.assertEqual(renderable._canvas, self.canvas)
self.assertIsNotNone(renderable._pil_image)
self.assertIsNone(renderable._error_message)
self.assertEqual(renderable._halign, Alignment.CENTER)
self.assertEqual(renderable._valign, Alignment.CENTER)
def test_renderable_image_initialization_with_constraints(self):
"""Test image initialization with size constraints"""
max_width = 50
max_height = 40
renderable = RenderableImage(
self.abstract_image,
self.draw,
max_width=max_width,
max_height=max_height
)
self.assertEqual(renderable._abstract_image, self.abstract_image)
# Size should be constrained
self.assertLessEqual(renderable._size[0], max_width)
self.assertLessEqual(renderable._size[1], max_height)
def test_renderable_image_initialization_with_custom_params(self):
"""Test image initialization with custom parameters"""
custom_origin = (20, 30)
custom_size = (120, 90)
renderable = RenderableImage(
self.abstract_image,
self.draw,
origin=custom_origin,
size=custom_size,
halign=Alignment.LEFT,
valign=Alignment.TOP
)
np.testing.assert_array_equal(renderable._origin, np.array(custom_origin))
np.testing.assert_array_equal(renderable._size, np.array(custom_size))
self.assertEqual(renderable._halign, Alignment.LEFT)
self.assertEqual(renderable._valign, Alignment.TOP)
def test_load_image_local_file(self):
"""Test loading image from local file"""
renderable = RenderableImage(self.abstract_image, self.draw)
# Image should be loaded
self.assertIsNotNone(renderable._pil_image)
self.assertIsNone(renderable._error_message)
self.assertEqual(renderable._pil_image.size, (100, 80))
def test_load_image_nonexistent_file(self):
"""Test loading image from nonexistent file"""
bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
renderable = RenderableImage(bad_abstract, self.draw)
# Should have error message, no PIL image
self.assertIsNone(renderable._pil_image)
self.assertIsNotNone(renderable._error_message)
@patch('requests.get')
def test_load_image_url_success(self, mock_get):
"""Test loading image from URL (success)"""
# Create a mock response
mock_response = Mock()
mock_response.status_code = 200
mock_response.content = open(self.test_image_path, 'rb').read()
mock_get.return_value = mock_response
url_abstract = AbstractImage("https://example.com/image.png", "URL Image")
renderable = RenderableImage(url_abstract, self.draw)
# Should successfully load image
self.assertIsNotNone(renderable._pil_image)
self.assertIsNone(renderable._error_message)
@patch('requests.get')
def test_load_image_url_failure(self, mock_get):
"""Test loading image from URL (failure)"""
# Mock a failed request
mock_response = Mock()
mock_response.status_code = 404
mock_get.return_value = mock_response
url_abstract = AbstractImage("https://example.com/notfound.png", "Bad URL Image")
renderable = RenderableImage(url_abstract, self.draw)
# Should have error message
self.assertIsNone(renderable._pil_image)
self.assertIsNotNone(renderable._error_message)
def test_load_image_no_requests_library(self):
"""Test loading URL image when requests library is not available"""
# Mock the import to raise ImportError for requests
def mock_import(name, *args, **kwargs):
if name == 'requests':
raise ImportError("No module named 'requests'")
return __import__(name, *args, **kwargs)
with patch('builtins.__import__', side_effect=mock_import):
url_abstract = AbstractImage("https://example.com/image.png", "URL Image")
renderable = RenderableImage(url_abstract, self.draw)
# Should have error message about missing requests
self.assertIsNone(renderable._pil_image)
self.assertIsNotNone(renderable._error_message)
self.assertIn("Requests library not available", renderable._error_message)
def test_resize_image_fit_within_bounds(self):
"""Test image resizing to fit within bounds"""
renderable = RenderableImage(self.abstract_image, self.draw)
# Original image is 100x80, resize to fit in 50x50
renderable._size = np.array([50, 50])
resized = renderable._resize_image()
self.assertIsInstance(resized, PILImage.Image)
# Should maintain aspect ratio and fit within bounds
self.assertLessEqual(resized.width, 50)
self.assertLessEqual(resized.height, 50)
# Check aspect ratio is maintained (approximately)
original_ratio = 100 / 80
new_ratio = resized.width / resized.height
self.assertAlmostEqual(original_ratio, new_ratio, delta=0.1)
def test_resize_image_larger_target(self):
"""Test image resizing when target is larger than original"""
renderable = RenderableImage(self.abstract_image, self.draw)
# Target size larger than original
renderable._size = np.array([200, 160])
resized = renderable._resize_image()
self.assertIsInstance(resized, PILImage.Image)
# Should scale up to fill the space while maintaining aspect ratio
self.assertGreater(resized.width, 100)
self.assertGreater(resized.height, 80)
def test_resize_image_no_image(self):
"""Test resize when no image is loaded"""
bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
renderable = RenderableImage(bad_abstract, self.draw)
resized = renderable._resize_image()
# Should return a placeholder image
self.assertIsInstance(resized, PILImage.Image)
self.assertEqual(resized.mode, 'RGBA')
def test_draw_error_placeholder(self):
"""Test drawing error placeholder"""
bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
renderable = RenderableImage(bad_abstract, self.canvas)
renderable._error_message = "File not found"
# Set origin for the placeholder
renderable.set_origin(np.array([10, 20]))
# Call the error placeholder method
renderable._draw_error_placeholder()
# We can't easily test the actual drawing without complex mocking,
# but we can verify the method doesn't raise an exception
self.assertIsNotNone(renderable._error_message)
def test_draw_error_placeholder_with_text(self):
"""Test drawing error placeholder with error message"""
bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
renderable = RenderableImage(bad_abstract, self.canvas)
renderable._error_message = "File not found"
# Set origin for the placeholder
renderable.set_origin(np.array([10, 20]))
# Call the error placeholder method
renderable._draw_error_placeholder()
# Verify error message is set
self.assertIsNotNone(renderable._error_message)
self.assertIn("File not found", renderable._error_message)
def test_render_successful_image(self):
"""Test rendering successfully loaded image"""
renderable = RenderableImage(self.abstract_image, self.canvas)
renderable.set_origin(np.array([10, 20]))
# Render returns nothing (draws directly into canvas)
result = renderable.render()
# Result should be None as it draws directly
self.assertIsNone(result)
# Verify image was loaded
self.assertIsNotNone(renderable._pil_image)
def test_render_failed_image(self):
"""Test rendering when image failed to load"""
bad_abstract = AbstractImage("/nonexistent/path.png", "Bad Image")
renderable = RenderableImage(bad_abstract, self.canvas)
renderable.set_origin(np.array([10, 20]))
with patch.object(renderable, '_draw_error_placeholder') as mock_draw_error:
result = renderable.render()
# Result should be None as it draws directly
self.assertIsNone(result)
mock_draw_error.assert_called_once()
def test_render_with_left_alignment(self):
"""Test rendering with left alignment"""
renderable = RenderableImage(
self.abstract_image,
self.canvas,
halign=Alignment.LEFT,
valign=Alignment.TOP
)
renderable.set_origin(np.array([10, 20]))
result = renderable.render()
# Result should be None as it draws directly
self.assertIsNone(result)
self.assertEqual(renderable._halign, Alignment.LEFT)
self.assertEqual(renderable._valign, Alignment.TOP)
def test_render_with_right_alignment(self):
"""Test rendering with right alignment"""
renderable = RenderableImage(
self.abstract_image,
self.canvas,
halign=Alignment.RIGHT,
valign=Alignment.BOTTOM
)
renderable.set_origin(np.array([10, 20]))
result = renderable.render()
# Result should be None as it draws directly
self.assertIsNone(result)
self.assertEqual(renderable._halign, Alignment.RIGHT)
self.assertEqual(renderable._valign, Alignment.BOTTOM)
def test_render_rgb_image_conversion(self):
"""Test rendering RGB image (should be converted to RGBA)"""
# Our test image is RGB, so this should test the conversion path
renderable = RenderableImage(self.abstract_image, self.canvas)
renderable.set_origin(np.array([10, 20]))
result = renderable.render()
# Result should be None as it draws directly
self.assertIsNone(result)
self.assertIsNotNone(renderable._pil_image)
def test_in_object(self):
"""Test in_object method"""
renderable = RenderableImage(self.abstract_image, self.draw, origin=(10, 20))
# Point inside image
self.assertTrue(renderable.in_object((15, 25)))
# Point outside image
self.assertFalse(renderable.in_object((200, 200)))
def test_in_object_with_numpy_array(self):
"""Test in_object with numpy array point"""
renderable = RenderableImage(self.abstract_image, self.draw, origin=(10, 20))
# Point inside image as numpy array
point = np.array([15, 25])
self.assertTrue(renderable.in_object(point))
# Point outside image as numpy array
point = np.array([200, 200])
self.assertFalse(renderable.in_object(point))
def test_image_size_calculation_with_abstract_image_dimensions(self):
"""Test that size is calculated from abstract image when available"""
# Abstract image has dimensions 100x80
renderable = RenderableImage(self.abstract_image, self.draw)
# Size should match the calculated scaled dimensions
expected_size = self.abstract_image.calculate_scaled_dimensions()
np.testing.assert_array_equal(renderable._size, np.array(expected_size))
def test_image_size_calculation_with_constraints(self):
"""Test size calculation with max constraints"""
max_width = 60
max_height = 50
renderable = RenderableImage(
self.abstract_image,
self.draw,
max_width=max_width,
max_height=max_height
)
# Size should respect constraints
self.assertLessEqual(renderable._size[0], max_width)
self.assertLessEqual(renderable._size[1], max_height)
def test_image_without_initial_dimensions(self):
"""Test image without initial dimensions in abstract image"""
renderable = RenderableImage(self.abstract_image_no_dims, self.draw)
# Should still work, using default or calculated size
self.assertIsInstance(renderable._size, np.ndarray)
self.assertEqual(len(renderable._size), 2)
def test_set_origin_method(self):
"""Test the set_origin method"""
renderable = RenderableImage(self.abstract_image, self.draw)
new_origin = np.array([50, 60])
renderable.set_origin(new_origin)
np.testing.assert_array_equal(renderable.origin, new_origin)
def test_properties(self):
"""Test the property methods"""
renderable = RenderableImage(self.abstract_image, self.draw, origin=(10, 20), size=(100, 80))
np.testing.assert_array_equal(renderable.origin, np.array([10, 20]))
np.testing.assert_array_equal(renderable.size, np.array([100, 80]))
self.assertEqual(renderable.width, 100)
if __name__ == '__main__':
unittest.main()