fix table in cell wrapping
This commit is contained in:
parent
5afad2ca07
commit
8e720d4037
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 42 KiB |
BIN
docs/images/example_11_table_text_wrapping.png
Normal file
BIN
docs/images/example_11_table_text_wrapping.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
BIN
docs/images/example_11b_simple_wrapping.png
Normal file
BIN
docs/images/example_11b_simple_wrapping.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
312
examples/11_table_text_wrapping_demo.py
Normal file
312
examples/11_table_text_wrapping_demo.py
Normal 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()
|
||||
121
examples/11b_simple_table_wrapping.py
Normal file
121
examples/11b_simple_table_wrapping.py
Normal 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()
|
||||
@ -174,6 +174,31 @@ Demonstrates all 14 FormFieldType variations:
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
@ -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
|
||||
word_index += 1
|
||||
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 = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user