Addtitional rending stuff...

This commit is contained in:
2025-06-07 22:32:54 +02:00
parent 4e65fe3e67
commit 3f0b2747d2
22 changed files with 3626 additions and 63 deletions
+1 -1
View File
@@ -287,7 +287,7 @@ class TestPage(unittest.TestCase):
self.assertEqual(page._mode, 'RGBA')
self.assertEqual(page._direction, 'vertical')
self.assertEqual(page._spacing, 10)
self.assertEqual(page._halign, Alignment.CENTER)
self.assertEqual(page._halign, Alignment.LEFT)
self.assertEqual(page._valign, Alignment.TOP)
def test_page_initialization_with_params(self):
+155
View File
@@ -0,0 +1,155 @@
#!/usr/bin/env python3
"""
Test the enhanced Page class with HTML loading capabilities
"""
from pyWebLayout.concrete.page import Page
from pyWebLayout.style.fonts import Font
from PIL import Image
import tempfile
import os
def test_page_html_loading():
"""Test loading HTML content into a Page"""
# Create a test HTML content
html_content = """
<html>
<head><title>Test Page</title></head>
<body>
<h1>Welcome to pyWebLayout</h1>
<p>This is a <strong>test paragraph</strong> with <em>some formatting</em>.</p>
<h2>Features</h2>
<ul>
<li>HTML parsing</li>
<li>Text rendering</li>
<li>Basic styling</li>
</ul>
<p>Another paragraph with different content.</p>
</body>
</html>
"""
# Create a page and load the HTML
page = Page(size=(800, 600))
page.load_html_string(html_content)
# Render the page
try:
image = page.render()
print(f"✓ Successfully rendered page: {image.size}")
# Save the rendered image for inspection
output_path = "test_page_output.png"
image.save(output_path)
print(f"✓ Saved rendered page to: {output_path}")
return True
except Exception as e:
print(f"✗ Error rendering page: {e}")
return False
def test_page_html_file_loading():
"""Test loading HTML from a file"""
# Create a temporary HTML file
html_content = """
<!DOCTYPE html>
<html>
<head><title>File Test</title></head>
<body>
<h1>Loading from File</h1>
<p>This content was loaded from a file.</p>
<h2>Styled Content</h2>
<p>Text with <strong>bold</strong> and <em>italic</em> formatting.</p>
</body>
</html>
"""
# Write to temporary file
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as f:
f.write(html_content)
temp_file = f.name
try:
# Create a page and load the file
page = Page(size=(800, 600))
page.load_html_file(temp_file)
# Render the page
image = page.render()
print(f"✓ Successfully loaded and rendered HTML file: {image.size}")
# Save the rendered image
output_path = "test_file_page_output.png"
image.save(output_path)
print(f"✓ Saved file-loaded page to: {output_path}")
return True
except Exception as e:
print(f"✗ Error loading HTML file: {e}")
return False
finally:
# Clean up temporary file
try:
os.unlink(temp_file)
except OSError:
pass
def test_epub_reader_imports():
"""Test that the EPUB reader can be imported without errors"""
try:
from epub_reader_tk import EPUBReaderApp
print("✓ Successfully imported EPUBReaderApp")
# Test creating the app (but don't show it)
app = EPUBReaderApp()
print("✓ Successfully created EPUBReaderApp instance")
return True
except Exception as e:
print(f"✗ Error importing/creating EPUB reader: {e}")
return False
def main():
"""Run all tests"""
print("Testing enhanced Page class and EPUB reader...")
print("=" * 50)
tests = [
("HTML String Loading", test_page_html_loading),
("HTML File Loading", test_page_html_file_loading),
("EPUB Reader Imports", test_epub_reader_imports),
]
results = []
for test_name, test_func in tests:
print(f"\nTesting: {test_name}")
print("-" * 30)
success = test_func()
results.append((test_name, success))
# 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 passed_tests == total_tests:
print("🎉 All tests passed!")
else:
print(f"⚠️ {total_tests - passed_tests} test(s) failed")
if __name__ == "__main__":
main()
+1 -1
View File
@@ -53,7 +53,7 @@ class TestStyleObjects(unittest.TestCase):
font = Font()
self.assertIsNone(font._font_path)
self.assertEqual(font.font_size, 12)
self.assertEqual(font.font_size, 16)
self.assertEqual(font.colour, (0, 0, 0))
self.assertEqual(font.color, (0, 0, 0)) # Alias
self.assertEqual(font.weight, FontWeight.NORMAL)
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""
Test to demonstrate and verify fix for the line splitting bug where
text is lost at line breaks due to improper hyphenation handling.
"""
import unittest
from unittest.mock import patch, Mock
from pyWebLayout.concrete.text import Line
from pyWebLayout.abstract.inline import Word
from pyWebLayout.style import Font
from pyWebLayout.style.layout import Alignment
class TestLineSplittingBug(unittest.TestCase):
"""Test cases for the line splitting bug"""
def setUp(self):
"""Set up test fixtures"""
self.font = Font(
font_path=None,
font_size=12,
colour=(0, 0, 0)
)
self.spacing = (5, 10)
self.origin = (0, 0)
self.size = (100, 20) # Narrow line to force hyphenation
@patch('pyWebLayout.abstract.inline.pyphen')
def test_hyphenation_preserves_word_boundaries(self, mock_pyphen_module):
"""Test that hyphenation properly preserves word boundaries"""
# Mock pyphen to return a multi-part hyphenated word
mock_dic = Mock()
mock_pyphen_module.Pyphen.return_value = mock_dic
# Simulate hyphenating "supercalifragilisticexpialidocious"
# into multiple parts: "super-", "cali-", "fragi-", "listic-", "expiali-", "docious"
mock_dic.inserted.return_value = "super-cali-fragi-listic-expiali-docious"
line = Line(self.spacing, self.origin, self.size, self.font)
# Add the word that will be hyphenated
overflow = line.add_word("supercalifragilisticexpialidocious")
# The overflow should be the next part only, not all remaining parts joined
# In the current buggy implementation, this would return "cali-fragi-listic-expiali-docious"
# But it should return "cali-" (the next single part)
print(f"Overflow returned: '{overflow}'")
# Check that the first part was added to the line
self.assertEqual(len(line.renderable_words), 1)
first_word_text = line.renderable_words[0].word.text
self.assertEqual(first_word_text, "super-")
# The overflow should be just the next part, not all parts joined
# This assertion will fail with the current bug, showing the issue
self.assertEqual(overflow, "cali-") # Should be next part only
# NOT this (which is what the bug produces):
# self.assertEqual(overflow, "cali-fragi-listic-expiali-docious")
@patch('pyWebLayout.abstract.inline.pyphen')
def test_single_word_overflow_behavior(self, mock_pyphen_module):
"""Test that overflow returns only the next part, not all remaining parts joined"""
# Mock pyphen to return a simple two-part hyphenated word
mock_dic = Mock()
mock_pyphen_module.Pyphen.return_value = mock_dic
mock_dic.inserted.return_value = "very-long"
# Create a narrow line that will force hyphenation
line = Line(self.spacing, (0, 0), (40, 20), self.font)
# Add the word that will be hyphenated
overflow = line.add_word("verylong")
# Check that the first part was added to the line
self.assertEqual(len(line.renderable_words), 1)
first_word_text = line.renderable_words[0].word.text
self.assertEqual(first_word_text, "very-")
# The overflow should be just the next part ("long"), not multiple parts joined
# This tests the core fix for the line splitting bug
self.assertEqual(overflow, "long")
print(f"First part in line: '{first_word_text}'")
print(f"Overflow returned: '{overflow}'")
def test_simple_overflow_case(self):
"""Test a simple word overflow without hyphenation to verify baseline behavior"""
line = Line(self.spacing, self.origin, (50, 20), self.font)
# Add a word that fits
result1 = line.add_word("short")
self.assertIsNone(result1)
# Add a word that doesn't fit (should overflow)
result2 = line.add_word("verylongword")
self.assertEqual(result2, "verylongword")
# Only the first word should be in the line
self.assertEqual(len(line.renderable_words), 1)
self.assertEqual(line.renderable_words[0].word.text, "short")
def demonstrate_bug():
"""Demonstrate the bug with a practical example"""
print("=" * 60)
print("DEMONSTRATING LINE SPLITTING BUG")
print("=" * 60)
font = Font(font_path=None, font_size=12, colour=(0, 0, 0))
# Create a very narrow line that will force hyphenation
line = Line((3, 6), (0, 0), (80, 20), font)
# Try to add a long word that should be hyphenated
with patch('pyWebLayout.abstract.inline.pyphen') as mock_pyphen_module:
mock_dic = Mock()
mock_pyphen_module.Pyphen.return_value = mock_dic
mock_dic.inserted.return_value = "hyper-long-example-word"
overflow = line.add_word("hyperlongexampleword")
print(f"Original word: 'hyperlongexampleword'")
print(f"Hyphenated to: 'hyper-long-example-word'")
print(f"First part added to line: '{line.renderable_words[0].word.text if line.renderable_words else 'None'}'")
print(f"Overflow returned: '{overflow}'")
print()
print("PROBLEM: The overflow should be 'long-' (next part only)")
print("but instead it returns 'long-example-word' (all remaining parts joined)")
print("This causes word boundary information to be lost!")
if __name__ == "__main__":
# First demonstrate the bug
demonstrate_bug()
print("\n" + "=" * 60)
print("RUNNING UNIT TESTS")
print("=" * 60)
# Run unit tests
unittest.main()
+162
View File
@@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""
Test script specifically for verifying the long word fix.
"""
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
def test_supercalifragilisticexpialidocious():
"""Test the specific long word that was causing issues"""
print("Testing long word handling...")
font_style = Font(
font_path=None,
font_size=12,
colour=(0, 0, 0, 255)
)
# The problematic sentence
sentence = "This sentence has some really long words like supercalifragilisticexpialidocious that might need hyphenation."
# Test with the same constraints that were failing
line_width = 150
line_height = 25
words = sentence.split()
# Create lines and track all the text
lines = []
words_remaining = words.copy()
all_rendered_text = []
print(f"Original sentence: {sentence}")
print(f"Line width: {line_width}px")
print()
line_number = 1
while words_remaining:
print(f"Creating line {line_number}...")
# Create a new line
current_line = Line(
spacing=(3, 8),
origin=(0, (line_number-1) * line_height),
size=(line_width, line_height),
font=font_style,
halign=Alignment.LEFT
)
lines.append(current_line)
# Add words to current line until it's full
words_added_to_line = []
while words_remaining:
word = words_remaining[0]
print(f" Trying to add word: '{word}'")
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)
print(f" ✓ Added '{word}' to line {line_number}")
else:
# Word didn't fit, or only part of it fit
if result == word:
# Whole word didn't fit
print(f" ✗ Word '{word}' didn't fit, moving to next line")
break
else:
# Part of word fit, remainder is in result
words_added_to_line.append(word) # The original word
words_remaining[0] = result # Replace with remainder
print(f" ⚡ Part of '{word}' fit, remainder: '{result}'")
break
# Show what's on this line
line_words = [word.word.text for word in current_line.renderable_words]
line_text = ' '.join(line_words)
all_rendered_text.extend(line_words)
print(f" Line {line_number} contains: \"{line_text}\"")
print(f" Line {line_number} width usage: {current_line._current_width}/{line_width}px")
print()
# If no words were added to this line, we have a problem
if not line_words:
print(f"ERROR: No words could be added to line {line_number}")
break
line_number += 1
# Safety check to prevent infinite loops
if line_number > 10:
print("Safety break: too many lines")
break
# Check if all words were rendered
original_words = sentence.split()
rendered_text_combined = ' '.join(all_rendered_text)
print("="*60)
print("VERIFICATION")
print("="*60)
print(f"Original text: {sentence}")
print(f"Rendered text: {rendered_text_combined}")
print()
# Check for the problematic word
long_word = "supercalifragilisticexpialidocious"
if long_word in rendered_text_combined:
print(f"✓ SUCCESS: Long word '{long_word}' was rendered!")
elif "supercalifragilisticexpialidocious" in rendered_text_combined:
print(f"✓ SUCCESS: Long word was rendered (possibly hyphenated)!")
else:
# Check if parts of the word are there
found_parts = []
for rendered_word in all_rendered_text:
if long_word.startswith(rendered_word.replace('-', '')):
found_parts.append(rendered_word)
elif rendered_word.replace('-', '') in long_word:
found_parts.append(rendered_word)
if found_parts:
print(f"✓ PARTIAL SUCCESS: Found parts of long word: {found_parts}")
else:
print(f"✗ FAILURE: Long word '{long_word}' was not rendered at all!")
print(f"Total lines used: {len(lines)}")
# Create combined image showing all lines
total_height = len(lines) * line_height
combined_image = Image.new('RGBA', (line_width, total_height), (255, 255, 255, 255))
for i, line in enumerate(lines):
line_img = line.render()
y_pos = i * line_height
combined_image.paste(line_img, (0, y_pos), line_img)
# Add a 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)
# Save the result
output_filename = "test_long_word_fix.png"
combined_image.save(output_filename)
print(f"Result saved as: {output_filename}")
return len(lines), all_rendered_text
if __name__ == "__main__":
print("Testing long word fix for 'supercalifragilisticexpialidocious'...\n")
lines_used, rendered_words = test_supercalifragilisticexpialidocious()
print(f"\nTest completed!")
print(f"- Lines used: {lines_used}")
print(f"- Total words rendered: {len(rendered_words)}")
print(f"- Check test_long_word_fix.png for visual verification")
+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""
Test paragraph layout specifically to diagnose the line breaking issue
"""
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.renderable_words) if hasattr(line, 'renderable_words') 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 a long paragraph to see line breaking"""
print("\nTesting page with long paragraph...")
html_content = """
<html>
<body>
<h1>Test Long Paragraph</h1>
<p>This is a very long paragraph that should definitely wrap across multiple lines when rendered in the page. It contains many words and should demonstrate the line breaking functionality of the paragraph layout system. The paragraph layout should break this text into multiple lines based on the available width, and each line should be rendered separately on the page. This allows for proper text flow and readability in the final rendered output.</p>
<p>This is another paragraph to test multiple paragraph rendering and spacing between paragraphs.</p>
</body>
</html>
"""
# Create a page with narrower width to force wrapping
page = Page(size=(400, 600))
page.load_html_string(html_content)
print(f"✓ Page loaded with {len(page._children)} top-level elements")
# Check the structure of the page
for i, child in enumerate(page._children):
child_type = type(child).__name__
print(f" Element {i+1}: {child_type}")
# If it's a container (paragraph), check its children
if hasattr(child, '_children'):
print(f" Contains {len(child._children)} child elements")
for j, subchild in enumerate(child._children):
subchild_type = type(subchild).__name__
print(f" Sub-element {j+1}: {subchild_type}")
# Try to render the page
try:
image = page.render()
print(f"✓ Page rendered successfully: {image.size}")
# 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():
"""Compare simple text vs paragraph rendering"""
print("\nTesting simple text vs paragraph rendering...")
# Test 1: Simple HTML with short text
simple_html = "<p>Short text</p>"
page1 = Page(size=(400, 200))
page1.load_html_string(simple_html)
print(f"Simple text page has {len(page1._children)} children")
# Test 2: Complex HTML with long text
complex_html = """
<p>This is a much longer paragraph that should wrap across multiple lines and demonstrate the difference between simple text rendering and proper paragraph layout with line breaking functionality.</p>
"""
page2 = Page(size=(400, 200))
page2.load_html_string(complex_html)
print(f"Complex text page has {len(page2._children)} children")
# Render both
try:
img1 = page1.render()
img2 = page2.render()
img1.save("test_simple_text.png")
img2.save("test_complex_text.png")
print("✓ Saved both test images")
return True
except Exception as e:
print(f"✗ Error rendering: {e}")
return False
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()
+339
View File
@@ -0,0 +1,339 @@
#!/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")
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""
Test script to verify the text rendering fixes for cropping and line length issues.
"""
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...")
# 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)
# 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)}")
# Render the text
rendered = text.render()
print(f" Rendered size: {rendered.size}")
# 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),
font=font_style,
halign=alignment
)
# Add some words
words = ["Testing", "text", "alignment", "and", "spacing"]
for word in words:
line.add_word(word)
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")
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")