complete the table rendering
All checks were successful
Python CI / test (push) Successful in 6m37s

This commit is contained in:
Duncan Tourolle 2025-11-07 19:45:47 +01:00
parent b1553f1628
commit 03918fc716
8 changed files with 1116 additions and 6 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

View 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()

View 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()

View File

@ -51,6 +51,38 @@ Demonstrates:
![Page Layouts Example](../docs/images/example_03_page_layouts.png) ![Page Layouts Example](../docs/images/example_03_page_layouts.png)
### 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
![Table Rendering Example](../docs/images/example_04_table_rendering.png)
### 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
![Table with Images Example](../docs/images/example_05_table_with_images.png)
## 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.

View File

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

View 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])