#!/usr/bin/env python3 """ 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 class TestParagraphLayoutSystem(unittest.TestCase): """Test cases for the paragraph layout system""" 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): self.assertGreater(len(line.text_objects), 0, f"Line {i+1} should have content") # Calculate total height total_height = layout.calculate_paragraph_height(paragraph) self.assertGreater(total_height, 0, "Total height should be positive") # 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) 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] 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): self.assertGreater(len(line.text_objects), 0, f"Line {i+1} should have content") # 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) 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. 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 = 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) # Assertions self.assertIsInstance(result, ParagraphLayoutResult) 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) 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") 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)) if __name__ == '__main__': unittest.main()