Compare commits
2 Commits
303179865d
...
8e720d4037
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e720d4037 | |||
| 5afad2ca07 |
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
|
## Advanced Examples
|
||||||
@ -207,6 +232,8 @@ python 06_functional_elements_demo.py
|
|||||||
python 08_pagination_demo.py # Multi-page documents
|
python 08_pagination_demo.py # Multi-page documents
|
||||||
python 09_link_navigation_demo.py # All link types
|
python 09_link_navigation_demo.py # All link types
|
||||||
python 10_forms_demo.py # All form field 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.
|
Output images are saved to the `docs/images/` directory.
|
||||||
|
|||||||
@ -108,21 +108,34 @@ class TableCellRenderer(Box):
|
|||||||
return None # Cell rendering is done directly on the page
|
return None # Cell rendering is done directly on the page
|
||||||
|
|
||||||
def _render_cell_content(self, x: int, y: int, width: int, height: int):
|
def _render_cell_content(self, x: int, y: int, width: int, height: int):
|
||||||
"""Render the content inside the cell (text and images)."""
|
"""Render the content inside the cell (text and images) with line wrapping."""
|
||||||
from PIL import ImageFont
|
from pyWebLayout.concrete.text import Line, Text
|
||||||
|
from pyWebLayout.style.fonts import Font
|
||||||
|
from pyWebLayout.style import FontWeight, Alignment
|
||||||
|
|
||||||
current_y = y + 2
|
current_y = y + 2
|
||||||
|
available_height = height - 4 # Account for top/bottom padding
|
||||||
|
|
||||||
# Get font
|
# Create font for the cell
|
||||||
try:
|
font_size = 12
|
||||||
|
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
|
||||||
if self._is_header_section and self._style.header_text_bold:
|
if self._is_header_section and self._style.header_text_bold:
|
||||||
font = ImageFont.truetype(
|
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
|
||||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 12)
|
|
||||||
else:
|
font = Font(
|
||||||
font = ImageFont.truetype(
|
font_path=font_path,
|
||||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 12)
|
font_size=font_size,
|
||||||
except BaseException:
|
weight=FontWeight.BOLD if self._is_header_section and self._style.header_text_bold else FontWeight.NORMAL
|
||||||
font = ImageFont.load_default()
|
)
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
# Render each block in the cell
|
# Render each block in the cell
|
||||||
for block in self._cell.blocks():
|
for block in self._cell.blocks():
|
||||||
@ -131,38 +144,102 @@ class TableCellRenderer(Box):
|
|||||||
current_y = self._render_image_in_cell(
|
current_y = self._render_image_in_cell(
|
||||||
block, x, current_y, width, height - (current_y - y))
|
block, x, current_y, width, height - (current_y - y))
|
||||||
elif isinstance(block, (Paragraph, Heading)):
|
elif isinstance(block, (Paragraph, Heading)):
|
||||||
# Extract and render text
|
# Get words from the block
|
||||||
words = []
|
from pyWebLayout.abstract.inline import Word as AbstractWord
|
||||||
word_items = block.words() if callable(block.words) else block.words
|
|
||||||
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 words:
|
word_items = block.words() if callable(block.words) else block.words
|
||||||
text = " ".join(words)
|
words = list(word_items)
|
||||||
if current_y <= y + height - 15:
|
|
||||||
self._draw.text((x + 2, current_y), text,
|
if not words:
|
||||||
fill=(0, 0, 0), font=font)
|
continue
|
||||||
current_y += 16
|
|
||||||
|
# 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 current_y > y + height - 10: # Don't overflow cell
|
if current_y > y + height - 10: # Don't overflow cell
|
||||||
break
|
break
|
||||||
|
|
||||||
# If no structured content, try to get any text representation
|
# If no structured content, try to get any text representation
|
||||||
if current_y == y + 2 and hasattr(self._cell, '_text_content'):
|
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(
|
self._draw.text(
|
||||||
(x + 2,
|
(x + 2, current_y),
|
||||||
current_y),
|
|
||||||
self._cell._text_content,
|
self._cell._text_content,
|
||||||
fill=(
|
fill=(0, 0, 0),
|
||||||
0,
|
font=pil_font
|
||||||
0,
|
)
|
||||||
0),
|
|
||||||
font=font)
|
|
||||||
|
|
||||||
def _render_image_in_cell(self, image_block: AbstractImage, x: int, y: int,
|
def _render_image_in_cell(self, image_block: AbstractImage, x: int, y: int,
|
||||||
max_width: int, max_height: int) -> int:
|
max_width: int, max_height: int) -> int:
|
||||||
@ -396,11 +473,16 @@ class TableRenderer(Box):
|
|||||||
column_widths = [column_width] * num_columns
|
column_widths = [column_width] * num_columns
|
||||||
|
|
||||||
# Calculate row heights
|
# 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
|
_ in all_rows if section == "header") else 0
|
||||||
|
|
||||||
# Check if any body rows contain images - if so, use larger height
|
# 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:
|
for section, row in all_rows:
|
||||||
if section == "body":
|
if section == "body":
|
||||||
for cell in row.cells():
|
for cell in row.cells():
|
||||||
@ -410,7 +492,7 @@ class TableRenderer(Box):
|
|||||||
body_height = max(body_height, 120)
|
body_height = max(body_height, 120)
|
||||||
break
|
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
|
_ in all_rows if section == "footer") else 0
|
||||||
|
|
||||||
row_heights = {
|
row_heights = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user