New alignement handlers
Some checks failed
Python CI / test (push) Failing after 4m28s

This commit is contained in:
Duncan Tourolle 2025-06-08 14:08:29 +02:00
parent 9baafe85bf
commit 8d892bfe28
5 changed files with 636 additions and 232 deletions

233
ARCHITECTURE.md Normal file
View File

@ -0,0 +1,233 @@
# pyWebLayout Architecture: Abstract vs Concrete
This document explains the fundamental architectural separation between **Abstract** and **Concrete** layers in the pyWebLayout library.
## Overview
The pyWebLayout library follows a clear separation between two distinct layers:
- **Abstract Layer**: Represents the logical structure and content of documents (HTML/EPUB text)
- **Concrete Layer**: Handles the spatial rendering and visual representation of content
This separation provides flexibility, testability, and clean separation of concerns.
## Abstract Layer (`pyWebLayout/abstract/`)
The Abstract layer deals with the **logical structure** of documents without concerning itself with how content will be visually rendered.
### Key Components
#### `abstract/block.py`
- `Block`: Base class for all block-level content
- `Paragraph`: Represents a logical paragraph containing words
- `Heading`: Represents headings with semantic levels (H1-H6)
- `HList`: Represents ordered/unordered lists
- `Image`: Represents image references
#### `abstract/inline.py`
- `Word`: Represents individual words with text content and styling information
- Contains methods for hyphenation and text manipulation
- Does **not** handle rendering or spatial layout
#### `abstract/document.py`
- `Document`: Container for the overall document structure
- `Chapter`: Logical grouping of blocks (for books/long documents)
### Characteristics of Abstract Classes
1. **Content-focused**: Store text, structure, and semantic meaning
2. **Layout-agnostic**: No knowledge of fonts, pixels, or rendering
3. **Reusable**: Same content can be rendered in different formats/sizes
4. **Serializable**: Can be saved/loaded without rendering context
### Example: Abstract Word
```python
# An Abstract Word knows its text content and semantic properties
word = Word("supercalifragilisticexpialidocious", font_style)
word.hyphenate() # Logical operation - finds break points
parts = word.get_hyphenated_parts() # Returns ["super-", "cali-", "fragi-", ...]
```
## Concrete Layer (`pyWebLayout/concrete/`)
The Concrete layer handles the **spatial representation** and actual rendering of content.
### Key Components
#### `concrete/text.py`
- `Text`: Renders a specific text fragment with precise positioning
- `Line`: Manages a line of `Text` objects with spacing and alignment
- Handles actual pixel measurements, font rendering, and positioning
#### `concrete/page.py`
- `Page`: Top-level container for rendered content
- `Container`: Layout manager for organizing renderable objects
- Handles spatial layout, pagination, and visual composition
#### `concrete/box.py`
- `Box`: Base class for all spatially-aware renderable objects
- Provides positioning, sizing, and rendering capabilities
### Characteristics of Concrete Classes
1. **Rendering-focused**: Handle pixels, fonts, images, and visual output
2. **Spatially-aware**: Know exact positions, sizes, and layout constraints
3. **Implementation-specific**: Tied to specific rendering technologies (PIL, etc.)
4. **Non-portable**: Rendering results are tied to specific display contexts
### Example: Concrete Text
```python
# A Concrete Text object handles actual rendering
text = Text("super-", font) # Specific text fragment
text._calculate_dimensions() # Computes exact pixel size
image = text.render() # Produces actual visual output
```
## The Transformation Process
The architecture involves a clear transformation from Abstract to Concrete:
```
Abstract Document
[Parser Layer]
Abstract Blocks (Paragraph, Heading, etc.)
[Layout Engine]
Concrete Objects (Text, Line, Page)
[Rendering Engine]
Visual Output (Images, PDF, etc.)
```
### Example Transformation
```python
# 1. Abstract content
paragraph = Paragraph()
paragraph.add_word(Word("This", font))
paragraph.add_word(Word("is", font))
paragraph.add_word(Word("a", font))
paragraph.add_word(Word("test", font))
# 2. Layout transformation
layout = ParagraphLayout(line_width=200, line_height=20)
lines = layout.layout_paragraph(paragraph) # Returns List[Line]
# 3. Each Line contains concrete Text objects
for line in lines:
for text_obj in line.text_objects: # List[Text]
print(f"Text: '{text_obj.text}' at position {text_obj._origin}")
```
## Key Architectural Principles
### 1. **Single Responsibility**
- Abstract classes: Handle content and structure
- Concrete classes: Handle rendering and layout
### 2. **Separation of Concerns**
- Text parsing/processing ≠ Text rendering
- Document structure ≠ Page layout
- Content semantics ≠ Visual presentation
### 3. **Immutable Abstract Content**
- Abstract content remains unchanged during rendering
- Multiple concrete representations can be generated from same abstract content
- Enables pagination, different formats, responsive layouts
### 4. **One-to-Many Relationships**
- One Abstract Word → Multiple Concrete Text objects (hyphenation)
- One Abstract Paragraph → Multiple Concrete Lines
- One Abstract Document → Multiple Concrete Pages
## Common Anti-Patterns to Avoid
### ❌ **Mixing Concerns**
```python
# WRONG: Abstract class knowing about pixels
class Word:
def __init__(self, text):
self.text = text
self.rendered_width = None # ❌ Concrete concern in abstract class
```
### ❌ **renderable_words Concept**
```python
# WRONG: Confusing abstract and concrete
line.renderable_words # ❌ This suggests Words are renderable
# Words are abstract - only Text objects render
```
### ✅ **Correct Separation**
```python
# CORRECT: Clear separation
abstract_word = Word("test") # Abstract content
concrete_text = Text("test", font) # Concrete rendering
line.text_objects.append(concrete_text) # Concrete objects in concrete container
```
## Benefits of This Architecture
### 1. **Flexibility**
- Same content can be rendered at different sizes
- Multiple output formats from single source
- Easy to implement responsive design
### 2. **Testability**
- Abstract logic can be tested without rendering
- Layout algorithms can be tested independently
- Visual rendering can be mocked
### 3. **Performance**
- Abstract content can be cached and reused
- Layout can be computed once for multiple renderings
- Incremental updates possible
### 4. **Maintainability**
- Clear boundaries between text processing and rendering
- Changes to rendering don't affect content parsing
- Easy to swap rendering backends
## File Organization
```
pyWebLayout/
├── abstract/ # Content and structure
│ ├── block.py # Document blocks (Paragraph, Heading, etc.)
│ ├── inline.py # Inline content (Word, etc.)
│ ├── document.py # Document structure
│ └── functional.py # Links, buttons, etc.
├── concrete/ # Rendering and layout
│ ├── text.py # Text and Line rendering
│ ├── page.py # Page layout and containers
│ ├── box.py # Base rendering classes
│ ├── image.py # Image rendering
│ └── functional.py # Interactive elements
├── typesetting/ # Layout algorithms
│ ├── paragraph_layout.py # Abstract → Concrete transformation
│ ├── flow.py # Text flow management
│ └── pagination.py # Page breaking logic
└── style/ # Styling and formatting
├── fonts.py # Font management
├── layout.py # Layout constants
└── alignment.py # Alignment enums
```
## Conclusion
The Abstract/Concrete separation is fundamental to pyWebLayout's design. It ensures clean separation between content processing and visual rendering, enabling flexible, maintainable, and testable document processing pipelines.
**Remember**:
- **Abstract** = What to display (content, structure, semantics)
- **Concrete** = How to display it (pixels, fonts, positioning, rendering)
This architecture enables the library to handle complex document layouts while maintaining clear, understandable code organization.

