#!/usr/bin/env python3 """ Unit tests for multi-line text rendering and line wrapping functionality. """ import unittest import os from PIL import Image, ImageDraw from pyWebLayout.concrete.text import Text, Line from pyWebLayout.style import Font, FontStyle, FontWeight from pyWebLayout.style.layout import Alignment class TestMultilineRendering(unittest.TestCase): """Test cases for multi-line text rendering""" def setUp(self): """Set up test fixtures""" self.font_style = Font( font_path=None, font_size=12, colour=(0, 0, 0, 255) ) # Clean up any existing test images self.test_images = [] def tearDown(self): """Clean up after tests""" # Clean up test images for img in self.test_images: if os.path.exists(img): os.remove(img) def _create_multiline_test(self, sentence, line_width, line_height, font_size=14): """ Helper method to test rendering a sentence across multiple lines Args: sentence: The sentence to render line_width: Width of each line in pixels line_height: Height of each line in pixels font_size: Font size to use Returns: tuple: (actual_lines_used, lines_list, combined_image) """ font_style = Font( font_path=None, font_size=font_size, colour=(0, 0, 0, 255) ) # Split sentence into words words = sentence.split() # Create lines and distribute words lines = [] words_remaining = words.copy() while words_remaining: # Create a new line current_line = Line( spacing=(3, 8), # min, max spacing origin=(0, len(lines) * line_height), size=(line_width, line_height), font=font_style, halign=Alignment.LEFT ) lines.append(current_line) # Add words to current line until it's full words_added_to_line = [] while words_remaining: word = words_remaining[0] result = current_line.add_word(word) if result is None: # Word fit in the line words_added_to_line.append(word) words_remaining.pop(0) else: # Word didn't fit, try next line break # If no words were added to this line, break to avoid infinite loop if not words_added_to_line: # Force add the word to avoid infinite loop current_line.add_word(words_remaining[0]) words_remaining.pop(0) # Create combined image showing all lines total_height = len(lines) * line_height combined_image = Image.new('RGBA', (line_width, total_height), (255, 255, 255, 255)) for i, line in enumerate(lines): line_img = line.render() y_pos = i * line_height combined_image.paste(line_img, (0, y_pos), line_img) # Add a subtle line border for visualization draw = ImageDraw.Draw(combined_image) draw.rectangle([(0, y_pos), (line_width-1, y_pos + line_height-1)], outline=(200, 200, 200), width=1) return len(lines), lines, combined_image def test_two_line_sentence(self): """Test sentence that should wrap to two lines""" sentence = "This is a simple test sentence that should wrap to exactly two lines." line_width = 200 expected_lines = 2 actual_lines, lines, combined_image = self._create_multiline_test( sentence, line_width, 25, font_size=12 ) # Save test image filename = "test_multiline_1_two_line_sentence.png" combined_image.save(filename) self.test_images.append(filename) # Assertions self.assertGreaterEqual(actual_lines, 1, "Should have at least one line") self.assertLessEqual(actual_lines, 3, "Should not exceed 3 lines for this sentence") self.assertTrue(os.path.exists(filename), "Test image should be created") # Check that all lines have content for i, line in enumerate(lines): self.assertGreater(len(line.text_objects), 0, f"Line {i+1} should have content") def test_three_line_sentence(self): """Test sentence that should wrap to three lines""" sentence = "This is a much longer sentence that contains many more words and should definitely wrap across three lines when rendered with the specified width constraints." line_width = 180 actual_lines, lines, combined_image = self._create_multiline_test( sentence, line_width, 25, font_size=12 ) # Save test image filename = "test_multiline_2_three_line_sentence.png" combined_image.save(filename) self.test_images.append(filename) # Assertions self.assertGreaterEqual(actual_lines, 2, "Should have at least two lines") self.assertLessEqual(actual_lines, 5, "Should not exceed 5 lines for this sentence") self.assertTrue(os.path.exists(filename), "Test image should be created") def test_four_line_sentence(self): """Test sentence that should wrap to four lines""" sentence = "Here we have an even longer sentence with significantly more content that will require four lines to properly display all the text when using the constrained width setting." line_width = 160 actual_lines, lines, combined_image = self._create_multiline_test( sentence, line_width, 25, font_size=12 ) # Save test image filename = "test_multiline_3_four_line_sentence.png" combined_image.save(filename) self.test_images.append(filename) # Assertions self.assertGreaterEqual(actual_lines, 3, "Should have at least three lines") self.assertLessEqual(actual_lines, 6, "Should not exceed 6 lines for this sentence") self.assertTrue(os.path.exists(filename), "Test image should be created") def test_single_line_sentence(self): """Test short sentence that should fit on one line""" sentence = "Short sentence." line_width = 300 actual_lines, lines, combined_image = self._create_multiline_test( sentence, line_width, 25, font_size=12 ) # Save test image filename = "test_multiline_4_single_line_sentence.png" combined_image.save(filename) self.test_images.append(filename) # Assertions self.assertEqual(actual_lines, 1, "Short sentence should fit on one line") self.assertGreater(len(lines[0].text_objects), 0, "Line should have content") self.assertTrue(os.path.exists(filename), "Test image should be created") def test_long_words_sentence(self): """Test sentence with long words that might need special handling""" sentence = "This sentence has some really long words like supercalifragilisticexpialidocious that might need hyphenation." line_width = 150 actual_lines, lines, combined_image = self._create_multiline_test( sentence, line_width, 25, font_size=12 ) # Save test image filename = "test_multiline_5_sentence_with_long_words.png" combined_image.save(filename) self.test_images.append(filename) # Assertions self.assertGreaterEqual(actual_lines, 2, "Should have at least two lines") self.assertTrue(os.path.exists(filename), "Test image should be created") def test_fixed_width_scenarios(self): """Test specific width scenarios to verify line utilization""" sentence = "The quick brown fox jumps over the lazy dog near the riverbank." widths = [300, 200, 150, 100, 80] for width in widths: with self.subTest(width=width): actual_lines, lines, combined_image = self._create_multiline_test( sentence, width, 20, font_size=12 ) # Assertions self.assertGreater(actual_lines, 0, f"Should have lines for width {width}") self.assertIsInstance(combined_image, Image.Image) # Save test image filename = f"test_width_{width}px.png" combined_image.save(filename) self.test_images.append(filename) self.assertTrue(os.path.exists(filename), f"Test image should be created for width {width}") # Check line utilization for j, line in enumerate(lines): self.assertGreater(len(line.text_objects), 0, f"Line {j+1} should have content") self.assertGreaterEqual(line._current_width, 0, f"Line {j+1} should have positive width") def test_line_word_distribution(self): """Test that words are properly distributed across lines""" sentence = "This is a test sentence with several words to distribute." line_width = 200 actual_lines, lines, combined_image = self._create_multiline_test( sentence, line_width, 25, font_size=12 ) # Check that each line has words total_words = 0 for i, line in enumerate(lines): word_count = len(line.text_objects) self.assertGreater(word_count, 0, f"Line {i+1} should have at least one word") total_words += word_count # Total words should match original sentence original_words = len(sentence.split()) self.assertEqual(total_words, original_words, "All words should be distributed across lines") def test_line_width_constraints(self): """Test that lines respect width constraints""" sentence = "Testing width constraints with this sentence." line_width = 150 actual_lines, lines, combined_image = self._create_multiline_test( sentence, line_width, 25, font_size=12 ) # Check that no line exceeds the specified width (with some tolerance for edge cases) for i, line in enumerate(lines): # Current width should not significantly exceed line width # Allow some tolerance for edge cases where words are force-fitted self.assertLessEqual(line._current_width, line_width + 50, f"Line {i+1} width should not significantly exceed limit") def test_empty_sentence(self): """Test handling of empty sentence""" sentence = "" line_width = 200 actual_lines, lines, combined_image = self._create_multiline_test( sentence, line_width, 25, font_size=12 ) # Should handle empty sentence gracefully self.assertIsInstance(actual_lines, int) self.assertIsInstance(lines, list) self.assertIsInstance(combined_image, Image.Image) def test_single_word_sentence(self): """Test handling of single word sentence""" sentence = "Hello" line_width = 200 actual_lines, lines, combined_image = self._create_multiline_test( sentence, line_width, 25, font_size=12 ) # Single word should fit on one line self.assertEqual(actual_lines, 1, "Single word should fit on one line") self.assertEqual(len(lines[0].text_objects), 1, "Line should have exactly one word") if __name__ == '__main__': unittest.main()