Adsditional fix
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Unit tests for the alignment handler system.
|
||||
Tests the various alignment handlers (Left, Center, Right, Justify) and their integration with Line objects.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import numpy as np
|
||||
from unittest.mock import Mock
|
||||
|
||||
from pyWebLayout.concrete.text import Line, Text, LeftAlignmentHandler, CenterRightAlignmentHandler, JustifyAlignmentHandler
|
||||
from pyWebLayout.style.layout import Alignment
|
||||
from pyWebLayout.style import Font
|
||||
|
||||
|
||||
class TestAlignmentHandlers(unittest.TestCase):
|
||||
"""Test cases for the alignment handler system"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.font = Font()
|
||||
self.test_words = ["This", "is", "a", "test", "sentence"]
|
||||
self.line_width = 300
|
||||
self.line_height = 30
|
||||
self.spacing = (5, 20) # min_spacing, max_spacing
|
||||
self.origin = (0, 0)
|
||||
self.size = (self.line_width, self.line_height)
|
||||
|
||||
def test_left_alignment_handler_assignment(self):
|
||||
"""Test that Line correctly assigns LeftAlignmentHandler for LEFT alignment"""
|
||||
left_line = Line(self.spacing, self.origin, self.size, self.font, halign=Alignment.LEFT)
|
||||
|
||||
self.assertIsInstance(left_line._alignment_handler, LeftAlignmentHandler)
|
||||
|
||||
def test_center_alignment_handler_assignment(self):
|
||||
"""Test that Line correctly assigns CenterRightAlignmentHandler for CENTER alignment"""
|
||||
center_line = Line(self.spacing, self.origin, self.size, self.font, halign=Alignment.CENTER)
|
||||
|
||||
self.assertIsInstance(center_line._alignment_handler, CenterRightAlignmentHandler)
|
||||
# Check that it's configured for CENTER alignment
|
||||
self.assertEqual(center_line._alignment_handler._alignment, Alignment.CENTER)
|
||||
|
||||
def test_right_alignment_handler_assignment(self):
|
||||
"""Test that Line correctly assigns CenterRightAlignmentHandler for RIGHT alignment"""
|
||||
right_line = Line(self.spacing, self.origin, self.size, self.font, halign=Alignment.RIGHT)
|
||||
|
||||
self.assertIsInstance(right_line._alignment_handler, CenterRightAlignmentHandler)
|
||||
# Check that it's configured for RIGHT alignment
|
||||
self.assertEqual(right_line._alignment_handler._alignment, Alignment.RIGHT)
|
||||
|
||||
def test_justify_alignment_handler_assignment(self):
|
||||
"""Test that Line correctly assigns JustifyAlignmentHandler for JUSTIFY alignment"""
|
||||
justify_line = Line(self.spacing, self.origin, self.size, self.font, halign=Alignment.JUSTIFY)
|
||||
|
||||
self.assertIsInstance(justify_line._alignment_handler, JustifyAlignmentHandler)
|
||||
|
||||
def test_left_alignment_word_addition(self):
|
||||
"""Test adding words to a left-aligned line"""
|
||||
left_line = Line(self.spacing, self.origin, self.size, self.font, halign=Alignment.LEFT)
|
||||
|
||||
# Add words until line is full or we run out
|
||||
words_added = 0
|
||||
for word in self.test_words:
|
||||
result = left_line.add_word(word)
|
||||
if result:
|
||||
# Word didn't fit, should return the word
|
||||
self.assertEqual(result, word)
|
||||
break
|
||||
else:
|
||||
words_added += 1
|
||||
|
||||
# Should have added at least some words
|
||||
self.assertGreater(words_added, 0)
|
||||
self.assertEqual(len(left_line.text_objects), words_added)
|
||||
|
||||
def test_center_alignment_word_addition(self):
|
||||
"""Test adding words to a center-aligned line"""
|
||||
center_line = Line(self.spacing, self.origin, self.size, self.font, halign=Alignment.CENTER)
|
||||
|
||||
# Add words until line is full or we run out
|
||||
words_added = 0
|
||||
for word in self.test_words:
|
||||
result = center_line.add_word(word)
|
||||
if result:
|
||||
# Word didn't fit, should return the word
|
||||
self.assertEqual(result, word)
|
||||
break
|
||||
else:
|
||||
words_added += 1
|
||||
|
||||
# Should have added at least some words
|
||||
self.assertGreater(words_added, 0)
|
||||
self.assertEqual(len(center_line.text_objects), words_added)
|
||||
|
||||
def test_right_alignment_word_addition(self):
|
||||
"""Test adding words to a right-aligned line"""
|
||||
right_line = Line(self.spacing, self.origin, self.size, self.font, halign=Alignment.RIGHT)
|
||||
|
||||
# Add words until line is full or we run out
|
||||
words_added = 0
|
||||
for word in self.test_words:
|
||||
result = right_line.add_word(word)
|
||||
if result:
|
||||
# Word didn't fit, should return the word
|
||||
self.assertEqual(result, word)
|
||||
break
|
||||
else:
|
||||
words_added += 1
|
||||
|
||||
# Should have added at least some words
|
||||
self.assertGreater(words_added, 0)
|
||||
self.assertEqual(len(right_line.text_objects), words_added)
|
||||
|
||||
def test_justify_alignment_word_addition(self):
|
||||
"""Test adding words to a justified line"""
|
||||
justify_line = Line(self.spacing, self.origin, self.size, self.font, halign=Alignment.JUSTIFY)
|
||||
|
||||
# Add words until line is full or we run out
|
||||
words_added = 0
|
||||
for word in self.test_words:
|
||||
result = justify_line.add_word(word)
|
||||
if result:
|
||||
# Word didn't fit, should return the word
|
||||
self.assertEqual(result, word)
|
||||
break
|
||||
else:
|
||||
words_added += 1
|
||||
|
||||
# Should have added at least some words
|
||||
self.assertGreater(words_added, 0)
|
||||
self.assertEqual(len(justify_line.text_objects), words_added)
|
||||
|
||||
def test_handler_spacing_and_position_calculations(self):
|
||||
"""Test spacing and position calculations for different alignment handlers"""
|
||||
# Create sample text objects
|
||||
text_objects = [Text(word, self.font) for word in ["Hello", "World"]]
|
||||
|
||||
# Test each handler type
|
||||
handlers = [
|
||||
("Left", LeftAlignmentHandler()),
|
||||
("Center", CenterRightAlignmentHandler(Alignment.CENTER)),
|
||||
("Right", CenterRightAlignmentHandler(Alignment.RIGHT)),
|
||||
("Justify", JustifyAlignmentHandler())
|
||||
]
|
||||
|
||||
for name, handler in handlers:
|
||||
with self.subTest(handler=name):
|
||||
spacing_calc, position = handler.calculate_spacing_and_position(
|
||||
text_objects, self.line_width, self.spacing[0], self.spacing[1])
|
||||
|
||||
# Check that spacing is a valid number
|
||||
self.assertIsInstance(spacing_calc, (int, float))
|
||||
self.assertGreaterEqual(spacing_calc, 0)
|
||||
|
||||
# Check that position is a valid number
|
||||
self.assertIsInstance(position, (int, float))
|
||||
self.assertGreaterEqual(position, 0)
|
||||
|
||||
# Position should be within line width
|
||||
self.assertLessEqual(position, self.line_width)
|
||||
|
||||
def test_left_handler_spacing_calculation(self):
|
||||
"""Test specific spacing calculation for left alignment"""
|
||||
handler = LeftAlignmentHandler()
|
||||
text_objects = [Text(word, self.font) for word in ["Hello", "World"]]
|
||||
|
||||
spacing_calc, position = handler.calculate_spacing_and_position(
|
||||
text_objects, self.line_width, self.spacing[0], self.spacing[1])
|
||||
|
||||
# Left alignment should have position at 0
|
||||
self.assertEqual(position, 0)
|
||||
|
||||
# Spacing should be minimum spacing for left alignment
|
||||
self.assertEqual(spacing_calc, self.spacing[0])
|
||||
|
||||
def test_center_handler_spacing_calculation(self):
|
||||
"""Test specific spacing calculation for center alignment"""
|
||||
handler = CenterRightAlignmentHandler(Alignment.CENTER)
|
||||
text_objects = [Text(word, self.font) for word in ["Hello", "World"]]
|
||||
|
||||
spacing_calc, position = handler.calculate_spacing_and_position(
|
||||
text_objects, self.line_width, self.spacing[0], self.spacing[1])
|
||||
|
||||
# Center alignment should have position > 0 (centered)
|
||||
self.assertGreater(position, 0)
|
||||
|
||||
# Spacing should be minimum spacing for center alignment
|
||||
self.assertEqual(spacing_calc, self.spacing[0])
|
||||
|
||||
def test_right_handler_spacing_calculation(self):
|
||||
"""Test specific spacing calculation for right alignment"""
|
||||
handler = CenterRightAlignmentHandler(Alignment.RIGHT)
|
||||
text_objects = [Text(word, self.font) for word in ["Hello", "World"]]
|
||||
|
||||
spacing_calc, position = handler.calculate_spacing_and_position(
|
||||
text_objects, self.line_width, self.spacing[0], self.spacing[1])
|
||||
|
||||
# Right alignment should have position at the right edge minus content width
|
||||
self.assertGreater(position, 0)
|
||||
|
||||
# Spacing should be minimum spacing for right alignment
|
||||
self.assertEqual(spacing_calc, self.spacing[0])
|
||||
|
||||
def test_justify_handler_spacing_calculation(self):
|
||||
"""Test specific spacing calculation for justify alignment"""
|
||||
handler = JustifyAlignmentHandler()
|
||||
text_objects = [Text(word, self.font) for word in ["Hello", "World"]]
|
||||
|
||||
spacing_calc, position = handler.calculate_spacing_and_position(
|
||||
text_objects, self.line_width, self.spacing[0], self.spacing[1])
|
||||
|
||||
# Justify alignment should have position at 0
|
||||
self.assertEqual(position, 0)
|
||||
|
||||
# Spacing should be calculated to fill the line (between min and max)
|
||||
self.assertGreaterEqual(spacing_calc, self.spacing[0])
|
||||
self.assertLessEqual(spacing_calc, self.spacing[1])
|
||||
|
||||
def test_hyphenation_decisions(self):
|
||||
"""Test hyphenation decisions for different alignment handlers"""
|
||||
text_objects = [Text(word, self.font) for word in ["Hello", "World"]]
|
||||
test_word_width = 50
|
||||
available_width = 40 # Word doesn't fit
|
||||
|
||||
handlers = [
|
||||
("Left", LeftAlignmentHandler()),
|
||||
("Center", CenterRightAlignmentHandler(Alignment.CENTER)),
|
||||
("Right", CenterRightAlignmentHandler(Alignment.RIGHT)),
|
||||
("Justify", JustifyAlignmentHandler())
|
||||
]
|
||||
|
||||
for name, handler in handlers:
|
||||
with self.subTest(handler=name):
|
||||
should_hyphenate = handler.should_try_hyphenation(
|
||||
text_objects, test_word_width, available_width, self.spacing[0], self.font)
|
||||
|
||||
# Should return a boolean
|
||||
self.assertIsInstance(should_hyphenate, bool)
|
||||
|
||||
def test_hyphenation_decision_logic(self):
|
||||
"""Test specific hyphenation decision logic"""
|
||||
text_objects = [Text(word, self.font) for word in ["Hello"]]
|
||||
|
||||
# Test with word that doesn't fit
|
||||
handler = LeftAlignmentHandler()
|
||||
should_hyphenate_large = handler.should_try_hyphenation(
|
||||
text_objects, 100, 50, self.spacing[0], self.font)
|
||||
|
||||
# Test with word that fits
|
||||
should_hyphenate_small = handler.should_try_hyphenation(
|
||||
text_objects, 30, 50, self.spacing[0], self.font)
|
||||
|
||||
# Large word should suggest hyphenation, small word should not
|
||||
self.assertIsInstance(should_hyphenate_large, bool)
|
||||
self.assertIsInstance(should_hyphenate_small, bool)
|
||||
|
||||
def test_empty_line_alignment_handlers(self):
|
||||
"""Test alignment handlers with empty lines"""
|
||||
alignments = [Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.JUSTIFY]
|
||||
|
||||
for alignment in alignments:
|
||||
with self.subTest(alignment=alignment):
|
||||
line = Line(self.spacing, self.origin, self.size, self.font, halign=alignment)
|
||||
|
||||
# Empty line should still have a handler
|
||||
self.assertIsNotNone(line._alignment_handler)
|
||||
|
||||
# Should be able to render empty line
|
||||
result = line.render()
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
def test_single_word_line_alignment(self):
|
||||
"""Test alignment handlers with single word lines"""
|
||||
alignments = [Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.JUSTIFY]
|
||||
|
||||
for alignment in alignments:
|
||||
with self.subTest(alignment=alignment):
|
||||
line = Line(self.spacing, self.origin, self.size, self.font, halign=alignment)
|
||||
|
||||
# Add a single word
|
||||
result = line.add_word("test")
|
||||
self.assertIsNone(result) # Should fit
|
||||
|
||||
# Should be able to render single word line
|
||||
rendered = line.render()
|
||||
self.assertIsNotNone(rendered)
|
||||
self.assertEqual(len(line.text_objects), 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -250,16 +250,24 @@ class TestLine(unittest.TestCase):
|
||||
"""Test adding word that gets hyphenated"""
|
||||
# Mock hyphenation behavior
|
||||
mock_hyphenate.return_value = True
|
||||
mock_get_part.side_effect = lambda i: ["supercalifragilisticexpialidocious-", "remainder"][i]
|
||||
mock_get_part.side_effect = lambda i: ["super-", "califragilisticexpialidocious"][i]
|
||||
mock_part_count.return_value = 2
|
||||
|
||||
# Use a very narrow line to ensure even the first part doesn't fit
|
||||
narrow_line = Line(self.spacing, self.origin, (30, 20), self.font)
|
||||
# Create a font with lower min_hyphenation_width to allow hyphenation in narrow spaces
|
||||
test_font = Font(
|
||||
font_path=None,
|
||||
font_size=12,
|
||||
colour=(0, 0, 0),
|
||||
min_hyphenation_width=20 # Allow hyphenation in narrow spaces for testing
|
||||
)
|
||||
|
||||
# Use a narrow line but wide enough for the first hyphenated part
|
||||
narrow_line = Line(self.spacing, self.origin, (60, 20), test_font)
|
||||
result = narrow_line.add_word("supercalifragilisticexpialidocious")
|
||||
|
||||
# Should return the original word since even the first part doesn't fit
|
||||
# Should return the remaining part after hyphenation
|
||||
self.assertIsInstance(result, str)
|
||||
self.assertEqual(result, "supercalifragilisticexpialidocious")
|
||||
self.assertEqual(result, "califragilisticexpialidocious")
|
||||
|
||||
def test_add_multiple_words(self):
|
||||
"""Test adding multiple words to line"""
|
||||
|
||||
+270
-199
@@ -1,228 +1,299 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify multi-line text rendering and line wrapping functionality.
|
||||
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
|
||||
import os
|
||||
|
||||
def create_multiline_test(sentence, target_lines, line_width, line_height, font_size=14):
|
||||
"""
|
||||
Test rendering a sentence across multiple lines
|
||||
|
||||
class TestMultilineRendering(unittest.TestCase):
|
||||
"""Test cases for multi-line text rendering"""
|
||||
|
||||
Args:
|
||||
sentence: The sentence to render
|
||||
target_lines: Expected number of lines
|
||||
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 = []
|
||||
current_line = None
|
||||
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
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.font_style = Font(
|
||||
font_path=None,
|
||||
font_size=12,
|
||||
colour=(0, 0, 0, 255)
|
||||
)
|
||||
|
||||
lines.append(current_line)
|
||||
# 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()
|
||||
|
||||
# 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)
|
||||
# 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
|
||||
)
|
||||
|
||||
if result is None:
|
||||
# Word fit in the line
|
||||
words_added_to_line.append(word)
|
||||
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)
|
||||
else:
|
||||
# Word didn't fit, try next line
|
||||
break
|
||||
|
||||
# If no words were added to this line, we have a problem
|
||||
if not words_added_to_line:
|
||||
print(f"ERROR: Word '{words_remaining[0]}' is too long for line width {line_width}")
|
||||
break
|
||||
|
||||
# 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)
|
||||
# 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))
|
||||
|
||||
# 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_sentence_wrapping():
|
||||
"""Test various sentences with different expected line counts"""
|
||||
|
||||
test_cases = [
|
||||
{
|
||||
"sentence": "This is a simple test sentence that should wrap to exactly two lines.",
|
||||
"expected_lines": 2,
|
||||
"line_width": 200,
|
||||
"description": "Two-line sentence"
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"expected_lines": 3,
|
||||
"line_width": 180,
|
||||
"description": "Three-line sentence"
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"expected_lines": 4,
|
||||
"line_width": 160,
|
||||
"description": "Four-line sentence"
|
||||
},
|
||||
{
|
||||
"sentence": "Short sentence.",
|
||||
"expected_lines": 1,
|
||||
"line_width": 300,
|
||||
"description": "Single line sentence"
|
||||
},
|
||||
{
|
||||
"sentence": "This sentence has some really long words like supercalifragilisticexpialidocious that might need hyphenation.",
|
||||
"expected_lines": 3,
|
||||
"line_width": 150,
|
||||
"description": "Sentence with long words"
|
||||
}
|
||||
]
|
||||
|
||||
print("Testing multi-line sentence rendering...\n")
|
||||
|
||||
results = []
|
||||
|
||||
for i, test_case in enumerate(test_cases):
|
||||
sentence = test_case["sentence"]
|
||||
expected_lines = test_case["expected_lines"]
|
||||
line_width = test_case["line_width"]
|
||||
description = test_case["description"]
|
||||
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)
|
||||
|
||||
print(f"Test {i+1}: {description}")
|
||||
print(f" Sentence: \"{sentence}\"")
|
||||
print(f" Expected lines: {expected_lines}")
|
||||
print(f" Line width: {line_width}px")
|
||||
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
|
||||
|
||||
# Run the test
|
||||
actual_lines, lines, combined_image = create_multiline_test(
|
||||
sentence, expected_lines, line_width, 25, font_size=12
|
||||
actual_lines, lines, combined_image = self._create_multiline_test(
|
||||
sentence, line_width, 25, font_size=12
|
||||
)
|
||||
|
||||
print(f" Actual lines: {actual_lines}")
|
||||
# Save test image
|
||||
filename = "test_multiline_1_two_line_sentence.png"
|
||||
combined_image.save(filename)
|
||||
self.test_images.append(filename)
|
||||
|
||||
# Show word distribution
|
||||
for j, line in enumerate(lines):
|
||||
words_in_line = [word.text for word in line.text_objects]
|
||||
print(f" Line {j+1}: {' '.join(words_in_line)}")
|
||||
# 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")
|
||||
|
||||
# Save the result
|
||||
output_filename = f"test_multiline_{i+1}_{description.lower().replace(' ', '_').replace('-', '_')}.png"
|
||||
combined_image.save(output_filename)
|
||||
print(f" Saved as: {output_filename}")
|
||||
# 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
|
||||
|
||||
# Check if it matches expectations
|
||||
if actual_lines == expected_lines:
|
||||
print(f" ✓ SUCCESS: Got expected {expected_lines} lines")
|
||||
else:
|
||||
print(f" ✗ MISMATCH: Expected {expected_lines} lines, got {actual_lines}")
|
||||
|
||||
results.append({
|
||||
"test": description,
|
||||
"expected": expected_lines,
|
||||
"actual": actual_lines,
|
||||
"success": actual_lines == expected_lines,
|
||||
"filename": output_filename
|
||||
})
|
||||
|
||||
print()
|
||||
|
||||
# Summary
|
||||
print("="*60)
|
||||
print("SUMMARY")
|
||||
print("="*60)
|
||||
|
||||
successful_tests = sum(1 for r in results if r["success"])
|
||||
total_tests = len(results)
|
||||
|
||||
print(f"Tests passed: {successful_tests}/{total_tests}")
|
||||
print()
|
||||
|
||||
for result in results:
|
||||
status = "✓ PASS" if result["success"] else "✗ FAIL"
|
||||
print(f"{status} {result['test']}: {result['actual']}/{result['expected']} lines ({result['filename']})")
|
||||
|
||||
return results
|
||||
|
||||
def test_fixed_width_scenarios():
|
||||
"""Test specific width scenarios to verify line utilization"""
|
||||
print("\n" + "="*60)
|
||||
print("TESTING FIXED WIDTH SCENARIOS")
|
||||
print("="*60)
|
||||
|
||||
# Test with progressively narrower widths
|
||||
sentence = "The quick brown fox jumps over the lazy dog near the riverbank."
|
||||
widths = [300, 200, 150, 100, 80]
|
||||
|
||||
for width in widths:
|
||||
print(f"\nTesting width: {width}px")
|
||||
actual_lines, lines, combined_image = create_multiline_test(
|
||||
sentence, None, width, 20, font_size=12
|
||||
actual_lines, lines, combined_image = self._create_multiline_test(
|
||||
sentence, line_width, 25, font_size=12
|
||||
)
|
||||
|
||||
# Calculate utilization
|
||||
for j, line in enumerate(lines):
|
||||
words_in_line = [word.text for word in line.text_objects]
|
||||
line_text = ' '.join(words_in_line)
|
||||
utilization = (line._current_width / width) * 100
|
||||
print(f" Line {j+1}: \"{line_text}\" (width: {line._current_width}/{width}px, {utilization:.1f}% utilization)")
|
||||
# Save test image
|
||||
filename = "test_multiline_2_three_line_sentence.png"
|
||||
combined_image.save(filename)
|
||||
self.test_images.append(filename)
|
||||
|
||||
output_filename = f"test_width_{width}px.png"
|
||||
combined_image.save(output_filename)
|
||||
print(f" Saved as: {output_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__":
|
||||
print("Running multi-line text rendering verification tests...\n")
|
||||
|
||||
# Test sentence wrapping
|
||||
results = test_sentence_wrapping()
|
||||
|
||||
# Test fixed width scenarios
|
||||
test_fixed_width_scenarios()
|
||||
|
||||
print(f"\nAll tests completed. Check the generated PNG files for visual verification.")
|
||||
print("Look for:")
|
||||
print("- Proper line wrapping at expected breakpoints")
|
||||
print("- Good utilization of available line width")
|
||||
print("- No text cropping at line boundaries")
|
||||
print("- Proper word spacing and alignment")
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
+229
-119
@@ -1,147 +1,257 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test paragraph layout specifically to diagnose the line breaking issue
|
||||
Unit tests for paragraph layout fixes.
|
||||
Tests the paragraph layout system and page rendering functionality.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import os
|
||||
from PIL import Image
|
||||
|
||||
from pyWebLayout.concrete.page import Page
|
||||
from pyWebLayout.style.fonts import Font
|
||||
from pyWebLayout.abstract.block import Paragraph
|
||||
from pyWebLayout.abstract.inline import Word
|
||||
from pyWebLayout.typesetting.paragraph_layout import ParagraphLayout
|
||||
from pyWebLayout.style.layout import Alignment
|
||||
from PIL import Image
|
||||
|
||||
def test_paragraph_layout_directly():
|
||||
"""Test the paragraph layout system directly"""
|
||||
print("Testing paragraph layout system directly...")
|
||||
|
||||
# Create a paragraph with multiple words
|
||||
paragraph = Paragraph()
|
||||
font = Font(font_size=14)
|
||||
|
||||
# Add many words to force line breaking
|
||||
words_text = [
|
||||
"This", "is", "a", "very", "long", "paragraph", "that", "should",
|
||||
"definitely", "wrap", "across", "multiple", "lines", "when", "rendered",
|
||||
"in", "a", "narrow", "width", "container", "to", "test", "the",
|
||||
"paragraph", "layout", "system", "and", "ensure", "proper", "line",
|
||||
"breaking", "functionality", "works", "correctly", "as", "expected."
|
||||
]
|
||||
|
||||
for word_text in words_text:
|
||||
word = Word(word_text, font)
|
||||
paragraph.add_word(word)
|
||||
|
||||
# Create paragraph layout with narrow width to force wrapping
|
||||
layout = ParagraphLayout(
|
||||
line_width=300, # Narrow width
|
||||
line_height=20,
|
||||
word_spacing=(3, 8),
|
||||
line_spacing=3,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
# Layout the paragraph
|
||||
lines = layout.layout_paragraph(paragraph)
|
||||
|
||||
print(f"✓ Created paragraph with {len(words_text)} words")
|
||||
print(f"✓ Layout produced {len(lines)} lines")
|
||||
|
||||
# Check each line
|
||||
for i, line in enumerate(lines):
|
||||
word_count = len(line.text_objects) if hasattr(line, 'text_objects') else 0
|
||||
print(f" Line {i+1}: {word_count} words")
|
||||
|
||||
return len(lines) > 1 # Should have multiple lines
|
||||
|
||||
def test_page_with_long_paragraph():
|
||||
"""Test a page with manual content creation"""
|
||||
print("\nTesting page with manual content creation...")
|
||||
class TestParagraphLayoutFix(unittest.TestCase):
|
||||
"""Test cases for paragraph layout fixes"""
|
||||
|
||||
# Since Page doesn't support HTML loading, test basic page functionality
|
||||
# Create a page with narrower width
|
||||
page = Page(size=(400, 600))
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.font = Font(font_size=14)
|
||||
self.words_text = [
|
||||
"This", "is", "a", "very", "long", "paragraph", "that", "should",
|
||||
"definitely", "wrap", "across", "multiple", "lines", "when", "rendered",
|
||||
"in", "a", "narrow", "width", "container", "to", "test", "the",
|
||||
"paragraph", "layout", "system", "and", "ensure", "proper", "line",
|
||||
"breaking", "functionality", "works", "correctly", "as", "expected."
|
||||
]
|
||||
|
||||
# Clean up any existing test images
|
||||
test_images = [
|
||||
"test_paragraph_layout_output.png",
|
||||
"test_small_page.png",
|
||||
"test_large_page.png"
|
||||
]
|
||||
for img in test_images:
|
||||
if os.path.exists(img):
|
||||
os.remove(img)
|
||||
|
||||
print(f"✓ Page created with size {page._size}")
|
||||
print(f"✓ Page has {len(page._children)} initial elements")
|
||||
def tearDown(self):
|
||||
"""Clean up after tests"""
|
||||
# Clean up test images after each test
|
||||
test_images = [
|
||||
"test_paragraph_layout_output.png",
|
||||
"test_small_page.png",
|
||||
"test_large_page.png"
|
||||
]
|
||||
for img in test_images:
|
||||
if os.path.exists(img):
|
||||
os.remove(img)
|
||||
|
||||
# Try to render the empty page
|
||||
try:
|
||||
def test_paragraph_layout_directly(self):
|
||||
"""Test the paragraph layout system directly"""
|
||||
# Create a paragraph with multiple words
|
||||
paragraph = Paragraph()
|
||||
|
||||
# Add many words to force line breaking
|
||||
for word_text in self.words_text:
|
||||
word = Word(word_text, self.font)
|
||||
paragraph.add_word(word)
|
||||
|
||||
# Create paragraph layout with narrow width to force wrapping
|
||||
layout = ParagraphLayout(
|
||||
line_width=300, # Narrow width
|
||||
line_height=20,
|
||||
word_spacing=(3, 8),
|
||||
line_spacing=3,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
# Layout the paragraph
|
||||
lines = layout.layout_paragraph(paragraph)
|
||||
|
||||
# Assertions
|
||||
self.assertGreater(len(lines), 1, "Should have multiple lines")
|
||||
self.assertIsInstance(lines, list)
|
||||
|
||||
# Check each line has content
|
||||
for i, line in enumerate(lines):
|
||||
if hasattr(line, 'text_objects'):
|
||||
word_count = len(line.text_objects)
|
||||
self.assertGreater(word_count, 0, f"Line {i+1} should have words")
|
||||
|
||||
def test_page_with_long_paragraph(self):
|
||||
"""Test a page with manual content creation"""
|
||||
# Since Page doesn't support HTML loading, test basic page functionality
|
||||
# Create a page with narrower width
|
||||
page = Page(size=(400, 600))
|
||||
|
||||
# Verify page creation
|
||||
self.assertEqual(page._size[0], 400)
|
||||
self.assertEqual(page._size[1], 600)
|
||||
self.assertIsInstance(page._children, list)
|
||||
|
||||
# Try to render the empty page
|
||||
image = page.render()
|
||||
print(f"✓ Page rendered successfully: {image.size}")
|
||||
|
||||
# Assertions
|
||||
self.assertIsInstance(image, Image.Image)
|
||||
self.assertEqual(image.size, (400, 600))
|
||||
|
||||
# Save for inspection
|
||||
image.save("test_paragraph_layout_output.png")
|
||||
print("✓ Saved rendered page to: test_paragraph_layout_output.png")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ Error rendering page: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def test_simple_text_vs_paragraph():
|
||||
"""Test different page configurations"""
|
||||
print("\nTesting different page configurations...")
|
||||
self.assertTrue(os.path.exists("test_paragraph_layout_output.png"))
|
||||
|
||||
# Test 1: Small page
|
||||
page1 = Page(size=(400, 200))
|
||||
print(f"Small page has {len(page1._children)} children")
|
||||
|
||||
# Test 2: Large page
|
||||
page2 = Page(size=(800, 400))
|
||||
print(f"Large page has {len(page2._children)} children")
|
||||
|
||||
# Render both
|
||||
try:
|
||||
def test_simple_text_vs_paragraph(self):
|
||||
"""Test different page configurations"""
|
||||
# Test 1: Small page
|
||||
page1 = Page(size=(400, 200))
|
||||
self.assertIsInstance(page1._children, list)
|
||||
|
||||
# Test 2: Large page
|
||||
page2 = Page(size=(800, 400))
|
||||
self.assertIsInstance(page2._children, list)
|
||||
|
||||
# Render both
|
||||
img1 = page1.render()
|
||||
img2 = page2.render()
|
||||
|
||||
# Verify renders
|
||||
self.assertIsInstance(img1, Image.Image)
|
||||
self.assertIsInstance(img2, Image.Image)
|
||||
self.assertEqual(img1.size, (400, 200))
|
||||
self.assertEqual(img2.size, (800, 400))
|
||||
|
||||
# Save images
|
||||
img1.save("test_small_page.png")
|
||||
img2.save("test_large_page.png")
|
||||
|
||||
print("✓ Saved both test images")
|
||||
print(f"✓ Small page size: {img1.size}")
|
||||
print(f"✓ Large page size: {img2.size}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ Error rendering: {e}")
|
||||
return False
|
||||
# Verify files were created
|
||||
self.assertTrue(os.path.exists("test_small_page.png"))
|
||||
self.assertTrue(os.path.exists("test_large_page.png"))
|
||||
|
||||
def test_paragraph_creation_with_words(self):
|
||||
"""Test creating paragraphs with multiple words"""
|
||||
paragraph = Paragraph()
|
||||
|
||||
# Add words to paragraph
|
||||
for word_text in self.words_text[:5]: # Use first 5 words
|
||||
word = Word(word_text, self.font)
|
||||
paragraph.add_word(word)
|
||||
|
||||
# Verify paragraph has words
|
||||
self.assertGreater(len(paragraph._words), 0)
|
||||
|
||||
def test_paragraph_layout_configuration(self):
|
||||
"""Test different paragraph layout configurations"""
|
||||
layouts = [
|
||||
# Wide layout
|
||||
ParagraphLayout(
|
||||
line_width=600,
|
||||
line_height=20,
|
||||
word_spacing=(3, 8),
|
||||
line_spacing=3,
|
||||
halign=Alignment.LEFT
|
||||
),
|
||||
# Narrow layout
|
||||
ParagraphLayout(
|
||||
line_width=200,
|
||||
line_height=16,
|
||||
word_spacing=(2, 6),
|
||||
line_spacing=2,
|
||||
halign=Alignment.CENTER
|
||||
),
|
||||
# Justified layout
|
||||
ParagraphLayout(
|
||||
line_width=400,
|
||||
line_height=18,
|
||||
word_spacing=(4, 10),
|
||||
line_spacing=4,
|
||||
halign=Alignment.JUSTIFY
|
||||
)
|
||||
]
|
||||
|
||||
# Create test paragraph
|
||||
paragraph = Paragraph()
|
||||
for word_text in self.words_text[:10]: # Use first 10 words
|
||||
word = Word(word_text, self.font)
|
||||
paragraph.add_word(word)
|
||||
|
||||
# Test each layout
|
||||
for i, layout in enumerate(layouts):
|
||||
with self.subTest(layout=i):
|
||||
lines = layout.layout_paragraph(paragraph)
|
||||
self.assertIsInstance(lines, list)
|
||||
if len(paragraph._words) > 0:
|
||||
self.assertGreater(len(lines), 0, f"Layout {i} should produce lines")
|
||||
|
||||
def test_empty_paragraph_layout(self):
|
||||
"""Test laying out an empty paragraph"""
|
||||
paragraph = Paragraph()
|
||||
|
||||
layout = ParagraphLayout(
|
||||
line_width=300,
|
||||
line_height=20,
|
||||
word_spacing=(3, 8),
|
||||
line_spacing=3,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
lines = layout.layout_paragraph(paragraph)
|
||||
|
||||
# Empty paragraph should still return a list (might be empty)
|
||||
self.assertIsInstance(lines, list)
|
||||
|
||||
def test_single_word_paragraph(self):
|
||||
"""Test paragraph with single word"""
|
||||
paragraph = Paragraph()
|
||||
word = Word("Hello", self.font)
|
||||
paragraph.add_word(word)
|
||||
|
||||
layout = ParagraphLayout(
|
||||
line_width=300,
|
||||
line_height=20,
|
||||
word_spacing=(3, 8),
|
||||
line_spacing=3,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
lines = layout.layout_paragraph(paragraph)
|
||||
|
||||
# Single word should produce at least one line
|
||||
self.assertGreater(len(lines), 0)
|
||||
if len(lines) > 0 and hasattr(lines[0], 'text_objects'):
|
||||
self.assertGreater(len(lines[0].text_objects), 0)
|
||||
|
||||
def test_different_alignments(self):
|
||||
"""Test paragraph layout with different alignments"""
|
||||
alignments = [Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.JUSTIFY]
|
||||
|
||||
# Create test paragraph
|
||||
paragraph = Paragraph()
|
||||
for word_text in self.words_text[:8]:
|
||||
word = Word(word_text, self.font)
|
||||
paragraph.add_word(word)
|
||||
|
||||
for alignment in alignments:
|
||||
with self.subTest(alignment=alignment):
|
||||
layout = ParagraphLayout(
|
||||
line_width=300,
|
||||
line_height=20,
|
||||
word_spacing=(3, 8),
|
||||
line_spacing=3,
|
||||
halign=alignment
|
||||
)
|
||||
|
||||
lines = layout.layout_paragraph(paragraph)
|
||||
self.assertIsInstance(lines, list)
|
||||
if len(paragraph._words) > 0:
|
||||
self.assertGreater(len(lines), 0)
|
||||
|
||||
def main():
|
||||
"""Run all paragraph layout tests"""
|
||||
print("Testing paragraph layout fixes...")
|
||||
print("=" * 50)
|
||||
|
||||
tests = [
|
||||
("Direct Paragraph Layout", test_paragraph_layout_directly),
|
||||
("Page with Long Paragraph", test_page_with_long_paragraph),
|
||||
("Simple vs Complex Text", test_simple_text_vs_paragraph),
|
||||
]
|
||||
|
||||
results = []
|
||||
for test_name, test_func in tests:
|
||||
print(f"\nTesting: {test_name}")
|
||||
print("-" * 30)
|
||||
try:
|
||||
success = test_func()
|
||||
results.append((test_name, success))
|
||||
except Exception as e:
|
||||
print(f"✗ Test failed with exception: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
results.append((test_name, False))
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 50)
|
||||
print("Test Summary:")
|
||||
for test_name, success in results:
|
||||
status = "PASS" if success else "FAIL"
|
||||
print(f" {test_name}: {status}")
|
||||
|
||||
total_tests = len(results)
|
||||
passed_tests = sum(1 for _, success in results if success)
|
||||
print(f"\nPassed: {passed_tests}/{total_tests}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -1,339 +1,402 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify the paragraph layout system with pagination and state management.
|
||||
Unit tests for the paragraph layout system with pagination and state management.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import os
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from pyWebLayout.abstract.block import Paragraph
|
||||
from pyWebLayout.abstract.inline import Word
|
||||
from pyWebLayout.style import Font, FontStyle, FontWeight
|
||||
from pyWebLayout.typesetting.paragraph_layout import ParagraphLayout, ParagraphRenderingState, ParagraphLayoutResult
|
||||
from pyWebLayout.style.layout import Alignment
|
||||
|
||||
def create_test_paragraph(text: str) -> Paragraph:
|
||||
"""Create a test paragraph with the given text."""
|
||||
font_style = Font(
|
||||
font_path=None,
|
||||
font_size=12,
|
||||
colour=(0, 0, 0, 255)
|
||||
)
|
||||
|
||||
paragraph = Paragraph(style=font_style)
|
||||
|
||||
# Split text into words and add them to the paragraph
|
||||
words = text.split()
|
||||
for word_text in words:
|
||||
word = Word(word_text, font_style)
|
||||
paragraph.add_word(word)
|
||||
|
||||
return paragraph
|
||||
|
||||
def test_basic_paragraph_layout():
|
||||
"""Test basic paragraph layout without height constraints."""
|
||||
print("Testing basic paragraph layout...")
|
||||
class TestParagraphLayoutSystem(unittest.TestCase):
|
||||
"""Test cases for the paragraph layout system"""
|
||||
|
||||
text = "This is a test paragraph that should be laid out across multiple lines based on the available width."
|
||||
paragraph = create_test_paragraph(text)
|
||||
|
||||
# Create layout manager
|
||||
layout = ParagraphLayout(
|
||||
line_width=200,
|
||||
line_height=20,
|
||||
word_spacing=(3, 8),
|
||||
line_spacing=2,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
# Layout the paragraph
|
||||
lines = layout.layout_paragraph(paragraph)
|
||||
|
||||
print(f" Generated {len(lines)} lines")
|
||||
for i, line in enumerate(lines):
|
||||
words_in_line = [word.text for word in line.text_objects]
|
||||
print(f" Line {i+1}: {' '.join(words_in_line)}")
|
||||
|
||||
# Calculate total height
|
||||
total_height = layout.calculate_paragraph_height(paragraph)
|
||||
print(f" Total height: {total_height}px")
|
||||
|
||||
# Create visual representation
|
||||
if lines:
|
||||
# Create combined image
|
||||
canvas = Image.new('RGB', (layout.line_width, total_height), (255, 255, 255))
|
||||
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_test_paragraph(self, text: str) -> Paragraph:
|
||||
"""Helper method to create a test paragraph with the given text."""
|
||||
paragraph = Paragraph(style=self.font_style)
|
||||
|
||||
# Split text into words and add them to the paragraph
|
||||
words = text.split()
|
||||
for word_text in words:
|
||||
word = Word(word_text, self.font_style)
|
||||
paragraph.add_word(word)
|
||||
|
||||
return paragraph
|
||||
|
||||
def test_basic_paragraph_layout(self):
|
||||
"""Test basic paragraph layout without height constraints."""
|
||||
text = "This is a test paragraph that should be laid out across multiple lines based on the available width."
|
||||
paragraph = self._create_test_paragraph(text)
|
||||
|
||||
# Create layout manager
|
||||
layout = ParagraphLayout(
|
||||
line_width=200,
|
||||
line_height=20,
|
||||
word_spacing=(3, 8),
|
||||
line_spacing=2,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
# Layout the paragraph
|
||||
lines = layout.layout_paragraph(paragraph)
|
||||
|
||||
# Assertions
|
||||
self.assertIsInstance(lines, list)
|
||||
self.assertGreater(len(lines), 0, "Should generate at least one line")
|
||||
|
||||
# Check that all lines have content
|
||||
for i, line in enumerate(lines):
|
||||
line_img = line.render()
|
||||
y_pos = i * (layout.line_height + layout.line_spacing)
|
||||
canvas.paste(line_img, (0, y_pos), line_img)
|
||||
self.assertGreater(len(line.text_objects), 0, f"Line {i+1} should have content")
|
||||
|
||||
canvas.save("test_basic_paragraph_layout.png")
|
||||
print(f" Saved as: test_basic_paragraph_layout.png")
|
||||
|
||||
print()
|
||||
|
||||
def test_pagination_with_height_constraint():
|
||||
"""Test paragraph layout with height constraints (pagination)."""
|
||||
print("Testing pagination with height constraints...")
|
||||
|
||||
text = "This is a much longer paragraph that will definitely need to be split across multiple pages. It contains many words and should demonstrate how the pagination system works when we have height constraints. The system should be able to break the paragraph at appropriate points and provide information about remaining content that needs to be rendered on subsequent pages."
|
||||
paragraph = create_test_paragraph(text)
|
||||
|
||||
layout = ParagraphLayout(
|
||||
line_width=180,
|
||||
line_height=18,
|
||||
word_spacing=(2, 6),
|
||||
line_spacing=3,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
# Test with different page heights
|
||||
page_heights = [60, 100, 150] # Different page sizes
|
||||
|
||||
for page_height in page_heights:
|
||||
print(f" Testing with page height: {page_height}px")
|
||||
# Calculate total height
|
||||
total_height = layout.calculate_paragraph_height(paragraph)
|
||||
self.assertGreater(total_height, 0, "Total height should be positive")
|
||||
|
||||
result = layout.layout_paragraph_with_pagination(paragraph, page_height)
|
||||
# Create visual representation
|
||||
if lines:
|
||||
canvas = Image.new('RGB', (layout.line_width, total_height), (255, 255, 255))
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
line_img = line.render()
|
||||
y_pos = i * (layout.line_height + layout.line_spacing)
|
||||
canvas.paste(line_img, (0, y_pos), line_img)
|
||||
|
||||
filename = "test_basic_paragraph_layout.png"
|
||||
canvas.save(filename)
|
||||
self.test_images.append(filename)
|
||||
self.assertTrue(os.path.exists(filename), "Test image should be created")
|
||||
|
||||
def test_pagination_with_height_constraint(self):
|
||||
"""Test paragraph layout with height constraints (pagination)."""
|
||||
text = "This is a much longer paragraph that will definitely need to be split across multiple pages. It contains many words and should demonstrate how the pagination system works when we have height constraints. The system should be able to break the paragraph at appropriate points and provide information about remaining content that needs to be rendered on subsequent pages."
|
||||
paragraph = self._create_test_paragraph(text)
|
||||
|
||||
print(f" Generated {len(result.lines)} lines")
|
||||
print(f" Total height used: {result.total_height}px")
|
||||
print(f" Is complete: {result.is_complete}")
|
||||
layout = ParagraphLayout(
|
||||
line_width=180,
|
||||
line_height=18,
|
||||
word_spacing=(2, 6),
|
||||
line_spacing=3,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
if result.state:
|
||||
print(f" Current word index: {result.state.current_word_index}")
|
||||
print(f" Current char index: {result.state.current_char_index}")
|
||||
print(f" Rendered lines: {result.state.rendered_lines}")
|
||||
# Test with different page heights
|
||||
page_heights = [60, 100, 150]
|
||||
|
||||
# Show lines
|
||||
for page_height in page_heights:
|
||||
with self.subTest(page_height=page_height):
|
||||
result = layout.layout_paragraph_with_pagination(paragraph, page_height)
|
||||
|
||||
# Assertions
|
||||
self.assertIsInstance(result, ParagraphLayoutResult)
|
||||
self.assertIsInstance(result.lines, list)
|
||||
self.assertGreaterEqual(result.total_height, 0)
|
||||
self.assertIsInstance(result.is_complete, bool)
|
||||
|
||||
if result.state:
|
||||
self.assertIsInstance(result.state, ParagraphRenderingState)
|
||||
self.assertGreaterEqual(result.state.current_word_index, 0)
|
||||
self.assertGreaterEqual(result.state.current_char_index, 0)
|
||||
self.assertGreaterEqual(result.state.rendered_lines, 0)
|
||||
|
||||
# Create visual representation
|
||||
if result.lines:
|
||||
canvas = Image.new('RGB', (layout.line_width, page_height), (255, 255, 255))
|
||||
|
||||
# Add a border to show the page boundary
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
draw.rectangle([(0, 0), (layout.line_width-1, page_height-1)], outline=(200, 200, 200), width=2)
|
||||
|
||||
for i, line in enumerate(result.lines):
|
||||
line_img = line.render()
|
||||
y_pos = i * (layout.line_height + layout.line_spacing)
|
||||
if y_pos + layout.line_height <= page_height:
|
||||
canvas.paste(line_img, (0, y_pos), line_img)
|
||||
|
||||
filename = f"test_pagination_{page_height}px.png"
|
||||
canvas.save(filename)
|
||||
self.test_images.append(filename)
|
||||
self.assertTrue(os.path.exists(filename), f"Test image should be created for page height {page_height}")
|
||||
|
||||
def test_state_management(self):
|
||||
"""Test state saving and restoration for resumable rendering."""
|
||||
text = "This is a test of the state management system. We will render part of this paragraph, save the state, and then continue rendering from where we left off. This demonstrates how the system can handle interruptions and resume rendering later."
|
||||
paragraph = self._create_test_paragraph(text)
|
||||
|
||||
layout = ParagraphLayout(
|
||||
line_width=150,
|
||||
line_height=16,
|
||||
word_spacing=(2, 5),
|
||||
line_spacing=2,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
# First page - render with height constraint
|
||||
page_height = 50
|
||||
result1 = layout.layout_paragraph_with_pagination(paragraph, page_height)
|
||||
|
||||
# Assertions for first page
|
||||
self.assertIsInstance(result1, ParagraphLayoutResult)
|
||||
self.assertGreater(len(result1.lines), 0, "First page should have lines")
|
||||
|
||||
if result1.state:
|
||||
# Save the state
|
||||
state_json = result1.state.to_json()
|
||||
self.assertIsInstance(state_json, str, "State should serialize to JSON string")
|
||||
|
||||
# Create image for first page
|
||||
if result1.lines:
|
||||
canvas1 = Image.new('RGB', (layout.line_width, page_height), (255, 255, 255))
|
||||
draw = ImageDraw.Draw(canvas1)
|
||||
draw.rectangle([(0, 0), (layout.line_width-1, page_height-1)], outline=(200, 200, 200), width=2)
|
||||
|
||||
for i, line in enumerate(result1.lines):
|
||||
line_img = line.render()
|
||||
y_pos = i * (layout.line_height + layout.line_spacing)
|
||||
canvas1.paste(line_img, (0, y_pos), line_img)
|
||||
|
||||
filename1 = "test_state_page1.png"
|
||||
canvas1.save(filename1)
|
||||
self.test_images.append(filename1)
|
||||
self.assertTrue(os.path.exists(filename1), "First page image should be created")
|
||||
|
||||
# Continue from saved state on second page
|
||||
if not result1.is_complete and result1.remaining_paragraph:
|
||||
# Restore state
|
||||
restored_state = ParagraphRenderingState.from_json(state_json)
|
||||
self.assertIsInstance(restored_state, ParagraphRenderingState)
|
||||
self.assertEqual(restored_state.current_word_index, result1.state.current_word_index)
|
||||
self.assertEqual(restored_state.current_char_index, result1.state.current_char_index)
|
||||
|
||||
# Continue rendering
|
||||
result2 = layout.layout_paragraph_with_pagination(result1.remaining_paragraph, page_height)
|
||||
self.assertIsInstance(result2, ParagraphLayoutResult)
|
||||
|
||||
# Create image for second page
|
||||
if result2.lines:
|
||||
canvas2 = Image.new('RGB', (layout.line_width, page_height), (255, 255, 255))
|
||||
draw = ImageDraw.Draw(canvas2)
|
||||
draw.rectangle([(0, 0), (layout.line_width-1, page_height-1)], outline=(200, 200, 200), width=2)
|
||||
|
||||
for i, line in enumerate(result2.lines):
|
||||
line_img = line.render()
|
||||
y_pos = i * (layout.line_height + layout.line_spacing)
|
||||
canvas2.paste(line_img, (0, y_pos), line_img)
|
||||
|
||||
filename2 = "test_state_page2.png"
|
||||
canvas2.save(filename2)
|
||||
self.test_images.append(filename2)
|
||||
self.assertTrue(os.path.exists(filename2), "Second page image should be created")
|
||||
|
||||
def test_long_word_handling(self):
|
||||
"""Test handling of long words that require force-fitting."""
|
||||
text = "This paragraph contains supercalifragilisticexpialidocious and other extraordinarily long words that should be handled gracefully."
|
||||
paragraph = self._create_test_paragraph(text)
|
||||
|
||||
layout = ParagraphLayout(
|
||||
line_width=120, # Narrow width to force long word issues
|
||||
line_height=18,
|
||||
word_spacing=(2, 5),
|
||||
line_spacing=2,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
result = layout.layout_paragraph_with_pagination(paragraph, 200) # Generous height
|
||||
|
||||
# Assertions
|
||||
self.assertIsInstance(result, ParagraphLayoutResult)
|
||||
self.assertGreater(len(result.lines), 0, "Should generate lines even with long words")
|
||||
self.assertIsInstance(result.is_complete, bool)
|
||||
|
||||
# Verify that all lines have content
|
||||
for i, line in enumerate(result.lines):
|
||||
words_in_line = [word.text for word in line.text_objects]
|
||||
print(f" Line {i+1}: {' '.join(words_in_line)}")
|
||||
self.assertGreater(len(line.text_objects), 0, f"Line {i+1} should have content")
|
||||
|
||||
# Create visual representation
|
||||
if result.lines:
|
||||
canvas = Image.new('RGB', (layout.line_width, page_height), (255, 255, 255))
|
||||
|
||||
# Add a border to show the page boundary
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
draw.rectangle([(0, 0), (layout.line_width-1, page_height-1)], outline=(200, 200, 200), width=2)
|
||||
total_height = len(result.lines) * (layout.line_height + layout.line_spacing)
|
||||
canvas = Image.new('RGB', (layout.line_width, total_height), (255, 255, 255))
|
||||
|
||||
for i, line in enumerate(result.lines):
|
||||
line_img = line.render()
|
||||
y_pos = i * (layout.line_height + layout.line_spacing)
|
||||
if y_pos + layout.line_height <= page_height:
|
||||
canvas.paste(line_img, (0, y_pos), line_img)
|
||||
canvas.paste(line_img, (0, y_pos), line_img)
|
||||
|
||||
canvas.save(f"test_pagination_{page_height}px.png")
|
||||
print(f" Saved as: test_pagination_{page_height}px.png")
|
||||
filename = "test_long_word_handling.png"
|
||||
canvas.save(filename)
|
||||
self.test_images.append(filename)
|
||||
self.assertTrue(os.path.exists(filename), "Long word test image should be created")
|
||||
|
||||
def test_multiple_page_scenario(self):
|
||||
"""Test a realistic multi-page scenario."""
|
||||
text = """This is a comprehensive test of the paragraph layout system with pagination support.
|
||||
The system needs to handle various scenarios including normal word wrapping, hyphenation of long words,
|
||||
state management for resumable rendering, and proper text flow across multiple pages.
|
||||
|
||||
print()
|
||||
|
||||
def test_state_management():
|
||||
"""Test state saving and restoration for resumable rendering."""
|
||||
print("Testing state management (save/restore)...")
|
||||
|
||||
text = "This is a test of the state management system. We will render part of this paragraph, save the state, and then continue rendering from where we left off. This demonstrates how the system can handle interruptions and resume rendering later."
|
||||
paragraph = create_test_paragraph(text)
|
||||
|
||||
layout = ParagraphLayout(
|
||||
line_width=150,
|
||||
line_height=16,
|
||||
word_spacing=(2, 5),
|
||||
line_spacing=2,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
# First page - render with height constraint
|
||||
page_height = 50
|
||||
print(f" First page (height: {page_height}px):")
|
||||
|
||||
result1 = layout.layout_paragraph_with_pagination(paragraph, page_height)
|
||||
|
||||
print(f" Lines: {len(result1.lines)}")
|
||||
print(f" Complete: {result1.is_complete}")
|
||||
|
||||
if result1.state:
|
||||
# Save the state
|
||||
state_json = result1.state.to_json()
|
||||
print(f" Saved state: {state_json}")
|
||||
When a paragraph is too long to fit on a single page, the system should break it at appropriate
|
||||
points and maintain state information so that rendering can be resumed on the next page.
|
||||
This is essential for document processing applications where content needs to be paginated
|
||||
across multiple pages or screens.
|
||||
|
||||
# Create image for first page
|
||||
if result1.lines:
|
||||
canvas1 = Image.new('RGB', (layout.line_width, page_height), (255, 255, 255))
|
||||
draw = ImageDraw.Draw(canvas1)
|
||||
draw.rectangle([(0, 0), (layout.line_width-1, page_height-1)], outline=(200, 200, 200), width=2)
|
||||
|
||||
for i, line in enumerate(result1.lines):
|
||||
line_img = line.render()
|
||||
y_pos = i * (layout.line_height + layout.line_spacing)
|
||||
canvas1.paste(line_img, (0, y_pos), line_img)
|
||||
|
||||
canvas1.save("test_state_page1.png")
|
||||
print(f" First page saved as: test_state_page1.png")
|
||||
The system also needs to handle edge cases such as very long words that don't fit on a single line,
|
||||
ensuring that no text is lost and that the rendering process can continue gracefully even
|
||||
when encountering challenging content.""".replace('\n', ' ').replace(' ', ' ')
|
||||
|
||||
# Continue from saved state on second page
|
||||
if not result1.is_complete and result1.remaining_paragraph:
|
||||
print(f" Second page (continuing from saved state):")
|
||||
paragraph = self._create_test_paragraph(text)
|
||||
|
||||
layout = ParagraphLayout(
|
||||
line_width=200,
|
||||
line_height=20,
|
||||
word_spacing=(3, 8),
|
||||
line_spacing=3,
|
||||
halign=Alignment.JUSTIFY
|
||||
)
|
||||
|
||||
page_height = 80 # Small pages to force pagination
|
||||
pages = []
|
||||
current_paragraph = paragraph
|
||||
page_num = 1
|
||||
|
||||
while current_paragraph and page_num <= 10: # Safety limit
|
||||
result = layout.layout_paragraph_with_pagination(current_paragraph, page_height)
|
||||
|
||||
# Restore state
|
||||
restored_state = ParagraphRenderingState.from_json(state_json)
|
||||
print(f" Restored state: word_index={restored_state.current_word_index}, char_index={restored_state.current_char_index}")
|
||||
# Assertions
|
||||
self.assertIsInstance(result, ParagraphLayoutResult)
|
||||
|
||||
# Continue rendering
|
||||
result2 = layout.layout_paragraph_with_pagination(result1.remaining_paragraph, page_height)
|
||||
|
||||
print(f" Lines: {len(result2.lines)}")
|
||||
print(f" Complete: {result2.is_complete}")
|
||||
|
||||
# Create image for second page
|
||||
if result2.lines:
|
||||
canvas2 = Image.new('RGB', (layout.line_width, page_height), (255, 255, 255))
|
||||
draw = ImageDraw.Draw(canvas2)
|
||||
draw.rectangle([(0, 0), (layout.line_width-1, page_height-1)], outline=(200, 200, 200), width=2)
|
||||
if result.lines:
|
||||
# Create page image
|
||||
canvas = Image.new('RGB', (layout.line_width, page_height), (255, 255, 255))
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
|
||||
for i, line in enumerate(result2.lines):
|
||||
# Page border
|
||||
draw.rectangle([(0, 0), (layout.line_width-1, page_height-1)], outline=(100, 100, 100), width=1)
|
||||
|
||||
# Page number
|
||||
draw.text((5, page_height-15), f"Page {page_num}", fill=(150, 150, 150))
|
||||
|
||||
# Content
|
||||
for i, line in enumerate(result.lines):
|
||||
line_img = line.render()
|
||||
y_pos = i * (layout.line_height + layout.line_spacing)
|
||||
canvas2.paste(line_img, (0, y_pos), line_img)
|
||||
if y_pos + layout.line_height <= page_height - 20: # Leave space for page number
|
||||
canvas.paste(line_img, (0, y_pos), line_img)
|
||||
|
||||
canvas2.save("test_state_page2.png")
|
||||
print(f" Second page saved as: test_state_page2.png")
|
||||
pages.append(canvas)
|
||||
filename = f"test_multipage_page_{page_num}.png"
|
||||
canvas.save(filename)
|
||||
self.test_images.append(filename)
|
||||
self.assertTrue(os.path.exists(filename), f"Page {page_num} image should be created")
|
||||
|
||||
# Continue with remaining content
|
||||
current_paragraph = result.remaining_paragraph
|
||||
page_num += 1
|
||||
|
||||
# Assertions
|
||||
self.assertGreater(len(pages), 1, "Should generate multiple pages")
|
||||
self.assertLessEqual(page_num, 11, "Should not exceed safety limit")
|
||||
|
||||
print()
|
||||
def test_empty_paragraph(self):
|
||||
"""Test handling of empty paragraph"""
|
||||
paragraph = self._create_test_paragraph("")
|
||||
|
||||
layout = ParagraphLayout(
|
||||
line_width=200,
|
||||
line_height=20,
|
||||
word_spacing=(3, 8),
|
||||
line_spacing=2,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
lines = layout.layout_paragraph(paragraph)
|
||||
|
||||
# Should handle empty paragraph gracefully
|
||||
self.assertIsInstance(lines, list)
|
||||
|
||||
def test_single_word_paragraph(self):
|
||||
"""Test paragraph with single word"""
|
||||
paragraph = self._create_test_paragraph("Hello")
|
||||
|
||||
layout = ParagraphLayout(
|
||||
line_width=200,
|
||||
line_height=20,
|
||||
word_spacing=(3, 8),
|
||||
line_spacing=2,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
lines = layout.layout_paragraph(paragraph)
|
||||
|
||||
# Single word should produce one line
|
||||
self.assertGreater(len(lines), 0, "Single word should produce at least one line")
|
||||
if len(lines) > 0:
|
||||
self.assertGreater(len(lines[0].text_objects), 0, "Line should have content")
|
||||
|
||||
def test_different_alignments(self):
|
||||
"""Test paragraph layout with different alignments"""
|
||||
text = "This is a test paragraph for alignment testing with multiple words."
|
||||
alignments = [Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.JUSTIFY]
|
||||
|
||||
for alignment in alignments:
|
||||
with self.subTest(alignment=alignment):
|
||||
paragraph = self._create_test_paragraph(text)
|
||||
|
||||
layout = ParagraphLayout(
|
||||
line_width=200,
|
||||
line_height=20,
|
||||
word_spacing=(3, 8),
|
||||
line_spacing=2,
|
||||
halign=alignment
|
||||
)
|
||||
|
||||
lines = layout.layout_paragraph(paragraph)
|
||||
|
||||
# Should generate lines regardless of alignment
|
||||
self.assertIsInstance(lines, list)
|
||||
if len(paragraph._words) > 0:
|
||||
self.assertGreater(len(lines), 0, f"Should generate lines for {alignment}")
|
||||
|
||||
def test_calculate_paragraph_height(self):
|
||||
"""Test paragraph height calculation"""
|
||||
text = "This is a test paragraph for height calculation."
|
||||
paragraph = self._create_test_paragraph(text)
|
||||
|
||||
layout = ParagraphLayout(
|
||||
line_width=200,
|
||||
line_height=20,
|
||||
word_spacing=(3, 8),
|
||||
line_spacing=2,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
height = layout.calculate_paragraph_height(paragraph)
|
||||
|
||||
# Height should be positive
|
||||
self.assertGreater(height, 0, "Paragraph height should be positive")
|
||||
self.assertIsInstance(height, (int, float))
|
||||
|
||||
def test_long_word_handling():
|
||||
"""Test handling of long words that require force-fitting."""
|
||||
print("Testing long word handling...")
|
||||
|
||||
text = "This paragraph contains supercalifragilisticexpialidocious and other extraordinarily long words that should be handled gracefully."
|
||||
paragraph = create_test_paragraph(text)
|
||||
|
||||
layout = ParagraphLayout(
|
||||
line_width=120, # Narrow width to force long word issues
|
||||
line_height=18,
|
||||
word_spacing=(2, 5),
|
||||
line_spacing=2,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
result = layout.layout_paragraph_with_pagination(paragraph, 200) # Generous height
|
||||
|
||||
print(f" Generated {len(result.lines)} lines")
|
||||
print(f" Complete: {result.is_complete}")
|
||||
|
||||
# Show how long words were handled
|
||||
for i, line in enumerate(result.lines):
|
||||
words_in_line = [word.text for word in line.text_objects]
|
||||
line_text = ' '.join(words_in_line)
|
||||
print(f" Line {i+1}: \"{line_text}\"")
|
||||
|
||||
# Create visual representation
|
||||
if result.lines:
|
||||
total_height = len(result.lines) * (layout.line_height + layout.line_spacing)
|
||||
canvas = Image.new('RGB', (layout.line_width, total_height), (255, 255, 255))
|
||||
|
||||
for i, line in enumerate(result.lines):
|
||||
line_img = line.render()
|
||||
y_pos = i * (layout.line_height + layout.line_spacing)
|
||||
canvas.paste(line_img, (0, y_pos), line_img)
|
||||
|
||||
canvas.save("test_long_word_handling.png")
|
||||
print(f" Saved as: test_long_word_handling.png")
|
||||
|
||||
print()
|
||||
|
||||
def test_multiple_page_scenario():
|
||||
"""Test a realistic multi-page scenario."""
|
||||
print("Testing realistic multi-page scenario...")
|
||||
|
||||
text = """This is a comprehensive test of the paragraph layout system with pagination support.
|
||||
The system needs to handle various scenarios including normal word wrapping, hyphenation of long words,
|
||||
state management for resumable rendering, and proper text flow across multiple pages.
|
||||
|
||||
When a paragraph is too long to fit on a single page, the system should break it at appropriate
|
||||
points and maintain state information so that rendering can be resumed on the next page.
|
||||
This is essential for document processing applications where content needs to be paginated
|
||||
across multiple pages or screens.
|
||||
|
||||
The system also needs to handle edge cases such as very long words that don't fit on a single line,
|
||||
ensuring that no text is lost and that the rendering process can continue gracefully even
|
||||
when encountering challenging content.""".replace('\n', ' ').replace(' ', ' ')
|
||||
|
||||
paragraph = create_test_paragraph(text)
|
||||
|
||||
layout = ParagraphLayout(
|
||||
line_width=200,
|
||||
line_height=20,
|
||||
word_spacing=(3, 8),
|
||||
line_spacing=3,
|
||||
halign=Alignment.JUSTIFY
|
||||
)
|
||||
|
||||
page_height = 80 # Small pages to force pagination
|
||||
pages = []
|
||||
current_paragraph = paragraph
|
||||
page_num = 1
|
||||
|
||||
while current_paragraph:
|
||||
print(f" Rendering page {page_num}...")
|
||||
|
||||
result = layout.layout_paragraph_with_pagination(current_paragraph, page_height)
|
||||
|
||||
print(f" Lines on page: {len(result.lines)}")
|
||||
print(f" Page complete: {result.is_complete}")
|
||||
|
||||
if result.lines:
|
||||
# Create page image
|
||||
canvas = Image.new('RGB', (layout.line_width, page_height), (255, 255, 255))
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
|
||||
# Page border
|
||||
draw.rectangle([(0, 0), (layout.line_width-1, page_height-1)], outline=(100, 100, 100), width=1)
|
||||
|
||||
# Page number
|
||||
draw.text((5, page_height-15), f"Page {page_num}", fill=(150, 150, 150))
|
||||
|
||||
# Content
|
||||
for i, line in enumerate(result.lines):
|
||||
line_img = line.render()
|
||||
y_pos = i * (layout.line_height + layout.line_spacing)
|
||||
if y_pos + layout.line_height <= page_height - 20: # Leave space for page number
|
||||
canvas.paste(line_img, (0, y_pos), line_img)
|
||||
|
||||
pages.append(canvas)
|
||||
canvas.save(f"test_multipage_page_{page_num}.png")
|
||||
print(f" Saved as: test_multipage_page_{page_num}.png")
|
||||
|
||||
# Continue with remaining content
|
||||
current_paragraph = result.remaining_paragraph
|
||||
page_num += 1
|
||||
|
||||
# Safety check to prevent infinite loop
|
||||
if page_num > 10:
|
||||
print(" Safety limit reached - stopping pagination")
|
||||
break
|
||||
|
||||
print(f" Total pages generated: {len(pages)}")
|
||||
print()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Testing paragraph layout system with pagination and state management...\n")
|
||||
|
||||
test_basic_paragraph_layout()
|
||||
test_pagination_with_height_constraint()
|
||||
test_state_management()
|
||||
test_long_word_handling()
|
||||
test_multiple_page_scenario()
|
||||
|
||||
print("All tests completed!")
|
||||
print("\nGenerated files:")
|
||||
print("- test_basic_paragraph_layout.png")
|
||||
print("- test_pagination_*.png (multiple files)")
|
||||
print("- test_state_page1.png, test_state_page2.png")
|
||||
print("- test_long_word_handling.png")
|
||||
print("- test_multipage_page_*.png (multiple files)")
|
||||
print("\nThese images demonstrate:")
|
||||
print("1. Basic paragraph layout with proper line wrapping")
|
||||
print("2. Pagination with height constraints")
|
||||
print("3. State management and resumable rendering")
|
||||
print("4. Handling of long words with force-fitting")
|
||||
print("5. Realistic multi-page document layout")
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
+260
-125
@@ -1,150 +1,285 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify the text rendering fixes for cropping and line length issues.
|
||||
Unit tests for text rendering fixes.
|
||||
Tests the fixes for text cropping and line length issues.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import os
|
||||
from PIL import Image, ImageFont
|
||||
|
||||
from pyWebLayout.concrete.text import Text, Line
|
||||
from pyWebLayout.style import Font, FontStyle, FontWeight
|
||||
from pyWebLayout.style.layout import Alignment
|
||||
import os
|
||||
|
||||
def test_text_cropping_fix():
|
||||
"""Test that text is no longer cropped at the beginning and end"""
|
||||
print("Testing text cropping fixes...")
|
||||
|
||||
class TestTextRenderingFix(unittest.TestCase):
|
||||
"""Test cases for text rendering fixes"""
|
||||
|
||||
# Create a font with a reasonable size
|
||||
font_style = Font(
|
||||
font_path=None, # Use default font
|
||||
font_size=16,
|
||||
colour=(0, 0, 0, 255),
|
||||
weight=FontWeight.NORMAL,
|
||||
style=FontStyle.NORMAL
|
||||
)
|
||||
|
||||
# Test with text that might have overhang (like italic or characters with descenders)
|
||||
test_texts = [
|
||||
"Hello World!",
|
||||
"Typography",
|
||||
"gjpqy", # Characters with descenders
|
||||
"AWVT", # Characters that might have overhang
|
||||
"Italic Text"
|
||||
]
|
||||
|
||||
for i, text_content in enumerate(test_texts):
|
||||
print(f" Testing text: '{text_content}'")
|
||||
text = Text(text_content, font_style)
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
self.font_style = Font(
|
||||
font_path=None, # Use default font
|
||||
font_size=16,
|
||||
colour=(0, 0, 0, 255),
|
||||
weight=FontWeight.NORMAL,
|
||||
style=FontStyle.NORMAL
|
||||
)
|
||||
|
||||
# Verify dimensions are reasonable
|
||||
print(f" Dimensions: {text.width}x{text.height}")
|
||||
print(f" Text offsets: x={getattr(text, '_text_offset_x', 0)}, y={getattr(text, '_text_offset_y', 0)}")
|
||||
# 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 test_text_cropping_fix(self):
|
||||
"""Test that text is no longer cropped at the beginning and end"""
|
||||
# Test with text that might have overhang (like italic or characters with descenders)
|
||||
test_texts = [
|
||||
"Hello World!",
|
||||
"Typography",
|
||||
"gjpqy", # Characters with descenders
|
||||
"AWVT", # Characters that might have overhang
|
||||
"Italic Text"
|
||||
]
|
||||
|
||||
# Render the text
|
||||
for i, text_content in enumerate(test_texts):
|
||||
with self.subTest(text=text_content):
|
||||
text = Text(text_content, self.font_style)
|
||||
|
||||
# Verify dimensions are reasonable
|
||||
self.assertGreater(text.width, 0, f"Text '{text_content}' should have positive width")
|
||||
self.assertGreater(text.height, 0, f"Text '{text_content}' should have positive height")
|
||||
|
||||
# Render the text
|
||||
rendered = text.render()
|
||||
|
||||
# Verify rendered image
|
||||
self.assertIsInstance(rendered, Image.Image)
|
||||
self.assertGreater(rendered.size[0], 0, "Rendered image should have positive width")
|
||||
self.assertGreater(rendered.size[1], 0, "Rendered image should have positive height")
|
||||
|
||||
# Save for visual inspection
|
||||
output_path = f"test_text_{i}_{text_content.replace(' ', '_').replace('!', '')}.png"
|
||||
rendered.save(output_path)
|
||||
self.test_images.append(output_path)
|
||||
self.assertTrue(os.path.exists(output_path), f"Test image should be created for '{text_content}'")
|
||||
|
||||
def test_line_length_fix(self):
|
||||
"""Test that lines are using the full available width properly"""
|
||||
font_style = Font(
|
||||
font_path=None,
|
||||
font_size=14,
|
||||
colour=(0, 0, 0, 255)
|
||||
)
|
||||
|
||||
# Create a line with specific width
|
||||
line_width = 300
|
||||
line_height = 20
|
||||
spacing = (5, 10) # min, max spacing
|
||||
|
||||
line = Line(
|
||||
spacing=spacing,
|
||||
origin=(0, 0),
|
||||
size=(line_width, line_height),
|
||||
font=font_style,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
# Add words to the line
|
||||
words = ["This", "is", "a", "test", "of", "line", "length", "calculation"]
|
||||
|
||||
words_added = 0
|
||||
for word in words:
|
||||
result = line.add_word(word)
|
||||
if result:
|
||||
# Word didn't fit
|
||||
break
|
||||
else:
|
||||
words_added += 1
|
||||
|
||||
# Assertions
|
||||
self.assertGreater(words_added, 0, "Should have added at least one word")
|
||||
self.assertGreaterEqual(line._current_width, 0, "Line width should be non-negative")
|
||||
self.assertLessEqual(line._current_width, line_width, "Line width should not exceed maximum")
|
||||
|
||||
# Render the line
|
||||
rendered_line = line.render()
|
||||
self.assertIsInstance(rendered_line, Image.Image)
|
||||
self.assertEqual(rendered_line.size, (line_width, line_height))
|
||||
|
||||
# Save for inspection
|
||||
output_path = "test_line_length.png"
|
||||
rendered_line.save(output_path)
|
||||
self.test_images.append(output_path)
|
||||
self.assertTrue(os.path.exists(output_path), "Line test image should be created")
|
||||
|
||||
def test_justification(self):
|
||||
"""Test text justification to ensure proper spacing"""
|
||||
font_style = Font(
|
||||
font_path=None,
|
||||
font_size=12,
|
||||
colour=(0, 0, 0, 255)
|
||||
)
|
||||
|
||||
alignments = [
|
||||
(Alignment.LEFT, "left"),
|
||||
(Alignment.CENTER, "center"),
|
||||
(Alignment.RIGHT, "right"),
|
||||
(Alignment.JUSTIFY, "justify")
|
||||
]
|
||||
|
||||
for alignment, name in alignments:
|
||||
with self.subTest(alignment=name):
|
||||
line = Line(
|
||||
spacing=(3, 8),
|
||||
origin=(0, 0),
|
||||
size=(250, 18),
|
||||
font=font_style,
|
||||
halign=alignment
|
||||
)
|
||||
|
||||
# Add some words
|
||||
words = ["Testing", "text", "alignment", "and", "spacing"]
|
||||
for word in words:
|
||||
line.add_word(word)
|
||||
|
||||
# Verify line has content
|
||||
self.assertGreater(len(line.text_objects), 0, f"{name} alignment should have text objects")
|
||||
|
||||
# Render and verify
|
||||
rendered = line.render()
|
||||
self.assertIsInstance(rendered, Image.Image)
|
||||
self.assertEqual(rendered.size, (250, 18))
|
||||
|
||||
# Save for inspection
|
||||
output_path = f"test_alignment_{name}.png"
|
||||
rendered.save(output_path)
|
||||
self.test_images.append(output_path)
|
||||
self.assertTrue(os.path.exists(output_path), f"Alignment test image should be created for {name}")
|
||||
|
||||
def test_text_dimensions_consistency(self):
|
||||
"""Test that text dimensions are consistent between calculation and rendering"""
|
||||
test_texts = ["Short", "Medium length text", "Very long text that might cause issues"]
|
||||
|
||||
for text_content in test_texts:
|
||||
with self.subTest(text=text_content):
|
||||
text = Text(text_content, self.font_style)
|
||||
|
||||
# Get calculated dimensions
|
||||
calc_width = text.width
|
||||
calc_height = text.height
|
||||
calc_size = text.size
|
||||
|
||||
# Verify consistency
|
||||
self.assertEqual(calc_size, (calc_width, calc_height))
|
||||
self.assertGreater(calc_width, 0)
|
||||
self.assertGreater(calc_height, 0)
|
||||
|
||||
# Render and check dimensions match expectation
|
||||
rendered = text.render()
|
||||
self.assertIsInstance(rendered, Image.Image)
|
||||
# Note: rendered size might differ slightly due to margins/padding
|
||||
self.assertGreaterEqual(rendered.size[0], calc_width - 10) # Allow small tolerance
|
||||
self.assertGreaterEqual(rendered.size[1], calc_height - 10)
|
||||
|
||||
def test_different_font_sizes(self):
|
||||
"""Test text rendering with different font sizes"""
|
||||
font_sizes = [8, 12, 16, 20, 24]
|
||||
test_text = "Sample Text"
|
||||
|
||||
for font_size in font_sizes:
|
||||
with self.subTest(font_size=font_size):
|
||||
font = Font(
|
||||
font_path=None,
|
||||
font_size=font_size,
|
||||
colour=(0, 0, 0, 255)
|
||||
)
|
||||
|
||||
text = Text(test_text, font)
|
||||
|
||||
# Larger fonts should generally produce larger text
|
||||
self.assertGreater(text.width, 0)
|
||||
self.assertGreater(text.height, 0)
|
||||
|
||||
# Render should work
|
||||
rendered = text.render()
|
||||
self.assertIsInstance(rendered, Image.Image)
|
||||
|
||||
def test_empty_text_handling(self):
|
||||
"""Test handling of empty text"""
|
||||
text = Text("", self.font_style)
|
||||
|
||||
# Should handle empty text gracefully
|
||||
self.assertGreaterEqual(text.width, 0)
|
||||
self.assertGreaterEqual(text.height, 0)
|
||||
|
||||
# Should be able to render
|
||||
rendered = text.render()
|
||||
print(f" Rendered size: {rendered.size}")
|
||||
self.assertIsInstance(rendered, Image.Image)
|
||||
|
||||
|
||||
def test_line_multiple_words(self):
|
||||
"""Test adding multiple words to a line"""
|
||||
font_style = Font(font_path=None, font_size=12, colour=(0, 0, 0, 255))
|
||||
|
||||
# Save for visual inspection
|
||||
output_path = f"test_text_{i}_{text_content.replace(' ', '_').replace('!', '')}.png"
|
||||
rendered.save(output_path)
|
||||
print(f" Saved as: {output_path}")
|
||||
|
||||
print("Text cropping test completed.\n")
|
||||
|
||||
def test_line_length_fix():
|
||||
"""Test that lines are using the full available width properly"""
|
||||
print("Testing line length fixes...")
|
||||
|
||||
font_style = Font(
|
||||
font_path=None,
|
||||
font_size=14,
|
||||
colour=(0, 0, 0, 255)
|
||||
)
|
||||
|
||||
# Create a line with specific width
|
||||
line_width = 300
|
||||
line_height = 20
|
||||
spacing = (5, 10) # min, max spacing
|
||||
|
||||
line = Line(
|
||||
spacing=spacing,
|
||||
origin=(0, 0),
|
||||
size=(line_width, line_height),
|
||||
font=font_style,
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
# Add words to the line
|
||||
words = ["This", "is", "a", "test", "of", "line", "length", "calculation"]
|
||||
|
||||
print(f" Line width: {line_width}")
|
||||
print(f" Adding words: {' '.join(words)}")
|
||||
|
||||
for word in words:
|
||||
result = line.add_word(word)
|
||||
if result:
|
||||
print(f" Word '{word}' didn't fit, overflow: '{result}'")
|
||||
break
|
||||
else:
|
||||
print(f" Added '{word}', current width: {line._current_width}")
|
||||
|
||||
print(f" Final line width used: {line._current_width}/{line_width}")
|
||||
print(f" Words in line: {len(line.renderable_words)}")
|
||||
|
||||
# Render the line
|
||||
rendered_line = line.render()
|
||||
rendered_line.save("test_line_length.png")
|
||||
print(f" Line saved as: test_line_length.png")
|
||||
print(f" Rendered line size: {rendered_line.size}")
|
||||
|
||||
print("Line length test completed.\n")
|
||||
|
||||
def test_justification():
|
||||
"""Test text justification to ensure proper spacing"""
|
||||
print("Testing text justification...")
|
||||
|
||||
font_style = Font(
|
||||
font_path=None,
|
||||
font_size=12,
|
||||
colour=(0, 0, 0, 255)
|
||||
)
|
||||
|
||||
alignments = [
|
||||
(Alignment.LEFT, "left"),
|
||||
(Alignment.CENTER, "center"),
|
||||
(Alignment.RIGHT, "right"),
|
||||
(Alignment.JUSTIFY, "justify")
|
||||
]
|
||||
|
||||
for alignment, name in alignments:
|
||||
line = Line(
|
||||
spacing=(3, 8),
|
||||
origin=(0, 0),
|
||||
size=(250, 18),
|
||||
size=(200, 20),
|
||||
font=font_style,
|
||||
halign=alignment
|
||||
halign=Alignment.LEFT
|
||||
)
|
||||
|
||||
# Add some words
|
||||
words = ["Testing", "text", "alignment", "and", "spacing"]
|
||||
words = ["One", "Two", "Three", "Four", "Five"]
|
||||
added_words = []
|
||||
|
||||
for word in words:
|
||||
result = line.add_word(word)
|
||||
if result is None:
|
||||
added_words.append(word)
|
||||
else:
|
||||
break
|
||||
|
||||
# Should have added at least some words
|
||||
self.assertGreater(len(added_words), 0)
|
||||
self.assertEqual(len(line.text_objects), len(added_words))
|
||||
|
||||
# Verify text objects contain correct text
|
||||
for i, text_obj in enumerate(line.text_objects):
|
||||
self.assertEqual(text_obj.text, added_words[i])
|
||||
|
||||
def test_line_spacing_constraints(self):
|
||||
"""Test that line spacing respects min/max constraints"""
|
||||
font_style = Font(font_path=None, font_size=12, colour=(0, 0, 0, 255))
|
||||
|
||||
min_spacing = 3
|
||||
max_spacing = 10
|
||||
|
||||
line = Line(
|
||||
spacing=(min_spacing, max_spacing),
|
||||
origin=(0, 0),
|
||||
size=(300, 20),
|
||||
font=font_style,
|
||||
halign=Alignment.JUSTIFY # Justify will test spacing limits
|
||||
)
|
||||
|
||||
# Add multiple words
|
||||
words = ["Test", "spacing", "constraints", "here"]
|
||||
for word in words:
|
||||
line.add_word(word)
|
||||
|
||||
# Render the line
|
||||
rendered = line.render()
|
||||
output_path = f"test_alignment_{name}.png"
|
||||
rendered.save(output_path)
|
||||
print(f" {name.capitalize()} alignment saved as: {output_path}")
|
||||
|
||||
print("Justification test completed.\n")
|
||||
self.assertIsInstance(rendered, Image.Image)
|
||||
|
||||
# Line should respect spacing constraints (this is more of a system test)
|
||||
self.assertGreater(len(line.text_objects), 1, "Should have multiple words for spacing test")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Running text rendering fix verification tests...\n")
|
||||
|
||||
test_text_cropping_fix()
|
||||
test_line_length_fix()
|
||||
test_justification()
|
||||
|
||||
print("All tests completed. Check the generated PNG files for visual verification.")
|
||||
print("Look for:")
|
||||
print("- Text should not be cropped at the beginning or end")
|
||||
print("- Lines should use available width more efficiently")
|
||||
print("- Different alignments should work correctly")
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user