pyWebLayout/tests/test_paragraph_layout_system.py

340 lines
14 KiB
Python

#!/usr/bin/env python3
"""
Test script to verify the paragraph layout system with pagination and state management.
"""
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...")
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.word.text for word in line.renderable_words]
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))
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)
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")
result = layout.layout_paragraph_with_pagination(paragraph, page_height)
print(f" Generated {len(result.lines)} lines")
print(f" Total height used: {result.total_height}px")
print(f" Is complete: {result.is_complete}")
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}")
# Show lines
for i, line in enumerate(result.lines):
words_in_line = [word.word.text for word in line.renderable_words]
print(f" Line {i+1}: {' '.join(words_in_line)}")
# 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)
canvas.save(f"test_pagination_{page_height}px.png")
print(f" Saved as: test_pagination_{page_height}px.png")
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}")
# 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")
# Continue from saved state on second page
if not result1.is_complete and result1.remaining_paragraph:
print(f" Second page (continuing from saved state):")
# 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}")
# 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)
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)
canvas2.save("test_state_page2.png")
print(f" Second page saved as: test_state_page2.png")
print()
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.word.text for word in line.renderable_words]
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")