""" 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 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 BaseException: 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()