pyWebLayout/tests/test_multiline_rendering.py
Duncan Tourolle a014de854e
Some checks failed
Python CI / test (push) Failing after 4m7s
fix to use same font in CI and local tests
2025-06-08 17:10:47 +02:00

300 lines
12 KiB
Python

#!/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 = 280
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 = 200
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()