fix table in cell wrapping
Some checks failed
Python CI / test (3.10) (push) Successful in 2m17s
Python CI / test (3.13) (push) Has been cancelled
Python CI / test (3.12) (push) Has been cancelled

This commit is contained in:
Duncan Tourolle 2025-11-10 15:15:03 +01:00
parent 5afad2ca07
commit 8e720d4037
7 changed files with 499 additions and 15 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,312 @@
#!/usr/bin/env python3
"""
Table Text Wrapping Example
This example demonstrates the line wrapping functionality in table cells:
- Tables with long text that wraps across multiple lines
- Automatic word wrapping within cell boundaries
- Hyphenation support for long words
- Multiple paragraphs per cell
- Comparison of narrow vs. wide columns
Shows how the Line-based text layout system handles text overflow in tables.
"""
from pyWebLayout.io.readers.html_extraction import parse_html_string
from pyWebLayout.layout.document_layouter import DocumentLayouter
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.concrete.table import TableStyle
from pyWebLayout.concrete.page import Page
import sys
from pathlib import Path
from PIL import Image
# Add pyWebLayout to path
sys.path.insert(0, str(Path(__file__).parent.parent))
def create_narrow_columns_example():
"""Create a table with narrow columns to show aggressive wrapping."""
print(" - Narrow columns with text wrapping")
html = """
<table>
<thead>
<tr>
<th>Feature</th>
<th>Description</th>
<th>Benefits</th>
</tr>
</thead>
<tbody>
<tr>
<td>Automatic Line Wrapping</td>
<td>Text automatically wraps to fit within the available cell width, creating multiple lines as needed.</td>
<td>Improves readability and prevents horizontal overflow in tables.</td>
</tr>
<tr>
<td>Hyphenation Support</td>
<td>Long words are intelligently hyphenated using pyphen library or brute-force splitting when necessary.</td>
<td>Handles extraordinarily long words that wouldn't fit on a single line.</td>
</tr>
<tr>
<td>Multi-paragraph Cells</td>
<td>Each cell can contain multiple paragraphs or headings, all properly wrapped.</td>
<td>Allows rich content within table cells.</td>
</tr>
</tbody>
</table>
"""
return html, "Text Wrapping in Narrow Columns"
def create_mixed_content_example():
"""Create a table with both short and long content."""
print(" - Mixed content lengths")
html = """
<table>
<caption>Product Comparison</caption>
<thead>
<tr>
<th>Product</th>
<th>Short Description</th>
<th>Detailed Features</th>
</tr>
</thead>
<tbody>
<tr>
<td>Widget Pro</td>
<td>Premium</td>
<td>Advanced functionality with enterprise-grade reliability, comprehensive warranty coverage, and dedicated customer support available around the clock.</td>
</tr>
<tr>
<td>Widget Lite</td>
<td>Basic</td>
<td>Essential features for everyday use with straightforward operation and minimal learning curve.</td>
</tr>
<tr>
<td>Widget Max</td>
<td>Ultimate</td>
<td>Everything from Widget Pro plus additional customization options, API integration capabilities, and advanced analytics dashboard.</td>
</tr>
</tbody>
</table>
"""
return html, "Mixed Short and Long Content"
def create_technical_documentation_example():
"""Create a table like technical documentation."""
print(" - Technical documentation style")
html = """
<table>
<thead>
<tr>
<th>API Method</th>
<th>Parameters</th>
<th>Description</th>
<th>Return Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>render_table()</td>
<td>table, origin, width, draw, style</td>
<td>Renders a table with automatic text wrapping in cells. Uses the Line class for intelligent word placement and hyphenation.</td>
<td>Rendered table with calculated height and width properties.</td>
</tr>
<tr>
<td>add_word()</td>
<td>word, pretext</td>
<td>Attempts to add a word to the current line. If it doesn't fit, tries hyphenation strategies including pyphen and brute-force splitting.</td>
<td>Tuple of (success, overflow_text) indicating whether word was added and any remaining text.</td>
</tr>
<tr>
<td>calculate_spacing()</td>
<td>text_objects, width, min_spacing, max_spacing</td>
<td>Determines optimal spacing between words to achieve proper justification within the specified constraints.</td>
<td>Calculated spacing value and position offset for alignment.</td>
</tr>
</tbody>
</table>
"""
return html, "Technical Documentation Table"
def create_news_article_example():
"""Create a table with article-style content."""
print(" - News article layout")
html = """
<table>
<thead>
<tr>
<th>Date</th>
<th>Headline</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
<tr>
<td>2024-01-15</td>
<td>New Text Wrapping Feature</td>
<td>PyWebLayout now supports automatic line wrapping in table cells, bringing sophisticated text layout capabilities to table rendering. The implementation leverages the existing Line class infrastructure.</td>
</tr>
<tr>
<td>2024-01-10</td>
<td>Hyphenation Improvements</td>
<td>Enhanced hyphenation algorithms now include both dictionary-based pyphen hyphenation and intelligent brute-force splitting for edge cases.</td>
</tr>
<tr>
<td>2024-01-05</td>
<td>Performance Optimization</td>
<td>Table rendering performance improved through better caching and reduced font object creation overhead.</td>
</tr>
</tbody>
</table>
"""
return html, "News Article Layout"
def render_table_example(html, title, style_variant=0):
"""Render a single table example."""
from pyWebLayout.style import Font
from pyWebLayout.abstract.block import Table
# Parse HTML
base_font = Font(font_size=12)
blocks = parse_html_string(html, base_font=base_font)
# Find the table block
table = None
for block in blocks:
if isinstance(block, Table):
table = block
break
if not table:
print(f" Warning: No table found in {title}")
return None
# Create page style
page_style = PageStyle(
padding=(20, 20, 20, 20),
background_color=(255, 255, 255)
)
# Create page
page_size = (900, 600)
page = Page(size=page_size, style=page_style)
# Create table style variants
table_styles = [
# Default style
TableStyle(
border_width=1,
border_color=(0, 0, 0),
cell_padding=(8, 8, 8, 8),
header_bg_color=(240, 240, 240),
cell_bg_color=(255, 255, 255)
),
# Blue header style
TableStyle(
border_width=2,
border_color=(70, 130, 180),
cell_padding=(10, 10, 10, 10),
header_bg_color=(176, 196, 222),
cell_bg_color=(245, 250, 255)
),
# Minimal style
TableStyle(
border_width=1,
border_color=(200, 200, 200),
cell_padding=(6, 6, 6, 6),
header_bg_color=(250, 250, 250),
cell_bg_color=(255, 255, 255)
),
]
table_style = table_styles[style_variant % len(table_styles)]
# Create layouter and render table
layouter = DocumentLayouter(page)
layouter.layout_table(table, style=table_style)
# Get the rendered canvas
_ = page.draw # Ensure canvas exists
img = page._canvas
return img
def combine_examples(examples):
"""Combine multiple example images into one."""
images = []
titles = []
for html, title in examples:
img = render_table_example(html, title)
if img:
images.append(img)
titles.append(title)
if not images:
return None
# Calculate combined image size
max_width = max(img.width for img in images)
total_height = sum(img.height for img in images) + 40 * len(images) # Extra space between images
# Create combined image
combined = Image.new('RGB', (max_width, total_height), color=(255, 255, 255))
# Paste images
y_offset = 20
for img in images:
combined.paste(img, (0, y_offset))
y_offset += img.height + 40
return combined
def main():
"""Run the table text wrapping example."""
print("\nTable Text Wrapping Example")
print("=" * 50)
# Create examples
print("\n Creating table examples...")
examples = [
create_narrow_columns_example(),
create_mixed_content_example(),
create_technical_documentation_example(),
create_news_article_example(),
]
print("\n Rendering table examples...")
combined_image = combine_examples(examples)
if combined_image:
# Save the output
output_dir = Path(__file__).parent.parent / 'docs' / 'images'
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / 'example_11_table_text_wrapping.png'
combined_image.save(str(output_path))
print("\n✓ Example completed!")
print(f" Output saved to: {output_path}")
print(f" Image size: {combined_image.width}x{combined_image.height} pixels")
print(f" Created {len(examples)} table examples with text wrapping")
else:
print("\n✗ Failed to generate examples")
if __name__ == '__main__':
main()

