This commit is contained in:
parent
b1553f1628
commit
03918fc716
22
README.md
22
README.md
@ -27,6 +27,7 @@ PyWebLayout is a Python library for HTML-like layout and rendering to paginated
|
|||||||
- 🔤 **Font Support** - Multiple font sizes, weights, and styles
|
- 🔤 **Font Support** - Multiple font sizes, weights, and styles
|
||||||
- ↔️ **Text Alignment** - Left, center, right, and justified text
|
- ↔️ **Text Alignment** - Left, center, right, and justified text
|
||||||
- 📖 **Rich Content** - Headings, paragraphs, bold, italic, and more
|
- 📖 **Rich Content** - Headings, paragraphs, bold, italic, and more
|
||||||
|
- 📊 **Table Rendering** - Full HTML table support with headers, borders, and styling
|
||||||
|
|
||||||
### Architecture
|
### Architecture
|
||||||
- **Abstract/Concrete Separation** - Clean separation between content structure and rendering
|
- **Abstract/Concrete Separation** - Clean separation between content structure and rendering
|
||||||
@ -87,21 +88,28 @@ The library supports various page layouts and configurations:
|
|||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" width="33%">
|
<td align="center" width="50%">
|
||||||
<b>Page Styles</b><br>
|
<b>Page Styles</b><br>
|
||||||
<img src="docs/images/example_01_page_rendering.png" width="250" alt="Page Rendering"><br>
|
<img src="docs/images/example_01_page_rendering.png" width="300" alt="Page Rendering"><br>
|
||||||
<em>Different borders, padding, and backgrounds</em>
|
<em>Different borders, padding, and backgrounds</em>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" width="33%">
|
<td align="center" width="50%">
|
||||||
<b>HTML Content</b><br>
|
<b>HTML Content</b><br>
|
||||||
<img src="docs/images/example_02_text_and_layout.png" width="250" alt="Text Layout"><br>
|
<img src="docs/images/example_02_text_and_layout.png" width="300" alt="Text Layout"><br>
|
||||||
<em>Parsed HTML with various text styles</em>
|
<em>Parsed HTML with various text styles</em>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" width="33%">
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" width="50%">
|
||||||
<b>Page Layouts</b><br>
|
<b>Page Layouts</b><br>
|
||||||
<img src="docs/images/example_03_page_layouts.png" width="250" alt="Page Layouts"><br>
|
<img src="docs/images/example_03_page_layouts.png" width="300" alt="Page Layouts"><br>
|
||||||
<em>Portrait, landscape, and square formats</em>
|
<em>Portrait, landscape, and square formats</em>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" width="50%">
|
||||||
|
<b>Table Rendering</b><br>
|
||||||
|
<img src="docs/images/example_04_table_rendering.png" width="300" alt="Table Rendering"><br>
|
||||||
|
<em>HTML tables with headers and styling</em>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@ -113,6 +121,8 @@ The `examples/` directory contains working demonstrations:
|
|||||||
- **[01_simple_page_rendering.py](examples/01_simple_page_rendering.py)** - Introduction to the Page system
|
- **[01_simple_page_rendering.py](examples/01_simple_page_rendering.py)** - Introduction to the Page system
|
||||||
- **[02_text_and_layout.py](examples/02_text_and_layout.py)** - HTML parsing and text rendering
|
- **[02_text_and_layout.py](examples/02_text_and_layout.py)** - HTML parsing and text rendering
|
||||||
- **[03_page_layouts.py](examples/03_page_layouts.py)** - Different page configurations
|
- **[03_page_layouts.py](examples/03_page_layouts.py)** - Different page configurations
|
||||||
|
- **[04_table_rendering.py](examples/04_table_rendering.py)** - HTML table rendering with styling
|
||||||
|
- **[05_table_with_images.py](examples/05_table_with_images.py)** - Tables with embedded images
|
||||||
|
|
||||||
### Advanced Examples
|
### Advanced Examples
|
||||||
- **[html_multipage_simple.py](examples/html_multipage_simple.py)** - Multi-page HTML rendering
|
- **[html_multipage_simple.py](examples/html_multipage_simple.py)** - Multi-page HTML rendering
|
||||||
|
|||||||
BIN
docs/images/example_04_table_rendering.png
Normal file
BIN
docs/images/example_04_table_rendering.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/images/example_05_table_with_images.png
Normal file
BIN
docs/images/example_05_table_with_images.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
369
examples/04_table_rendering.py
Normal file
369
examples/04_table_rendering.py
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Table Rendering Example
|
||||||
|
|
||||||
|
This example demonstrates rendering HTML tables:
|
||||||
|
- Simple tables with headers
|
||||||
|
- Tables with multiple rows and columns
|
||||||
|
- Tables with colspan and borders
|
||||||
|
- Tables with formatted content
|
||||||
|
- Tables parsed from HTML
|
||||||
|
|
||||||
|
Shows the TableRenderer system in action.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
# Add pyWebLayout to path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
from pyWebLayout.concrete.page import Page
|
||||||
|
from pyWebLayout.concrete.table import TableRenderer, TableStyle
|
||||||
|
from pyWebLayout.style.page_style import PageStyle
|
||||||
|
from pyWebLayout.io.readers.html_extraction import parse_html_string
|
||||||
|
from pyWebLayout.style import Font
|
||||||
|
from pyWebLayout.abstract.block import Table
|
||||||
|
|
||||||
|
|
||||||
|
def create_simple_table_example():
|
||||||
|
"""Create a simple table from HTML."""
|
||||||
|
print(" - Simple data table")
|
||||||
|
|
||||||
|
html = """
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Age</th>
|
||||||
|
<th>City</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Alice</td>
|
||||||
|
<td>28</td>
|
||||||
|
<td>Paris</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Bob</td>
|
||||||
|
<td>34</td>
|
||||||
|
<td>London</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Charlie</td>
|
||||||
|
<td>25</td>
|
||||||
|
<td>Tokyo</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return html, "Simple Table"
|
||||||
|
|
||||||
|
|
||||||
|
def create_styled_table_example():
|
||||||
|
"""Create a table with custom styling."""
|
||||||
|
print(" - Styled table")
|
||||||
|
|
||||||
|
html = """
|
||||||
|
<table>
|
||||||
|
<caption>Monthly Sales Report</caption>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Month</th>
|
||||||
|
<th>Revenue</th>
|
||||||
|
<th>Expenses</th>
|
||||||
|
<th>Profit</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>January</td>
|
||||||
|
<td>$50,000</td>
|
||||||
|
<td>$30,000</td>
|
||||||
|
<td>$20,000</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>February</td>
|
||||||
|
<td>$55,000</td>
|
||||||
|
<td>$32,000</td>
|
||||||
|
<td>$23,000</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>March</td>
|
||||||
|
<td>$60,000</td>
|
||||||
|
<td>$35,000</td>
|
||||||
|
<td>$25,000</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return html, "Styled Table"
|
||||||
|
|
||||||
|
|
||||||
|
def create_complex_table_example():
|
||||||
|
"""Create a table with colspan."""
|
||||||
|
print(" - Complex table with colspan")
|
||||||
|
|
||||||
|
html = """
|
||||||
|
<table>
|
||||||
|
<caption>Product Specifications</caption>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Product</th>
|
||||||
|
<th>Features</th>
|
||||||
|
<th>Price</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Laptop</td>
|
||||||
|
<td>16GB RAM, 512GB SSD</td>
|
||||||
|
<td>$1,299</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Monitor</td>
|
||||||
|
<td>27 inch, 4K Resolution</td>
|
||||||
|
<td>$599</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Keyboard</td>
|
||||||
|
<td>Mechanical, RGB</td>
|
||||||
|
<td>$129</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return html, "Complex Table"
|
||||||
|
|
||||||
|
|
||||||
|
def create_data_table_example():
|
||||||
|
"""Create a table with numerical data."""
|
||||||
|
print(" - Data table")
|
||||||
|
|
||||||
|
html = """
|
||||||
|
<table>
|
||||||
|
<caption>Test Results</caption>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Test</th>
|
||||||
|
<th>Score</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Unit Tests</td>
|
||||||
|
<td>98%</td>
|
||||||
|
<td>Pass</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Integration</td>
|
||||||
|
<td>95%</td>
|
||||||
|
<td>Pass</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Performance</td>
|
||||||
|
<td>87%</td>
|
||||||
|
<td>Pass</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return html, "Data Table"
|
||||||
|
|
||||||
|
|
||||||
|
def render_table_example(html: str, title: str, style_variant: int = 0, page_size=(500, 400)):
|
||||||
|
"""Render a table from HTML to an image."""
|
||||||
|
# Create page with varying backgrounds
|
||||||
|
bg_colors = [
|
||||||
|
(255, 255, 255), # White
|
||||||
|
(250, 255, 250), # Light green tint
|
||||||
|
(255, 250, 245), # Light orange tint
|
||||||
|
(245, 250, 255), # Light blue tint
|
||||||
|
]
|
||||||
|
|
||||||
|
page_style = PageStyle(
|
||||||
|
border_width=2,
|
||||||
|
border_color=(200, 200, 200),
|
||||||
|
padding=(20, 20, 20, 20),
|
||||||
|
background_color=bg_colors[style_variant % len(bg_colors)]
|
||||||
|
)
|
||||||
|
|
||||||
|
page = Page(size=page_size, style=page_style)
|
||||||
|
image = page.render()
|
||||||
|
draw = ImageDraw.Draw(image)
|
||||||
|
|
||||||
|
# Add title
|
||||||
|
from PIL import ImageFont
|
||||||
|
try:
|
||||||
|
title_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 14)
|
||||||
|
except:
|
||||||
|
title_font = ImageFont.load_default()
|
||||||
|
|
||||||
|
draw.text((page.border_size + 10, page.border_size + 10), title, fill=(50, 50, 150), font=title_font)
|
||||||
|
|
||||||
|
# Parse HTML to get table
|
||||||
|
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 table:
|
||||||
|
# Create table renderer with different styles
|
||||||
|
table_styles = [
|
||||||
|
# Style 0: Classic blue header
|
||||||
|
TableStyle(
|
||||||
|
border_width=1,
|
||||||
|
border_color=(80, 80, 80),
|
||||||
|
cell_padding=(8, 10, 8, 10),
|
||||||
|
header_bg_color=(70, 130, 180), # Steel blue
|
||||||
|
cell_bg_color=(255, 255, 255),
|
||||||
|
alternate_row_color=(240, 248, 255) # Alice blue
|
||||||
|
),
|
||||||
|
# Style 1: Green theme
|
||||||
|
TableStyle(
|
||||||
|
border_width=2,
|
||||||
|
border_color=(34, 139, 34), # Forest green
|
||||||
|
cell_padding=(10, 12, 10, 12),
|
||||||
|
header_bg_color=(144, 238, 144), # Light green
|
||||||
|
cell_bg_color=(255, 255, 255),
|
||||||
|
alternate_row_color=(240, 255, 240) # Honeydew
|
||||||
|
),
|
||||||
|
# Style 2: Minimal style
|
||||||
|
TableStyle(
|
||||||
|
border_width=0,
|
||||||
|
border_color=(200, 200, 200),
|
||||||
|
cell_padding=(6, 8, 6, 8),
|
||||||
|
header_bg_color=(245, 245, 245),
|
||||||
|
cell_bg_color=(255, 255, 255),
|
||||||
|
alternate_row_color=None # No alternating
|
||||||
|
),
|
||||||
|
# Style 3: Bold borders
|
||||||
|
TableStyle(
|
||||||
|
border_width=3,
|
||||||
|
border_color=(0, 0, 0),
|
||||||
|
cell_padding=(10, 10, 10, 10),
|
||||||
|
header_bg_color=(255, 215, 0), # Gold
|
||||||
|
cell_bg_color=(255, 255, 255),
|
||||||
|
alternate_row_color=(255, 250, 205) # Lemon chiffon
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
table_style = table_styles[style_variant % len(table_styles)]
|
||||||
|
|
||||||
|
# Position table below title
|
||||||
|
table_origin = (page.border_size + 10, page.border_size + 40)
|
||||||
|
table_width = page.content_size[0] - 20
|
||||||
|
|
||||||
|
renderer = TableRenderer(
|
||||||
|
table,
|
||||||
|
table_origin,
|
||||||
|
table_width,
|
||||||
|
draw,
|
||||||
|
table_style
|
||||||
|
)
|
||||||
|
|
||||||
|
renderer.render()
|
||||||
|
else:
|
||||||
|
# Draw "No table found" message
|
||||||
|
draw.text((page.border_size + 10, page.border_size + 50),
|
||||||
|
"No table found in HTML",
|
||||||
|
fill=(200, 0, 0), font=title_font)
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
def combine_examples(examples):
|
||||||
|
"""Combine multiple table examples into a grid."""
|
||||||
|
print("\n Rendering table examples...")
|
||||||
|
|
||||||
|
images = []
|
||||||
|
for i, (html, title) in enumerate(examples):
|
||||||
|
img = render_table_example(html, title, style_variant=i)
|
||||||
|
images.append(img)
|
||||||
|
|
||||||
|
# Create grid (2x2)
|
||||||
|
padding = 15
|
||||||
|
cols = 2
|
||||||
|
rows = 2
|
||||||
|
|
||||||
|
img_width = images[0].size[0]
|
||||||
|
img_height = images[0].size[1]
|
||||||
|
|
||||||
|
total_width = cols * img_width + (cols + 1) * padding
|
||||||
|
total_height = rows * img_height + (rows + 1) * padding + 50 # Extra for main title
|
||||||
|
|
||||||
|
combined = Image.new('RGB', (total_width, total_height), (240, 240, 240))
|
||||||
|
draw = ImageDraw.Draw(combined)
|
||||||
|
|
||||||
|
# Add main title
|
||||||
|
from PIL import ImageFont
|
||||||
|
try:
|
||||||
|
main_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 20)
|
||||||
|
except:
|
||||||
|
main_font = ImageFont.load_default()
|
||||||
|
|
||||||
|
title_text = "Table Rendering Examples"
|
||||||
|
bbox = draw.textbbox((0, 0), title_text, font=main_font)
|
||||||
|
text_width = bbox[2] - bbox[0]
|
||||||
|
title_x = (total_width - text_width) // 2
|
||||||
|
draw.text((title_x, 15), title_text, fill=(50, 50, 50), font=main_font)
|
||||||
|
|
||||||
|
# Place images
|
||||||
|
y_offset = 50 + padding
|
||||||
|
for row in range(rows):
|
||||||
|
x_offset = padding
|
||||||
|
for col in range(cols):
|
||||||
|
idx = row * cols + col
|
||||||
|
if idx < len(images):
|
||||||
|
combined.paste(images[idx], (x_offset, y_offset))
|
||||||
|
x_offset += img_width + padding
|
||||||
|
y_offset += img_height + padding
|
||||||
|
|
||||||
|
return combined
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Demonstrate table rendering."""
|
||||||
|
print("Table Rendering Example")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Create table examples
|
||||||
|
print("\n Creating table examples...")
|
||||||
|
examples = [
|
||||||
|
create_simple_table_example(),
|
||||||
|
create_styled_table_example(),
|
||||||
|
create_complex_table_example(),
|
||||||
|
create_data_table_example()
|
||||||
|
]
|
||||||
|
|
||||||
|
# Render and combine
|
||||||
|
combined_image = combine_examples(examples)
|
||||||
|
|
||||||
|
# Save output
|
||||||
|
output_dir = Path("docs/images")
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
output_path = output_dir / "example_04_table_rendering.png"
|
||||||
|
combined_image.save(output_path)
|
||||||
|
|
||||||
|
print(f"\n✓ Example completed!")
|
||||||
|
print(f" Output saved to: {output_path}")
|
||||||
|
print(f" Image size: {combined_image.size[0]}x{combined_image.size[1]} pixels")
|
||||||
|
print(f" Created {len(examples)} table examples")
|
||||||
|
|
||||||
|
return combined_image
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
252
examples/05_table_with_images.py
Normal file
252
examples/05_table_with_images.py
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Table with Images Example
|
||||||
|
|
||||||
|
This example demonstrates rendering tables with images:
|
||||||
|
- Creating tables programmatically
|
||||||
|
- Adding images to table cells
|
||||||
|
- Book catalog / product showcase tables
|
||||||
|
- Mixed content (images and text) in cells
|
||||||
|
|
||||||
|
Uses the cover images from tests/data directory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
# Add pyWebLayout to path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
from pyWebLayout.concrete.page import Page
|
||||||
|
from pyWebLayout.style.page_style import PageStyle
|
||||||
|
from pyWebLayout.concrete.table import TableRenderer, TableStyle
|
||||||
|
from pyWebLayout.abstract.block import Table, Image as AbstractImage
|
||||||
|
from pyWebLayout.abstract.inline import Word
|
||||||
|
from pyWebLayout.style import Font
|
||||||
|
|
||||||
|
|
||||||
|
def create_book_catalog_table(cover_images: dict):
|
||||||
|
"""Create a book catalog table with cover images and details."""
|
||||||
|
print(" - Creating book catalog table...")
|
||||||
|
|
||||||
|
# Get base path for images
|
||||||
|
data_path = Path(__file__).parent.parent / "tests" / "data"
|
||||||
|
|
||||||
|
# Create table
|
||||||
|
table = Table(caption="Book Catalog", style=Font(font_size=14))
|
||||||
|
|
||||||
|
# Header row
|
||||||
|
header = table.create_row("header")
|
||||||
|
header.create_cell(is_header=True).create_paragraph().add_word(Word("Cover", Font(font_size=12)))
|
||||||
|
header.create_cell(is_header=True).create_paragraph().add_word(Word("Title", Font(font_size=12)))
|
||||||
|
header.create_cell(is_header=True).create_paragraph().add_word(Word("Author", Font(font_size=12)))
|
||||||
|
header.create_cell(is_header=True).create_paragraph().add_word(Word("Price", Font(font_size=12)))
|
||||||
|
|
||||||
|
# Book entries
|
||||||
|
books = [
|
||||||
|
("cover 1.png", "The Great Adventure", "John Smith", "$19.99"),
|
||||||
|
("cover 2.png", "Mystery of the Ages", "Jane Doe", "$24.99"),
|
||||||
|
("cover 3.png", "Science Today", "Dr. Brown", "$29.99"),
|
||||||
|
("cover 4.png", "Art & Design", "M. Artist", "$34.99"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for cover_file, title, author, price in books:
|
||||||
|
row = table.create_row("body")
|
||||||
|
|
||||||
|
# Cover cell with actual Image block
|
||||||
|
cover_cell = row.create_cell()
|
||||||
|
if cover_file in cover_images:
|
||||||
|
cover_path = str(data_path / cover_file)
|
||||||
|
AbstractImage.create_and_add_to(cover_cell, source=cover_path, alt_text=title)
|
||||||
|
|
||||||
|
# Title cell
|
||||||
|
title_cell = row.create_cell()
|
||||||
|
title_para = title_cell.create_paragraph()
|
||||||
|
for word in title.split():
|
||||||
|
title_para.add_word(Word(word, Font(font_size=11)))
|
||||||
|
|
||||||
|
# Author cell
|
||||||
|
author_cell = row.create_cell()
|
||||||
|
author_para = author_cell.create_paragraph()
|
||||||
|
for word in author.split():
|
||||||
|
author_para.add_word(Word(word, Font(font_size=11)))
|
||||||
|
|
||||||
|
# Price cell
|
||||||
|
price_cell = row.create_cell()
|
||||||
|
price_para = price_cell.create_paragraph()
|
||||||
|
price_para.add_word(Word(price, Font(font_size=11, weight="bold")))
|
||||||
|
|
||||||
|
return table
|
||||||
|
|
||||||
|
|
||||||
|
def create_product_showcase_table(cover_images: dict):
|
||||||
|
"""Create a product showcase table."""
|
||||||
|
print(" - Creating product showcase table...")
|
||||||
|
|
||||||
|
# Get base path for images
|
||||||
|
data_path = Path(__file__).parent.parent / "tests" / "data"
|
||||||
|
|
||||||
|
table = Table(caption="Product Showcase", style=Font(font_size=14))
|
||||||
|
|
||||||
|
# Header row
|
||||||
|
header = table.create_row("header")
|
||||||
|
header.create_cell(is_header=True).create_paragraph().add_word(Word("Product", Font(font_size=12)))
|
||||||
|
header.create_cell(is_header=True).create_paragraph().add_word(Word("Description", Font(font_size=12)))
|
||||||
|
|
||||||
|
# Products with covers
|
||||||
|
products = [
|
||||||
|
("cover 1.png", "Premium Edition - Hardcover with gold embossing"),
|
||||||
|
("cover 2.png", "Collector's Item - Limited print run"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for cover_file, description in products:
|
||||||
|
row = table.create_row("body")
|
||||||
|
|
||||||
|
# Product cell with actual Image block
|
||||||
|
product_cell = row.create_cell()
|
||||||
|
if cover_file in cover_images:
|
||||||
|
cover_path = str(data_path / cover_file)
|
||||||
|
AbstractImage.create_and_add_to(product_cell, source=cover_path, alt_text="Product cover")
|
||||||
|
|
||||||
|
# Description cell
|
||||||
|
desc_cell = row.create_cell()
|
||||||
|
desc_para = desc_cell.create_paragraph()
|
||||||
|
for word in description.split():
|
||||||
|
desc_para.add_word(Word(word, Font(font_size=10)))
|
||||||
|
|
||||||
|
return table
|
||||||
|
|
||||||
|
|
||||||
|
def render_table_with_images(table: Table, title: str, style_variant: int = 0,
|
||||||
|
page_size=(600, 500)):
|
||||||
|
"""Render a table with images using the library API."""
|
||||||
|
# Create page
|
||||||
|
page_style = PageStyle(
|
||||||
|
border_width=2,
|
||||||
|
border_color=(180, 180, 180),
|
||||||
|
padding=(20, 20, 20, 20),
|
||||||
|
background_color=(255, 255, 255)
|
||||||
|
)
|
||||||
|
|
||||||
|
page = Page(size=page_size, style=page_style)
|
||||||
|
canvas = page.render()
|
||||||
|
draw = ImageDraw.Draw(canvas)
|
||||||
|
|
||||||
|
# Add title
|
||||||
|
try:
|
||||||
|
title_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 16)
|
||||||
|
except:
|
||||||
|
title_font = ImageFont.load_default()
|
||||||
|
|
||||||
|
draw.text((page.border_size + 10, page.border_size + 10), title, fill=(50, 50, 150), font=title_font)
|
||||||
|
|
||||||
|
# Table styles
|
||||||
|
table_styles = [
|
||||||
|
TableStyle(
|
||||||
|
border_width=1,
|
||||||
|
border_color=(100, 100, 100),
|
||||||
|
cell_padding=(8, 10, 8, 10),
|
||||||
|
header_bg_color=(70, 130, 180),
|
||||||
|
cell_bg_color=(255, 255, 255),
|
||||||
|
alternate_row_color=(245, 248, 250)
|
||||||
|
),
|
||||||
|
TableStyle(
|
||||||
|
border_width=2,
|
||||||
|
border_color=(60, 120, 60),
|
||||||
|
cell_padding=(10, 12, 10, 12),
|
||||||
|
header_bg_color=(144, 238, 144),
|
||||||
|
cell_bg_color=(255, 255, 255),
|
||||||
|
alternate_row_color=(240, 255, 240)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
table_style = table_styles[style_variant % len(table_styles)]
|
||||||
|
|
||||||
|
# Position table
|
||||||
|
table_origin = (page.border_size + 10, page.border_size + 45)
|
||||||
|
table_width = page.content_size[0] - 20
|
||||||
|
|
||||||
|
# Render table with canvas support for images
|
||||||
|
renderer = TableRenderer(
|
||||||
|
table,
|
||||||
|
table_origin,
|
||||||
|
table_width,
|
||||||
|
draw,
|
||||||
|
table_style,
|
||||||
|
canvas # Pass canvas to enable image rendering
|
||||||
|
)
|
||||||
|
renderer.render()
|
||||||
|
|
||||||
|
return canvas
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Demonstrate tables with images."""
|
||||||
|
print("Table with Images Example")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Load cover images
|
||||||
|
print("\n Loading cover images...")
|
||||||
|
cover_images = {}
|
||||||
|
data_path = Path(__file__).parent.parent / "tests" / "data"
|
||||||
|
|
||||||
|
for i in range(1, 5):
|
||||||
|
cover_path = data_path / f"cover {i}.png"
|
||||||
|
if cover_path.exists():
|
||||||
|
cover_images[f"cover {i}.png"] = Image.open(cover_path)
|
||||||
|
print(f" ✓ Loaded cover {i}.png")
|
||||||
|
|
||||||
|
if not cover_images:
|
||||||
|
print(" ✗ No cover images found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create tables
|
||||||
|
print("\n Creating tables...")
|
||||||
|
book_table = create_book_catalog_table(cover_images)
|
||||||
|
product_table = create_product_showcase_table(cover_images)
|
||||||
|
|
||||||
|
# Render tables
|
||||||
|
print("\n Rendering tables with images...")
|
||||||
|
print(" - Rendering book catalog...")
|
||||||
|
book_image = render_table_with_images(
|
||||||
|
book_table,
|
||||||
|
"Book Catalog with Covers",
|
||||||
|
style_variant=0,
|
||||||
|
page_size=(700, 600)
|
||||||
|
)
|
||||||
|
|
||||||
|
print(" - Rendering product showcase...")
|
||||||
|
product_image = render_table_with_images(
|
||||||
|
product_table,
|
||||||
|
"Product Showcase",
|
||||||
|
style_variant=1,
|
||||||
|
page_size=(600, 350)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Combine images side by side
|
||||||
|
print("\n Combining images...")
|
||||||
|
padding = 20
|
||||||
|
total_width = book_image.size[0] + product_image.size[0] + padding * 3
|
||||||
|
total_height = max(book_image.size[1], product_image.size[1]) + padding * 2
|
||||||
|
|
||||||
|
combined = Image.new('RGB', (total_width, total_height), (240, 240, 240))
|
||||||
|
combined.paste(book_image, (padding, padding))
|
||||||
|
combined.paste(product_image, (book_image.size[0] + padding * 2, padding))
|
||||||
|
|
||||||
|
# Save output
|
||||||
|
output_dir = Path("docs/images")
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
output_path = output_dir / "example_05_table_with_images.png"
|
||||||
|
combined.save(output_path)
|
||||||
|
|
||||||
|
print(f"\n✓ Example completed!")
|
||||||
|
print(f" Output saved to: {output_path}")
|
||||||
|
print(f" Image size: {combined.size[0]}x{combined.size[1]} pixels")
|
||||||
|
print(f" Used {len(cover_images)} cover images")
|
||||||
|
|
||||||
|
return combined
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -51,6 +51,38 @@ Demonstrates:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### 04. Table Rendering
|
||||||
|
**`04_table_rendering.py`** - HTML table rendering with styling
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python 04_table_rendering.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Demonstrates:
|
||||||
|
- Rendering HTML tables
|
||||||
|
- Table headers and body rows
|
||||||
|
- Cell borders and padding
|
||||||
|
- Caption support
|
||||||
|
- Custom table styling
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 05. Tables with Images
|
||||||
|
**`05_table_with_images.py`** - Tables containing images and mixed content
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python 05_table_with_images.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Demonstrates:
|
||||||
|
- Creating tables programmatically
|
||||||
|
- Adding images to table cells
|
||||||
|
- Book catalog and product showcase tables
|
||||||
|
- Mixed content (images and text) in cells
|
||||||
|
- Using cover images from test data
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Advanced Examples
|
## Advanced Examples
|
||||||
|
|
||||||
### HTML Rendering
|
### HTML Rendering
|
||||||
@ -72,6 +104,8 @@ cd examples
|
|||||||
python 01_simple_page_rendering.py
|
python 01_simple_page_rendering.py
|
||||||
python 02_text_and_layout.py
|
python 02_text_and_layout.py
|
||||||
python 03_page_layouts.py
|
python 03_page_layouts.py
|
||||||
|
python 04_table_rendering.py
|
||||||
|
python 05_table_with_images.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Output images are saved to the `docs/images/` directory.
|
Output images are saved to the `docs/images/` directory.
|
||||||
|
|||||||
@ -3,3 +3,4 @@ from .page import Page
|
|||||||
from .text import Text, Line
|
from .text import Text, Line
|
||||||
from .functional import LinkText, ButtonText, FormFieldText, create_link_text, create_button_text, create_form_field_text
|
from .functional import LinkText, ButtonText, FormFieldText, create_link_text, create_button_text, create_form_field_text
|
||||||
from .image import RenderableImage
|
from .image import RenderableImage
|
||||||
|
from .table import TableRenderer, TableRowRenderer, TableCellRenderer, TableStyle
|
||||||
|
|||||||
444
pyWebLayout/concrete/table.py
Normal file
444
pyWebLayout/concrete/table.py
Normal file
@ -0,0 +1,444 @@
|
|||||||
|
"""
|
||||||
|
Concrete table rendering implementation for pyWebLayout.
|
||||||
|
|
||||||
|
This module provides the concrete rendering classes for tables, including:
|
||||||
|
- TableRenderer: Main table rendering with borders and spacing
|
||||||
|
- TableRowRenderer: Individual row rendering
|
||||||
|
- TableCellRenderer: Cell rendering with support for nested content (text, images, links)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Tuple, List, Optional, Dict
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from pyWebLayout.core.base import Renderable, Queriable
|
||||||
|
from pyWebLayout.concrete.box import Box
|
||||||
|
from pyWebLayout.abstract.block import Table, TableRow, TableCell, Paragraph, Heading, Image as AbstractImage
|
||||||
|
from pyWebLayout.style import Font, Alignment
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TableStyle:
|
||||||
|
"""Styling configuration for table rendering."""
|
||||||
|
|
||||||
|
# Border configuration
|
||||||
|
border_width: int = 1
|
||||||
|
border_color: Tuple[int, int, int] = (0, 0, 0)
|
||||||
|
|
||||||
|
# Cell padding
|
||||||
|
cell_padding: Tuple[int, int, int, int] = (5, 5, 5, 5) # top, right, bottom, left
|
||||||
|
|
||||||
|
# Header styling
|
||||||
|
header_bg_color: Tuple[int, int, int] = (240, 240, 240)
|
||||||
|
header_text_bold: bool = True
|
||||||
|
|
||||||
|
# Cell background
|
||||||
|
cell_bg_color: Tuple[int, int, int] = (255, 255, 255)
|
||||||
|
alternate_row_color: Optional[Tuple[int, int, int]] = (250, 250, 250)
|
||||||
|
|
||||||
|
# Spacing
|
||||||
|
cell_spacing: int = 0 # Space between cells (for separated borders model)
|
||||||
|
|
||||||
|
|
||||||
|
class TableCellRenderer(Box):
|
||||||
|
"""
|
||||||
|
Renders a single table cell with its content.
|
||||||
|
Supports paragraphs, headings, images, and links within cells.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, cell: TableCell, origin: Tuple[int, int], size: Tuple[int, int],
|
||||||
|
draw: ImageDraw.Draw, style: TableStyle, is_header_section: bool = False,
|
||||||
|
canvas: Optional[Image.Image] = None):
|
||||||
|
"""
|
||||||
|
Initialize a table cell renderer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cell: The abstract TableCell to render
|
||||||
|
origin: Top-left position of the cell
|
||||||
|
size: Width and height of the cell
|
||||||
|
draw: PIL ImageDraw object for rendering
|
||||||
|
style: Table styling configuration
|
||||||
|
is_header_section: Whether this cell is in the header section
|
||||||
|
canvas: Optional PIL Image for pasting images (required for image rendering)
|
||||||
|
"""
|
||||||
|
super().__init__(origin, size)
|
||||||
|
self._cell = cell
|
||||||
|
self._draw = draw
|
||||||
|
self._style = style
|
||||||
|
self._is_header_section = is_header_section or cell.is_header
|
||||||
|
self._canvas = canvas
|
||||||
|
self._children: List[Renderable] = []
|
||||||
|
|
||||||
|
def render(self) -> Image.Image:
|
||||||
|
"""Render the table cell."""
|
||||||
|
# Determine background color
|
||||||
|
if self._is_header_section:
|
||||||
|
bg_color = self._style.header_bg_color
|
||||||
|
else:
|
||||||
|
bg_color = self._style.cell_bg_color
|
||||||
|
|
||||||
|
# Draw cell background
|
||||||
|
x, y = self._origin
|
||||||
|
w, h = self._size
|
||||||
|
self._draw.rectangle(
|
||||||
|
[x, y, x + w, y + h],
|
||||||
|
fill=bg_color,
|
||||||
|
outline=self._style.border_color,
|
||||||
|
width=self._style.border_width
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calculate content area (inside padding)
|
||||||
|
padding = self._style.cell_padding
|
||||||
|
content_x = x + padding[3] # left padding
|
||||||
|
content_y = y + padding[0] # top padding
|
||||||
|
content_width = w - (padding[1] + padding[3]) # minus left and right padding
|
||||||
|
content_height = h - (padding[0] + padding[2]) # minus top and bottom padding
|
||||||
|
|
||||||
|
# Render cell content (text)
|
||||||
|
self._render_cell_content(content_x, content_y, content_width, content_height)
|
||||||
|
|
||||||
|
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)."""
|
||||||
|
from PIL import ImageFont
|
||||||
|
|
||||||
|
current_y = y + 2
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
font = ImageFont.load_default()
|
||||||
|
|
||||||
|
# Render each block in the cell
|
||||||
|
for block in self._cell.blocks():
|
||||||
|
if isinstance(block, AbstractImage):
|
||||||
|
# Render image
|
||||||
|
current_y = self._render_image_in_cell(block, x, current_y, width, height - (current_y - y))
|
||||||
|
elif isinstance(block, (Paragraph, Heading)):
|
||||||
|
# Extract and render text
|
||||||
|
words = []
|
||||||
|
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:
|
||||||
|
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'):
|
||||||
|
self._draw.text((x + 2, current_y), self._cell._text_content, 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:
|
||||||
|
"""
|
||||||
|
Render an image block inside a table cell.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The new Y position after the image
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get the image path from the block
|
||||||
|
image_path = None
|
||||||
|
if hasattr(image_block, 'source'):
|
||||||
|
image_path = image_block.source
|
||||||
|
elif hasattr(image_block, '_source'):
|
||||||
|
image_path = image_block._source
|
||||||
|
elif hasattr(image_block, 'path'):
|
||||||
|
image_path = image_block.path
|
||||||
|
elif hasattr(image_block, 'src'):
|
||||||
|
image_path = image_block.src
|
||||||
|
elif hasattr(image_block, '_path'):
|
||||||
|
image_path = image_block._path
|
||||||
|
elif hasattr(image_block, '_src'):
|
||||||
|
image_path = image_block._src
|
||||||
|
|
||||||
|
if not image_path:
|
||||||
|
return y + 20 # Skip if no image path
|
||||||
|
|
||||||
|
# Load and resize image to fit in cell
|
||||||
|
img = Image.open(image_path)
|
||||||
|
|
||||||
|
# Calculate scaling to fit within max dimensions
|
||||||
|
# Use more of the cell space for images
|
||||||
|
img_width, img_height = img.size
|
||||||
|
scale_w = max_width / img_width if img_width > max_width else 1
|
||||||
|
scale_h = (max_height - 10) / img_height if img_height > (max_height - 10) else 1
|
||||||
|
scale = min(scale_w, scale_h, 1.0) # Don't upscale
|
||||||
|
|
||||||
|
new_width = int(img_width * scale)
|
||||||
|
new_height = int(img_height * scale)
|
||||||
|
|
||||||
|
if scale < 1.0:
|
||||||
|
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
# Center image horizontally in cell
|
||||||
|
img_x = x + (max_width - new_width) // 2
|
||||||
|
|
||||||
|
# Paste the image onto the canvas if available
|
||||||
|
if self._canvas is not None:
|
||||||
|
if img.mode == 'RGBA':
|
||||||
|
self._canvas.paste(img, (img_x, y), img)
|
||||||
|
else:
|
||||||
|
self._canvas.paste(img, (img_x, y))
|
||||||
|
else:
|
||||||
|
# Fallback: draw a placeholder if no canvas provided
|
||||||
|
self._draw.rectangle(
|
||||||
|
[img_x, y, img_x + new_width, y + new_height],
|
||||||
|
fill=(200, 200, 200),
|
||||||
|
outline=(150, 150, 150)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Draw image indicator text
|
||||||
|
from PIL import ImageFont
|
||||||
|
try:
|
||||||
|
small_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 9)
|
||||||
|
except:
|
||||||
|
small_font = ImageFont.load_default()
|
||||||
|
|
||||||
|
text = f"[Image: {new_width}x{new_height}]"
|
||||||
|
bbox = self._draw.textbbox((0, 0), text, font=small_font)
|
||||||
|
text_width = bbox[2] - bbox[0]
|
||||||
|
text_x = img_x + (new_width - text_width) // 2
|
||||||
|
text_y = y + (new_height - 12) // 2
|
||||||
|
self._draw.text((text_x, text_y), text, fill=(100, 100, 100), font=small_font)
|
||||||
|
|
||||||
|
return y + new_height + 5 # Add some spacing after image
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# If image loading fails, just return current position
|
||||||
|
return y + 20
|
||||||
|
|
||||||
|
|
||||||
|
class TableRowRenderer(Box):
|
||||||
|
"""
|
||||||
|
Renders a single table row containing multiple cells.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, row: TableRow, origin: Tuple[int, int],
|
||||||
|
column_widths: List[int], row_height: int,
|
||||||
|
draw: ImageDraw.Draw, style: TableStyle, is_header_section: bool = False,
|
||||||
|
canvas: Optional[Image.Image] = None):
|
||||||
|
"""
|
||||||
|
Initialize a table row renderer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
row: The abstract TableRow to render
|
||||||
|
origin: Top-left position of the row
|
||||||
|
column_widths: List of widths for each column
|
||||||
|
row_height: Height of this row
|
||||||
|
draw: PIL ImageDraw object for rendering
|
||||||
|
style: Table styling configuration
|
||||||
|
is_header_section: Whether this row is in the header section
|
||||||
|
canvas: Optional PIL Image for pasting images
|
||||||
|
"""
|
||||||
|
width = sum(column_widths) + style.border_width * (len(column_widths) + 1)
|
||||||
|
super().__init__(origin, (width, row_height))
|
||||||
|
self._row = row
|
||||||
|
self._column_widths = column_widths
|
||||||
|
self._row_height = row_height
|
||||||
|
self._draw = draw
|
||||||
|
self._style = style
|
||||||
|
self._is_header_section = is_header_section
|
||||||
|
self._canvas = canvas
|
||||||
|
self._cell_renderers: List[TableCellRenderer] = []
|
||||||
|
|
||||||
|
def render(self) -> Image.Image:
|
||||||
|
"""Render the table row by rendering each cell."""
|
||||||
|
x, y = self._origin
|
||||||
|
current_x = x
|
||||||
|
|
||||||
|
# Render each cell
|
||||||
|
cells = list(self._row.cells())
|
||||||
|
for i, cell in enumerate(cells):
|
||||||
|
if i < len(self._column_widths):
|
||||||
|
cell_width = self._column_widths[i]
|
||||||
|
|
||||||
|
# Handle colspan
|
||||||
|
if cell.colspan > 1 and i + cell.colspan <= len(self._column_widths):
|
||||||
|
# Sum up widths for spanned columns
|
||||||
|
cell_width = sum(self._column_widths[i:i + cell.colspan])
|
||||||
|
cell_width += self._style.border_width * (cell.colspan - 1)
|
||||||
|
|
||||||
|
# Create and render cell
|
||||||
|
cell_renderer = TableCellRenderer(
|
||||||
|
cell,
|
||||||
|
(current_x, y),
|
||||||
|
(cell_width, self._row_height),
|
||||||
|
self._draw,
|
||||||
|
self._style,
|
||||||
|
self._is_header_section,
|
||||||
|
self._canvas
|
||||||
|
)
|
||||||
|
cell_renderer.render()
|
||||||
|
self._cell_renderers.append(cell_renderer)
|
||||||
|
|
||||||
|
current_x += cell_width + self._style.border_width
|
||||||
|
|
||||||
|
return None # Row rendering is done directly on the page
|
||||||
|
|
||||||
|
|
||||||
|
class TableRenderer(Box):
|
||||||
|
"""
|
||||||
|
Main table renderer that orchestrates the rendering of an entire table.
|
||||||
|
Handles layout calculation, row/cell placement, and overall table structure.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, table: Table, origin: Tuple[int, int],
|
||||||
|
available_width: int, draw: ImageDraw.Draw,
|
||||||
|
style: Optional[TableStyle] = None, canvas: Optional[Image.Image] = None):
|
||||||
|
"""
|
||||||
|
Initialize a table renderer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
table: The abstract Table to render
|
||||||
|
origin: Top-left position where the table should be rendered
|
||||||
|
available_width: Maximum width available for the table
|
||||||
|
draw: PIL ImageDraw object for rendering
|
||||||
|
style: Optional table styling configuration
|
||||||
|
canvas: Optional PIL Image for pasting images
|
||||||
|
"""
|
||||||
|
self._table = table
|
||||||
|
self._draw = draw
|
||||||
|
self._style = style or TableStyle()
|
||||||
|
self._available_width = available_width
|
||||||
|
self._canvas = canvas
|
||||||
|
|
||||||
|
# Calculate table dimensions
|
||||||
|
self._column_widths, self._row_heights = self._calculate_dimensions()
|
||||||
|
total_width = sum(self._column_widths) + self._style.border_width * (len(self._column_widths) + 1)
|
||||||
|
total_height = sum(self._row_heights.values()) + self._style.border_width * (len(self._row_heights) + 1)
|
||||||
|
|
||||||
|
super().__init__(origin, (total_width, total_height))
|
||||||
|
self._row_renderers: List[TableRowRenderer] = []
|
||||||
|
|
||||||
|
def _calculate_dimensions(self) -> Tuple[List[int], Dict[str, int]]:
|
||||||
|
"""
|
||||||
|
Calculate column widths and row heights for the table.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (column_widths, row_heights_dict)
|
||||||
|
"""
|
||||||
|
# Determine number of columns (from first row)
|
||||||
|
num_columns = 0
|
||||||
|
all_rows = list(self._table.all_rows())
|
||||||
|
if all_rows:
|
||||||
|
first_row = all_rows[0][1]
|
||||||
|
num_columns = first_row.cell_count
|
||||||
|
|
||||||
|
if num_columns == 0:
|
||||||
|
return ([100], {"header": 30, "body": 30, "footer": 30})
|
||||||
|
|
||||||
|
# Calculate column widths (equal distribution for now)
|
||||||
|
# Account for borders between columns
|
||||||
|
total_border_width = self._style.border_width * (num_columns + 1)
|
||||||
|
available_for_columns = self._available_width - total_border_width
|
||||||
|
column_width = max(50, available_for_columns // num_columns)
|
||||||
|
column_widths = [column_width] * num_columns
|
||||||
|
|
||||||
|
# Calculate row heights
|
||||||
|
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 = 30
|
||||||
|
for section, row in all_rows:
|
||||||
|
if section == "body":
|
||||||
|
for cell in row.cells():
|
||||||
|
for block in cell.blocks():
|
||||||
|
if isinstance(block, AbstractImage):
|
||||||
|
# Use larger height for rows with images
|
||||||
|
body_height = max(body_height, 120)
|
||||||
|
break
|
||||||
|
|
||||||
|
footer_height = 30 if any(1 for section, _ in all_rows if section == "footer") else 0
|
||||||
|
|
||||||
|
row_heights = {
|
||||||
|
"header": header_height,
|
||||||
|
"body": body_height,
|
||||||
|
"footer": footer_height
|
||||||
|
}
|
||||||
|
|
||||||
|
return (column_widths, row_heights)
|
||||||
|
|
||||||
|
def render(self) -> Image.Image:
|
||||||
|
"""Render the complete table."""
|
||||||
|
x, y = self._origin
|
||||||
|
current_y = y
|
||||||
|
|
||||||
|
# Render caption if present
|
||||||
|
if self._table.caption:
|
||||||
|
current_y = self._render_caption(x, current_y)
|
||||||
|
current_y += 10 # Space after caption
|
||||||
|
|
||||||
|
# Render header rows
|
||||||
|
for section, row in self._table.all_rows():
|
||||||
|
if section == "header":
|
||||||
|
row_height = self._row_heights["header"]
|
||||||
|
elif section == "footer":
|
||||||
|
row_height = self._row_heights["footer"]
|
||||||
|
else:
|
||||||
|
row_height = self._row_heights["body"]
|
||||||
|
|
||||||
|
is_header = (section == "header")
|
||||||
|
|
||||||
|
row_renderer = TableRowRenderer(
|
||||||
|
row,
|
||||||
|
(x, current_y),
|
||||||
|
self._column_widths,
|
||||||
|
row_height,
|
||||||
|
self._draw,
|
||||||
|
self._style,
|
||||||
|
is_header,
|
||||||
|
self._canvas
|
||||||
|
)
|
||||||
|
row_renderer.render()
|
||||||
|
self._row_renderers.append(row_renderer)
|
||||||
|
|
||||||
|
current_y += row_height + self._style.border_width
|
||||||
|
|
||||||
|
return None # Table rendering is done directly on the page
|
||||||
|
|
||||||
|
def _render_caption(self, x: int, y: int) -> int:
|
||||||
|
"""Render the table caption and return the new Y position."""
|
||||||
|
from PIL import ImageFont
|
||||||
|
|
||||||
|
try:
|
||||||
|
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 13)
|
||||||
|
except:
|
||||||
|
font = ImageFont.load_default()
|
||||||
|
|
||||||
|
# Center the caption
|
||||||
|
bbox = self._draw.textbbox((0, 0), self._table.caption, font=font)
|
||||||
|
text_width = bbox[2] - bbox[0]
|
||||||
|
caption_x = x + (self._size[0] - text_width) // 2
|
||||||
|
|
||||||
|
self._draw.text((caption_x, y), self._table.caption, fill=(0, 0, 0), font=font)
|
||||||
|
|
||||||
|
return y + 20 # Caption height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def height(self) -> int:
|
||||||
|
"""Get the total height of the rendered table."""
|
||||||
|
return int(self._size[1])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self) -> int:
|
||||||
|
"""Get the total width of the rendered table."""
|
||||||
|
return int(self._size[0])
|
||||||
Loading…
x
Reference in New Issue
Block a user