Compare commits

..

No commits in common. "8e720d40370f5079ffee36b454febf711064942e" and "303179865d27fd75cdad0ae4e4e083a4de231c08" have entirely different histories.

7 changed files with 37 additions and 579 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@ -1,312 +0,0 @@
#!/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

@ -1,121 +0,0 @@
#!/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,31 +174,6 @@ 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
@ -232,8 +207,6 @@ 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

@ -108,34 +108,21 @@ class TableCellRenderer(Box):
return None # Cell rendering is done directly on the page
def _render_cell_content(self, x: int, y: int, width: int, height: int):
"""Render the content inside the cell (text and images) with line wrapping."""
from pyWebLayout.concrete.text import Line, Text
from pyWebLayout.style.fonts import Font
from pyWebLayout.style import FontWeight, Alignment
"""Render the content inside the cell (text and images)."""
from PIL import ImageFont
current_y = y + 2
available_height = height - 4 # Account for top/bottom padding
# Create font for the cell
font_size = 12
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
if self._is_header_section and self._style.header_text_bold:
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
font = Font(
font_path=font_path,
font_size=font_size,
weight=FontWeight.BOLD if self._is_header_section and self._style.header_text_bold else FontWeight.NORMAL
)
# Word spacing constraints (min, max)
min_spacing = int(font_size * 0.25)
max_spacing = int(font_size * 0.5)
word_spacing = (min_spacing, max_spacing)
# Line height (baseline spacing)
line_height = font_size + 4
ascent, descent = font.font.getmetrics()
# Get font
try:
if self._is_header_section and self._style.header_text_bold:
font = ImageFont.truetype(
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 12)
else:
font = ImageFont.truetype(
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 12)
except BaseException:
font = ImageFont.load_default()
# Render each block in the cell
for block in self._cell.blocks():
@ -144,102 +131,38 @@ class TableCellRenderer(Box):
current_y = self._render_image_in_cell(
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
# Extract and render text
words = []
word_items = block.words() if callable(block.words) else block.words
words = list(word_items)
for word in word_items:
if hasattr(word, 'text'):
words.append(word.text)
elif isinstance(word, tuple) and len(word) >= 2:
word_obj = word[1]
if hasattr(word_obj, 'text'):
words.append(word_obj.text)
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(wrapped_words):
# Check if we have space for another line
if current_y + ascent + descent > y + available_height:
break # No more space in cell
# Create a new line
line = Line(
spacing=word_spacing,
origin=(x + 2, current_y),
size=(width - 4, line_height),
draw=self._draw,
font=font,
halign=Alignment.LEFT
)
# Add words to this line until it's full
line_has_content = False
while word_index < len(wrapped_words):
word = wrapped_words[word_index]
# Try to add word to line
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 and advance
# This prevents infinite loops with words that truly can't fit
word_index += 1
break
# Render the line if it has content
if line_has_content or len(line.text_objects) > 0:
line.render()
current_y += line_height
if words:
text = " ".join(words)
if current_y <= y + height - 15:
self._draw.text((x + 2, current_y), text,
fill=(0, 0, 0), font=font)
current_y += 16
if current_y > y + height - 10: # Don't overflow cell
break
# If no structured content, try to get any text representation
if current_y == y + 2 and hasattr(self._cell, '_text_content'):
# Use simple text rendering for fallback case
from PIL import ImageFont
try:
pil_font = ImageFont.truetype(font_path, font_size)
except BaseException:
pil_font = ImageFont.load_default()
self._draw.text(
(x + 2, current_y),
(x + 2,
current_y),
self._cell._text_content,
fill=(0, 0, 0),
font=pil_font
)
fill=(
0,
0,
0),
font=font)
def _render_image_in_cell(self, image_block: AbstractImage, x: int, y: int,
max_width: int, max_height: int) -> int:
@ -473,16 +396,11 @@ class TableRenderer(Box):
column_widths = [column_width] * num_columns
# Calculate row heights
# 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,
header_height = 35 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 = 60 # Increased from 30 to allow for text wrapping
body_height = 30
for section, row in all_rows:
if section == "body":
for cell in row.cells():
@ -492,7 +410,7 @@ class TableRenderer(Box):
body_height = max(body_height, 120)
break
footer_height = 60 if any(1 for section,
footer_height = 30 if any(1 for section,
_ in all_rows if section == "footer") else 0
row_heights = {