diff --git a/README.md b/README.md
index 81c45c9..5f74296 100644
--- a/README.md
+++ b/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
- āļø **Text Alignment** - Left, center, right, and justified text
- š **Rich Content** - Headings, paragraphs, bold, italic, and more
+- š **Table Rendering** - Full HTML table support with headers, borders, and styling
### Architecture
- **Abstract/Concrete Separation** - Clean separation between content structure and rendering
@@ -87,21 +88,28 @@ The library supports various page layouts and configurations:
- |
+ |
Page Styles
- 
+ 
Different borders, padding, and backgrounds
|
-
+ |
HTML Content
- 
+ 
Parsed HTML with various text styles
|
-
+ |
+
+
Page Layouts
- 
+ 
Portrait, landscape, and square formats
|
+
+ Table Rendering
+ 
+ HTML tables with headers and styling
+ |
@@ -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
- **[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
+- **[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
- **[html_multipage_simple.py](examples/html_multipage_simple.py)** - Multi-page HTML rendering
diff --git a/docs/images/example_04_table_rendering.png b/docs/images/example_04_table_rendering.png
new file mode 100644
index 0000000..97d4aed
Binary files /dev/null and b/docs/images/example_04_table_rendering.png differ
diff --git a/docs/images/example_05_table_with_images.png b/docs/images/example_05_table_with_images.png
new file mode 100644
index 0000000..2fd543e
Binary files /dev/null and b/docs/images/example_05_table_with_images.png differ
diff --git a/examples/04_table_rendering.py b/examples/04_table_rendering.py
new file mode 100644
index 0000000..09cfa2c
--- /dev/null
+++ b/examples/04_table_rendering.py
@@ -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 = """
+
+
+
+ | Name |
+ Age |
+ City |
+
+
+
+
+ | Alice |
+ 28 |
+ Paris |
+
+
+ | Bob |
+ 34 |
+ London |
+
+
+ | Charlie |
+ 25 |
+ Tokyo |
+
+
+
+ """
+
+ return html, "Simple Table"
+
+
+def create_styled_table_example():
+ """Create a table with custom styling."""
+ print(" - Styled table")
+
+ html = """
+
+ Monthly Sales Report
+
+
+ | Month |
+ Revenue |
+ Expenses |
+ Profit |
+
+
+
+
+ | January |
+ $50,000 |
+ $30,000 |
+ $20,000 |
+
+
+ | February |
+ $55,000 |
+ $32,000 |
+ $23,000 |
+
+
+ | March |
+ $60,000 |
+ $35,000 |
+ $25,000 |
+
+
+
+ """
+
+ return html, "Styled Table"
+
+
+def create_complex_table_example():
+ """Create a table with colspan."""
+ print(" - Complex table with colspan")
+
+ html = """
+
+ Product Specifications
+
+
+ | Product |
+ Features |
+ Price |
+
+
+
+
+ | Laptop |
+ 16GB RAM, 512GB SSD |
+ $1,299 |
+
+
+ | Monitor |
+ 27 inch, 4K Resolution |
+ $599 |
+
+
+ | Keyboard |
+ Mechanical, RGB |
+ $129 |
+
+
+
+ """
+
+ return html, "Complex Table"
+
+
+def create_data_table_example():
+ """Create a table with numerical data."""
+ print(" - Data table")
+
+ html = """
+
+ Test Results
+
+
+ | Test |
+ Score |
+ Status |
+
+
+
+
+ | Unit Tests |
+ 98% |
+ Pass |
+
+
+ | Integration |
+ 95% |
+ Pass |
+
+
+ | Performance |
+ 87% |
+ Pass |
+
+
+
+ """
+
+ 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()
diff --git a/examples/05_table_with_images.py b/examples/05_table_with_images.py
new file mode 100644
index 0000000..eba263c
--- /dev/null
+++ b/examples/05_table_with_images.py
@@ -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()
diff --git a/examples/README.md b/examples/README.md
index 81fc46d..9384445 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -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
### HTML Rendering
@@ -72,6 +104,8 @@ cd examples
python 01_simple_page_rendering.py
python 02_text_and_layout.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.
diff --git a/pyWebLayout/concrete/__init__.py b/pyWebLayout/concrete/__init__.py
index 4e2f210..3501b05 100644
--- a/pyWebLayout/concrete/__init__.py
+++ b/pyWebLayout/concrete/__init__.py
@@ -3,3 +3,4 @@ from .page import Page
from .text import Text, Line
from .functional import LinkText, ButtonText, FormFieldText, create_link_text, create_button_text, create_form_field_text
from .image import RenderableImage
+from .table import TableRenderer, TableRowRenderer, TableCellRenderer, TableStyle
diff --git a/pyWebLayout/concrete/table.py b/pyWebLayout/concrete/table.py
new file mode 100644
index 0000000..c47739d
--- /dev/null
+++ b/pyWebLayout/concrete/table.py
@@ -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])