View File

@ -5,8 +5,146 @@ from pyWebLayout.style.layout import Alignment
from pyWebLayout.style import Font, FontStyle, FontWeight, TextDecoration
from pyWebLayout.abstract.inline import Word
from PIL import Image, ImageDraw, ImageFont
from typing import Tuple, Union, List, Optional
from typing import Tuple, Union, List, Optional, Protocol
import numpy as np
from abc import ABC, abstractmethod
class AlignmentHandler(ABC):
"""
Abstract base class for text alignment handlers.
Each handler implements a specific alignment strategy.
"""
@abstractmethod
def calculate_spacing_and_position(self, text_objects: List['Text'],
available_width: int, min_spacing: int,
max_spacing: int) -> Tuple[int, int]:
"""
Calculate the spacing between words and starting position for the line.
Args:
text_objects: List of Text objects in the line
available_width: Total width available for the line
min_spacing: Minimum spacing between words
max_spacing: Maximum spacing between words
Returns:
Tuple of (spacing_between_words, starting_x_position)
"""
pass
@abstractmethod
def should_try_hyphenation(self, text_objects: List['Text'], word_width: int,
available_width: int, spacing: int) -> bool:
"""
Determine if hyphenation should be attempted for better spacing.
Args:
text_objects: Current text objects in the line
word_width: Width of the word trying to be added
available_width: Available width remaining
spacing: Current minimum spacing being used
Returns:
True if hyphenation should be attempted
"""
pass
class LeftAlignmentHandler(AlignmentHandler):
"""Handler for left-aligned text."""
def calculate_spacing_and_position(self, text_objects: List['Text'],
available_width: int, min_spacing: int,
max_spacing: int) -> Tuple[int, int]:
"""Left alignment uses minimum spacing and starts at position 0."""
return min_spacing, 0
def should_try_hyphenation(self, text_objects: List['Text'], word_width: int,
available_width: int, spacing: int) -> bool:
"""For left alignment, hyphenate only if the word doesn't fit."""
return word_width > available_width
class CenterRightAlignmentHandler(AlignmentHandler):
"""Handler for center and right-aligned text."""
def __init__(self, alignment: Alignment):
self._alignment = alignment
def calculate_spacing_and_position(self, text_objects: List['Text'],
available_width: int, min_spacing: int,
max_spacing: int) -> Tuple[int, int]:
"""Center/right alignment uses minimum spacing with calculated start position."""
if not text_objects:
return min_spacing, 0
total_text_width = sum(text_obj.width for text_obj in text_objects)
num_spaces = len(text_objects) - 1
spacing = min_spacing
if self._alignment == Alignment.RIGHT:
x_pos = available_width - (total_text_width + spacing * num_spaces)
else: # CENTER
x_pos = (available_width - (total_text_width + spacing * num_spaces)) // 2
return spacing, max(0, x_pos)
def should_try_hyphenation(self, text_objects: List['Text'], word_width: int,
available_width: int, spacing: int) -> bool:
"""For center/right alignment, hyphenate only if the word doesn't fit."""
return word_width > available_width
class JustifyAlignmentHandler(AlignmentHandler):
"""Handler for justified text with optimal spacing."""
def calculate_spacing_and_position(self, text_objects: List['Text'],
available_width: int, min_spacing: int,
max_spacing: int) -> Tuple[int, int]:
"""Justified alignment distributes space evenly between words."""
if not text_objects or len(text_objects) == 1:
# Single word or empty line - use left alignment
return min_spacing, 0
total_text_width = sum(text_obj.width for text_obj in text_objects)
num_spaces = len(text_objects) - 1
available_space = available_width - total_text_width
if num_spaces > 0:
spacing = available_space // num_spaces
# Ensure spacing is within acceptable bounds
spacing = max(min_spacing, min(max_spacing, spacing))
else:
spacing = min_spacing
return spacing, 0
def should_try_hyphenation(self, text_objects: List['Text'], word_width: int,
available_width: int, spacing: int) -> bool:
"""
For justified text, consider hyphenation if it would improve spacing quality.
This includes cases where the word fits but would create poor spacing.
"""
if word_width > available_width:
return True
# Calculate what the spacing would be with this word added
if not text_objects:
return False
total_text_width = sum(text_obj.width for text_obj in text_objects) + word_width
num_spaces = len(text_objects) # Will be len(text_objects) after adding the word
available_space = available_width - total_text_width
if num_spaces > 0:
projected_spacing = available_space // num_spaces
# If spacing would be too large, consider hyphenation for better distribution
max_acceptable_spacing = spacing * 2 # Allow up to 2x normal spacing
return projected_spacing > max_acceptable_spacing
return False
class Text(Renderable, Queriable):
@ -228,6 +366,26 @@ class Line(Box):
self._previous = previous
self._next = None
# Create the appropriate alignment handler
self._alignment_handler = self._create_alignment_handler(halign)
def _create_alignment_handler(self, alignment: Alignment) -> AlignmentHandler:
"""
Create the appropriate alignment handler based on the alignment type.
Args:
alignment: The alignment type
Returns:
The appropriate alignment handler instance
"""
if alignment == Alignment.LEFT:
return LeftAlignmentHandler()
elif alignment == Alignment.JUSTIFY:
return JustifyAlignmentHandler()
else: # CENTER or RIGHT
return CenterRightAlignmentHandler(alignment)
@property
def text_objects(self) -> List[Text]:
"""Get the list of Text objects in this line"""
@ -286,7 +444,7 @@ class Line(Box):
def add_word(self, text: str, font: Optional[Font] = None) -> Union[None, str]:
"""
Add a word to this line as a Text object.
Add a word to this line as a Text object using intelligent hyphenation decisions.
Args:
text: The text content of the word
@ -307,65 +465,105 @@ class Line(Box):
spacing_needed = min_spacing if self._text_objects else 0
# Add a small margin to prevent edge cases where words appear to fit but get cropped
# This addresses the issue of lines appearing too short
safety_margin = max(1, int(font.font_size * 0.05)) # 5% of font size as safety margin
# Check if word fits in the line with safety margin
available_width = self._size[0] - self._current_width - spacing_needed - safety_margin
if word_width <= available_width:
# Word fits - add it to the line
# Use the alignment handler to decide if hyphenation should be attempted
should_hyphenate = self._alignment_handler.should_try_hyphenation(
self._text_objects, word_width, available_width, min_spacing)
if word_width <= available_width and not should_hyphenate:
# Word fits and alignment handler doesn't suggest hyphenation - add it to the line
text_obj.add_to_line(self)
self._text_objects.append(text_obj)
self._current_width += spacing_needed + word_width
return None
elif should_hyphenate:
# Try hyphenation for better spacing
return self._try_hyphenation_or_fit(text, font, available_width, spacing_needed, safety_margin)
else:
# Word doesn't fit - try to hyphenate
abstract_word = Word(text, font)
if abstract_word.hyphenate():
# Get the first hyphenated part
first_part_text = abstract_word.get_hyphenated_part(0)
first_part_obj = Text(first_part_text, font)
# Check if first part fits (with safety margin)
if first_part_obj.width <= available_width:
# First part fits - add it to the line
first_part_obj.add_to_line(self)
self._text_objects.append(first_part_obj)
self._current_width += spacing_needed + first_part_obj.width
# Return the remaining part(s)
if abstract_word.get_hyphenated_part_count() > 1:
return abstract_word.get_hyphenated_part(1)
else:
return None
else:
# Even the first hyphenated part doesn't fit
if self._text_objects:
# Line already has words, can't fit this one at all
return text
else:
# Empty line - must fit something or infinite loop
first_part_text = abstract_word.get_hyphenated_part(0)
# If the first part is nearly as long as the original word, this is likely a test
if len(first_part_text.rstrip('-')) >= len(text) * 0.8: # 80% of original length
# This is likely a mocked test scenario - return original word unchanged
return text
else:
# Real scenario with proper hyphenation - try force fitting
return self._force_fit_long_word(text, font, available_width + safety_margin)
# Word doesn't fit and no hyphenation recommended
if self._text_objects:
# Line already has words, can't fit this one at all
return text
else:
# Word cannot be hyphenated
if self._text_objects:
# Line already has words, can't fit this unhyphenatable word
return text
# Empty line with word that's too long - force fit
return self._force_fit_long_word(text, font, available_width + safety_margin)
def _try_hyphenation_or_fit(self, text: str, font: Font, available_width: int,
spacing_needed: int, safety_margin: int) -> Union[None, str]:
"""
Try different hyphenation options and choose the best one for spacing.
Args:
text: The text to hyphenate
font: The font to use
available_width: Available width for the word
spacing_needed: Spacing needed before the word
safety_margin: Safety margin for fitting
Returns:
None if the word fits, or remaining text if it doesn't fit
"""
abstract_word = Word(text, font)
if abstract_word.hyphenate():
# Try different hyphenation breakpoints to find the best spacing
best_option = None
best_spacing_quality = float('inf') # Lower is better
for i in range(abstract_word.get_hyphenated_part_count()):
part_text = abstract_word.get_hyphenated_part(i)
part_obj = Text(part_text, font)
if part_obj.width <= available_width:
# Calculate spacing quality with this hyphenation
temp_text_objects = self._text_objects + [part_obj]
spacing, _ = self._alignment_handler.calculate_spacing_and_position(
temp_text_objects, self._size[0], self._spacing[0], self._spacing[1])
# Quality metric: prefer spacing closer to minimum, avoid extremes
spacing_quality = abs(spacing - self._spacing[0])
if spacing_quality < best_spacing_quality:
best_spacing_quality = spacing_quality
best_option = (i, part_obj, part_text)
else:
# Empty line with unhyphenatable word that's too long
# Force-fit as many characters as possible
# Can't fit this part, no point trying longer parts
break
if best_option:
# Use the best hyphenation option
i, part_obj, part_text = best_option
part_obj.add_to_line(self)
self._text_objects.append(part_obj)
self._current_width += spacing_needed + part_obj.width
# Return remaining part(s) if any
if i + 1 < abstract_word.get_hyphenated_part_count():
return abstract_word.get_hyphenated_part(i + 1)
else:
return None
else:
# No hyphenation part fits
if self._text_objects:
return text # Line already has words, can't fit this one
else:
# Empty line - must fit something
return self._force_fit_long_word(text, font, available_width + safety_margin)
else:
# Word cannot be hyphenated
if self._text_objects:
return text # Line already has words, can't fit this unhyphenatable word
else:
# Empty line with unhyphenatable word that's too long
return self._force_fit_long_word(text, font, available_width + safety_margin)
def render(self) -> Image.Image:
"""
Render the line with all its text objects.
Render the line with all its text objects using the alignment handler system.
Returns:
A PIL Image containing the rendered line
@ -377,30 +575,9 @@ class Line(Box):
if not self._text_objects:
return canvas
# Calculate total width of text objects
total_text_width = sum(text_obj.width for text_obj in self._text_objects)
# Calculate spacing based on alignment and available space
available_space = self._size[0] - total_text_width
num_spaces = len(self._text_objects) - 1
if num_spaces > 0:
if self._halign == Alignment.JUSTIFY:
# For justified text, distribute space evenly between words
spacing = available_space // num_spaces
else:
# Use minimum spacing for other alignments
spacing = self._spacing[0]
else:
spacing = 0
# Calculate starting x position based on alignment
if self._halign == Alignment.LEFT:
x_pos = 0
elif self._halign == Alignment.RIGHT:
x_pos = self._size[0] - (total_text_width + spacing * num_spaces)
else: # CENTER
x_pos = (self._size[0] - (total_text_width + spacing * num_spaces)) // 2
# Use the alignment handler to calculate spacing and position
spacing, x_pos = self._alignment_handler.calculate_spacing_and_position(
self._text_objects, self._size[0], self._spacing[0], self._spacing[1])
# Vertical alignment - center text vertically in the line
y_pos = (self._size[1] - max(text_obj.height for text_obj in self._text_objects)) // 2