View File

@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""
Simple Table Text Wrapping Example
A minimal example showing text wrapping in table cells.
Perfect for quick testing and demonstration.
"""
import sys
from pathlib import Path
# Add pyWebLayout to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from pyWebLayout.io.readers.html_extraction import parse_html_string
from pyWebLayout.layout.document_layouter import DocumentLayouter
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.style import Font
from pyWebLayout.concrete.table import TableStyle
from pyWebLayout.concrete.page import Page
from pyWebLayout.abstract.block import Table
def main():
"""Create a simple table with text wrapping."""
print("\nSimple Table Text Wrapping Example")
print("=" * 50)
# HTML with a table containing long text
html = """
<table>
<caption>Text Wrapping Demonstration</caption>
<thead>
<tr>
<th>Column 1</th>
<th>Column 2</th>
<th>Column 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>This is a cell with quite a lot of text that will need to wrap across multiple lines.</td>
<td>Short text</td>
<td>Another cell with enough content to demonstrate the automatic line wrapping functionality.</td>
</tr>
<tr>
<td>Cell A</td>
<td>This middle cell contains a paragraph with several words that should wrap nicely within the available space.</td>
<td>Cell C</td>
</tr>
<tr>
<td>Words like supercalifragilisticexpialidocious might need hyphenation.</td>
<td>Normal text</td>
<td>The wrapping algorithm handles both regular word wrapping and hyphenation seamlessly.</td>
</tr>
</tbody>
</table>
"""
print("\n Parsing HTML and creating table...")
# Parse HTML
base_font = Font(font_size=12)
blocks = parse_html_string(html, base_font=base_font)
# Find table
table = None
for block in blocks:
if isinstance(block, Table):
table = block
break
if not table:
print(" ✗ No table found!")
return
print(" ✓ Table parsed successfully")
# Create page
page_style = PageStyle(
padding=(30, 30, 30, 30),
background_color=(255, 255, 255)
)
page = Page(size=(800, 600), style=page_style)
# Create table style
table_style = TableStyle(
border_width=2,
border_color=(70, 130, 180),
cell_padding=(10, 10, 10, 10),
header_bg_color=(176, 196, 222),
cell_bg_color=(245, 250, 255)
)
print(" Rendering table with text wrapping...")
# Layout and render
layouter = DocumentLayouter(page)
layouter.layout_table(table, style=table_style)
# Get rendered image
_ = page.draw
img = page._canvas
# Save output
output_path = Path(__file__).parent.parent / 'docs' / 'images' / 'example_11b_simple_wrapping.png'
output_path.parent.mkdir(parents=True, exist_ok=True)
img.save(str(output_path))
print(f"\n✓ Example completed!")
print(f" Output saved to: {output_path}")
print(f" Image size: {img.width}x{img.height} pixels")
print(f"\n The table demonstrates:")
print(f" • Automatic line wrapping in cells")
print(f" • Proper word spacing and alignment")
print(f" • Hyphenation for very long words")
print(f" • Multi-line text within cell boundaries")
if __name__ == '__main__':
main()

View File

@ -174,6 +174,31 @@ Demonstrates all 14 FormFieldType variations:
![Comprehensive Forms Example](../docs/images/example_10_forms.png)
### 11. Table Text Wrapping (NEW) ✅
**`11_table_text_wrapping_demo.py`** - Automatic line wrapping in table cells
```bash
python 11_table_text_wrapping_demo.py
```
**Simple Version:** `11b_simple_table_wrapping.py` - Quick demonstration
Demonstrates:
- **Automatic line wrapping** - Text wraps across multiple lines within cells
- **Word hyphenation** - Long words are intelligently hyphenated
- **Narrow columns** - Aggressive wrapping for tight spaces
- **Mixed content** - Both short and long text in the same table
- **Technical documentation** - API reference style tables
- **News layouts** - Article-style table content
**Implementation:** Uses the Line class from `pyWebLayout.concrete.text` with:
- Word-by-word fitting with intelligent spacing
- Pyphen-based dictionary hyphenation
- Brute-force splitting for edge cases
- Proper baseline alignment and metrics
![Table Text Wrapping Example](../docs/images/example_11_table_text_wrapping.png)
---
## Advanced Examples
@ -207,6 +232,8 @@ python 06_functional_elements_demo.py
python 08_pagination_demo.py # Multi-page documents
python 09_link_navigation_demo.py # All link types
python 10_forms_demo.py # All form field types
python 11_table_text_wrapping_demo.py # Table cell text wrapping
python 11b_simple_table_wrapping.py # Simple wrapping demo
```
Output images are saved to the `docs/images/` directory.

View File

@ -145,17 +145,36 @@ class TableCellRenderer(Box):
block, x, current_y, width, height - (current_y - y))
elif isinstance(block, (Paragraph, Heading)):
# Get words from the block
from pyWebLayout.abstract.inline import Word as AbstractWord
word_items = block.words() if callable(block.words) else block.words
words = list(word_items)
if not words:
continue
# Create new Word objects with the table cell's font
# The words from the paragraph may have AbstractStyle, but we need Font objects
wrapped_words = []
for word_item in words:
# Handle word tuples (index, word_obj)
if isinstance(word_item, tuple) and len(word_item) >= 2:
word_obj = word_item[1]
else:
word_obj = word_item
# Extract text from the word
word_text = word_obj.text if hasattr(word_obj, 'text') else str(word_obj)
# Create a new Word with the cell's Font
new_word = AbstractWord(word_text, font)
wrapped_words.append(new_word)
# Layout words using Line objects with wrapping
word_index = 0
pretext = None
while word_index < len(words):
while word_index < len(wrapped_words):
# Check if we have space for another line
if current_y + ascent + descent > y + available_height:
break # No more space in cell
@ -172,29 +191,29 @@ class TableCellRenderer(Box):
# Add words to this line until it's full
line_has_content = False
while word_index < len(words):
word = words[word_index]
# Handle word tuples (index, word_obj)
if isinstance(word, tuple) and len(word) >= 2:
word_obj = word[1]
else:
word_obj = word
while word_index < len(wrapped_words):
word = wrapped_words[word_index]
# Try to add word to line
success, overflow = line.add_word(word_obj, pretext)
success, overflow = line.add_word(word, pretext)
pretext = None # Clear pretext after use
if success:
line_has_content = True
if overflow:
# Word was hyphenated, carry over to next line
# DON'T increment word_index - we need to add the overflow
# to the next line with the same word
pretext = overflow
break # Move to next line
else:
# Word fit completely, move to next word
word_index += 1
else:
# Word doesn't fit on this line
if not line_has_content:
# Even first word doesn't fit, force it anyway to avoid infinite loop
# Even first word doesn't fit, force it anyway and advance
# This prevents infinite loops with words that truly can't fit
word_index += 1
break
@ -454,11 +473,16 @@ class TableRenderer(Box):
column_widths = [column_width] * num_columns
# Calculate row heights
header_height = 35 if any(1 for section,
# Minimum height needs to account for:
# - Font size (12px) + line height (4px) = 16px per line
# - Cell padding (varies, but typically 10-20px top+bottom)
# - At least 2 lines of text for wrapping
# So minimum should be ~60px to accommodate padding + 2 lines
header_height = 60 if any(1 for section,
_ in all_rows if section == "header") else 0
# Check if any body rows contain images - if so, use larger height
body_height = 30
body_height = 60 # Increased from 30 to allow for text wrapping
for section, row in all_rows:
if section == "body":
for cell in row.cells():
@ -468,7 +492,7 @@ class TableRenderer(Box):
body_height = max(body_height, 120)
break
footer_height = 30 if any(1 for section,
footer_height = 60 if any(1 for section,
_ in all_rows if section == "footer") else 0
row_heights = {