38
simple_test.py Normal file
View File

@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""
Simple test to check if the refactored text alignment system works.
"""
try:
from pyWebLayout.concrete.text import Line, Text, AlignmentHandler, LeftAlignmentHandler
from pyWebLayout.style.layout import Alignment
from pyWebLayout.style import Font
print("✓ All imports successful")
# Create a simple font
font = Font()
print("✓ Font created")
# Create a line with left alignment
line = Line((5, 20), (0, 0), (200, 30), font, halign=Alignment.LEFT)
print("✓ Line created with left alignment")
print(f" Handler type: {type(line._alignment_handler).__name__}")
# Try adding a word
result = line.add_word("Hello")
print(f"✓ Added word 'Hello', result: {result}")
print(f" Line now has {len(line.text_objects)} text objects")
# Test different alignments
alignments = [Alignment.LEFT, Alignment.CENTER, Alignment.RIGHT, Alignment.JUSTIFY]
for align in alignments:
test_line = Line((5, 20), (0, 0), (200, 30), font, halign=align)
handler_name = type(test_line._alignment_handler).__name__
print(f"{align.name} alignment uses {handler_name}")
print("✓ All tests passed!")
except Exception as e:
print(f"✗ Error: {e}")
import traceback
traceback.print_exc()

118
test_alignment_handlers.py Normal file
View File

@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""
Test script to verify the alignment handler system is working correctly.
"""
from pyWebLayout.concrete.text import Line, Text, LeftAlignmentHandler, CenterRightAlignmentHandler, JustifyAlignmentHandler
from pyWebLayout.style.layout import Alignment
from pyWebLayout.style import Font
import numpy as np
def test_alignment_handlers():
"""Test all three alignment handlers."""
print("Testing Alignment Handler System")
print("=" * 40)
# Create a basic font for testing
font = Font()
# Test data
test_words = ["This", "is", "a", "test", "sentence"]
line_width = 300
line_height = 30
spacing = (5, 20) # min_spacing, max_spacing
# Test Left Alignment
print("\n1. Testing Left Alignment:")
left_line = Line(spacing, (0, 0), (line_width, line_height), font, halign=Alignment.LEFT)
for word in test_words:
result = left_line.add_word(word)
if result:
print(f" Word '{word}' didn't fit, remainder: '{result}'")
break
else:
print(f" Added word: '{word}'")
print(f" Final line has {len(left_line.text_objects)} words")
print(f" Handler type: {type(left_line._alignment_handler).__name__}")
# Test Center Alignment
print("\n2. Testing Center Alignment:")
center_line = Line(spacing, (0, 0), (line_width, line_height), font, halign=Alignment.CENTER)
for word in test_words:
result = center_line.add_word(word)
if result:
print(f" Word '{word}' didn't fit, remainder: '{result}'")
break
else:
print(f" Added word: '{word}'")
print(f" Final line has {len(center_line.text_objects)} words")
print(f" Handler type: {type(center_line._alignment_handler).__name__}")
# Test Right Alignment
print("\n3. Testing Right Alignment:")
right_line = Line(spacing, (0, 0), (line_width, line_height), font, halign=Alignment.RIGHT)
for word in test_words:
result = right_line.add_word(word)
if result:
print(f" Word '{word}' didn't fit, remainder: '{result}'")
break
else:
print(f" Added word: '{word}'")
print(f" Final line has {len(right_line.text_objects)} words")
print(f" Handler type: {type(right_line._alignment_handler).__name__}")
# Test Justify Alignment
print("\n4. Testing Justify Alignment:")
justify_line = Line(spacing, (0, 0), (line_width, line_height), font, halign=Alignment.JUSTIFY)
for word in test_words:
result = justify_line.add_word(word)
if result:
print(f" Word '{word}' didn't fit, remainder: '{result}'")
break
else:
print(f" Added word: '{word}'")
print(f" Final line has {len(justify_line.text_objects)} words")
print(f" Handler type: {type(justify_line._alignment_handler).__name__}")
# Test spacing and position calculations
print("\n5. Testing Handler Calculations:")
# Create sample text objects
text_objects = [Text(word, font) for word in ["Hello", "World"]]
handlers = [
("Left", LeftAlignmentHandler()),
("Center", CenterRightAlignmentHandler(Alignment.CENTER)),
("Right", CenterRightAlignmentHandler(Alignment.RIGHT)),
("Justify", JustifyAlignmentHandler())
]
for name, handler in handlers:
spacing_calc, position = handler.calculate_spacing_and_position(
text_objects, line_width, spacing[0], spacing[1])
print(f" {name}: spacing={spacing_calc}, position={position}")
print("\n6. Testing Hyphenation Decisions:")
# Test hyphenation decisions for different alignments
test_word_width = 50
available_width = 40 # Word doesn't fit
for name, handler in handlers:
should_hyphenate = handler.should_try_hyphenation(
text_objects, test_word_width, available_width, spacing[0])
print(f" {name}: should_hyphenate={should_hyphenate}")
print("\nAlignment Handler Test Completed Successfully!")
return True
if __name__ == "__main__":
test_alignment_handlers()

View File

@ -1,162 +0,0 @@
#!/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.text for word in current_line.text_objects]
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")