end2end table layouter
All checks were successful
Python CI / test (push) Successful in 6m35s

This commit is contained in:
Duncan Tourolle 2025-11-07 21:13:01 +01:00
parent 5fe4db4cbe
commit 496f3bf334
5 changed files with 859 additions and 17 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View File

@ -0,0 +1,318 @@
#!/usr/bin/env python3
"""
Table with Images Example - HTML Output
This example demonstrates creating HTML tables with images:
- Creating HTML tables programmatically
- Embedding images in table cells
- Book catalog / product showcase tables
- Styled tables with CSS
- Mixed content (images and text) in cells
Generates standalone HTML files with embedded images (base64).
"""
import sys
from pathlib import Path
import base64
from typing import Dict, List, Tuple
# Add pyWebLayout to path
sys.path.insert(0, str(Path(__file__).parent.parent))
def image_to_base64(image_path: Path) -> str:
"""Convert image file to base64 string for HTML embedding."""
with open(image_path, 'rb') as img_file:
img_data = img_file.read()
return base64.b64encode(img_data).decode('utf-8')
def create_html_header(title: str) -> str:
"""Create HTML document header with CSS styles."""
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<style>
body {{
font-family: 'DejaVu Sans', Arial, sans-serif;
background-color: #f0f0f0;
padding: 20px;
margin: 0;
}}
.container {{
max-width: 1400px;
margin: 0 auto;
background-color: white;
padding: 30px;
border: 2px solid #b4b4b4;
border-radius: 5px;
}}
h1 {{
color: #323296;
font-size: 24px;
margin-bottom: 30px;
text-align: center;
}}
.table-container {{
margin-bottom: 40px;
}}
.table-title {{
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
color: #333;
}}
/* Blue style table */
table.style-blue {{
width: 100%;
border-collapse: collapse;
border: 1px solid #646464;
margin-bottom: 20px;
}}
table.style-blue th {{
background-color: #4682b4;
color: white;
padding: 10px;
text-align: left;
font-size: 12px;
border: 1px solid #646464;
}}
table.style-blue td {{
padding: 10px;
border: 1px solid #646464;
font-size: 11px;
}}
table.style-blue tr:nth-child(even) {{
background-color: #f5f8fa;
}}
table.style-blue tr:nth-child(odd) {{
background-color: white;
}}
/* Green style table */
table.style-green {{
width: 100%;
border-collapse: collapse;
border: 2px solid #3c783c;
margin-bottom: 20px;
}}
table.style-green th {{
background-color: #90ee90;
color: #333;
padding: 12px;
text-align: left;
font-size: 12px;
border: 2px solid #3c783c;
}}
table.style-green td {{
padding: 12px;
border: 2px solid #3c783c;
font-size: 10px;
}}
table.style-green tr:nth-child(even) {{
background-color: #f0fff0;
}}
table.style-green tr:nth-child(odd) {{
background-color: white;
}}
.cover-image {{
max-width: 100px;
height: auto;
display: block;
}}
.product-image {{
max-width: 120px;
height: auto;
display: block;
}}
.price {{
font-weight: bold;
}}
.footer {{
margin-top: 40px;
text-align: center;
color: #666;
font-size: 12px;
}}
</style>
</head>
<body>
<div class="container">
<h1>{title}</h1>
"""
def create_html_footer() -> str:
"""Create HTML document footer."""
return """
<div class="footer">
Generated by pyWebLayout - Table with Images Example (HTML Version)
</div>
</div>
</body>
</html>
"""
def create_book_catalog_html(cover_images: Dict[str, str]) -> str:
"""Create HTML for book catalog table with cover images."""
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"),
]
html = '<div class="table-container">\n'
html += ' <div class="table-title">Book Catalog</div>\n'
html += ' <table class="style-blue">\n'
html += ' <thead>\n'
html += ' <tr>\n'
html += ' <th>Cover</th>\n'
html += ' <th>Title</th>\n'
html += ' <th>Author</th>\n'
html += ' <th>Price</th>\n'
html += ' </tr>\n'
html += ' </thead>\n'
html += ' <tbody>\n'
for cover_file, title, author, price in books:
html += ' <tr>\n'
# Cover cell
html += ' <td>\n'
if cover_file in cover_images:
html += f' <img src="data:image/png;base64,{cover_images[cover_file]}" alt="{title}" class="cover-image">\n'
html += ' </td>\n'
# Title cell
html += f' <td>{title}</td>\n'
# Author cell
html += f' <td>{author}</td>\n'
# Price cell
html += f' <td class="price">{price}</td>\n'
html += ' </tr>\n'
html += ' </tbody>\n'
html += ' </table>\n'
html += '</div>\n'
return html
def create_product_showcase_html(cover_images: Dict[str, str]) -> str:
"""Create HTML for product showcase table."""
products = [
("cover 1.png", "Premium Edition - Hardcover with gold embossing"),
("cover 2.png", "Collector's Item - Limited print run"),
]
html = '<div class="table-container">\n'
html += ' <div class="table-title">Product Showcase</div>\n'
html += ' <table class="style-green">\n'
html += ' <thead>\n'
html += ' <tr>\n'
html += ' <th>Product</th>\n'
html += ' <th>Description</th>\n'
html += ' </tr>\n'
html += ' </thead>\n'
html += ' <tbody>\n'
for cover_file, description in products:
html += ' <tr>\n'
# Product cell with image
html += ' <td>\n'
if cover_file in cover_images:
html += f' <img src="data:image/png;base64,{cover_images[cover_file]}" alt="Product cover" class="product-image">\n'
html += ' </td>\n'
# Description cell
html += f' <td>{description}</td>\n'
html += ' </tr>\n'
html += ' </tbody>\n'
html += ' </table>\n'
html += '</div>\n'
return html
def main():
"""Generate HTML tables with images."""
print("Table with Images Example - HTML Version")
print("=" * 50)
# Load cover images and convert to base64
print("\n Loading and encoding 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():
try:
cover_images[f"cover {i}.png"] = image_to_base64(cover_path)
print(f" ✓ Loaded and encoded cover {i}.png")
except Exception as e:
print(f" ✗ Failed to load cover {i}.png: {e}")
if not cover_images:
print(" ✗ No cover images found!")
return
# Generate HTML content
print("\n Generating HTML tables...")
html_content = create_html_header("Table with Images - HTML Example")
print(" - Creating book catalog table...")
html_content += create_book_catalog_html(cover_images)
print(" - Creating product showcase table...")
html_content += create_product_showcase_html(cover_images)
html_content += create_html_footer()
# Save HTML output
output_dir = Path("docs/html")
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / "example_05_table_with_images.html"
with open(output_path, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"\n✓ Example completed!")
print(f" Output saved to: {output_path}")
print(f" Used {len(cover_images)} cover images (embedded as base64)")
print(f" Open the file in a web browser to view the tables")
return output_path
if __name__ == "__main__":
main()

View File

@ -0,0 +1,273 @@
#!/usr/bin/env python3
"""
HTML Table with Images Example - End-to-End Rendering
This example demonstrates the complete pipeline:
1. HTML table source with <img> tags in cells
2. parse_html_string() converts HTML Abstract document structure
3. DocumentLayouter handles all layout and rendering
No custom rendering code needed - DocumentLayouter handles everything!
"""
import sys
from pathlib import Path
from PIL import Image
# Add pyWebLayout to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from pyWebLayout.io.readers.html_extraction import parse_html_string
from pyWebLayout.concrete.page import Page
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.layout.document_layouter import DocumentLayouter
from pyWebLayout.concrete.table import TableStyle
from pyWebLayout.style import Font
def create_book_catalog_html():
"""Create HTML for a book catalog table with actual <img> tags."""
# Get base path for images - use absolute paths for the img src
data_path = Path(__file__).parent.parent / "tests" / "data"
html = f"""
<html>
<body>
<table>
<thead>
<tr>
<th>Cover</th>
<th>Title</th>
<th>Author</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr>
<td><img src="{data_path / 'cover 1.png'}" alt="The Great Adventure" /></td>
<td>The Great Adventure</td>
<td>John Smith</td>
<td><b>$19.99</b></td>
</tr>
<tr>
<td><img src="{data_path / 'cover 2.png'}" alt="Mystery of the Ages" /></td>
<td>Mystery of the Ages</td>
<td>Jane Doe</td>
<td><b>$24.99</b></td>
</tr>
<tr>
<td><img src="{data_path / 'cover 3.png'}" alt="Science Today" /></td>
<td>Science Today</td>
<td>Dr. Brown</td>
<td><b>$29.99</b></td>
</tr>
<tr>
<td><img src="{data_path / 'cover 4.png'}" alt="Art & Design" /></td>
<td>Art & Design</td>
<td>M. Artist</td>
<td><b>$34.99</b></td>
</tr>
</tbody>
</table>
</body>
</html>
"""
return html
def create_product_showcase_html():
"""Create HTML for a product showcase table with images."""
data_path = Path(__file__).parent.parent / "tests" / "data"
html = f"""
<html>
<body>
<table>
<thead>
<tr>
<th>Product</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><img src="{data_path / 'cover 1.png'}" alt="Premium Edition" /></td>
<td>Premium Edition - Hardcover with gold embossing</td>
</tr>
<tr>
<td><img src="{data_path / 'cover 2.png'}" alt="Collector's Item" /></td>
<td>Collector's Item - Limited print run</td>
</tr>
</tbody>
</table>
</body>
</html>
"""
return html
def render_html_with_layouter(html_string: str, title: str,
table_style: TableStyle,
page_size=(600, 500)):
"""
Render HTML using DocumentLayouter - the proper way!
This function demonstrates the correct usage:
1. Parse HTML Abstract blocks
2. Create Page
3. Create DocumentLayouter
4. Layout all blocks using layouter
Args:
html_string: HTML source containing table with <img> tags
title: Title for the output (for logging)
table_style: Table styling configuration
page_size: Page dimensions
Returns:
PIL Image with rendered content
"""
print(f"\n Processing '{title}'...")
# Step 1: Parse HTML to abstract blocks
print(" 1. Parsing HTML → Abstract blocks...")
base_font = Font(font_size=11)
blocks = parse_html_string(html_string, base_font=base_font)
print(f" → Parsed {len(blocks)} blocks")
# Step 2: Create page
print(" 2. Creating 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)
# Step 3: Create DocumentLayouter
print(" 3. Creating DocumentLayouter...")
layouter = DocumentLayouter(page)
# Step 4: Layout all blocks using the layouter
print(" 4. Laying out all blocks...")
for block in blocks:
# For tables, we can pass a custom style
from pyWebLayout.abstract.block import Table
if isinstance(block, Table):
success = layouter.layout_table(block, style=table_style)
else:
# For other blocks (paragraphs, headings, images), use layout_document
success = layouter.layout_document([block])
if not success:
print(f" ⚠ Warning: Block {type(block).__name__} didn't fit on page")
print(f" ✓ Layout complete!")
# Step 5: Get the rendered canvas
# Note: Tables render directly onto page._canvas
# We access page.draw to ensure canvas is initialized
print(" 5. Getting rendered canvas...")
_ = page.draw # Ensure canvas exists
return page._canvas
def main():
"""Demonstrate end-to-end HTML table with images rendering using DocumentLayouter."""
print("HTML Table with Images Example - DocumentLayouter")
print("=" * 60)
print("\nThis example demonstrates:")
print(" 1. HTML with <img> tags inside <td> cells")
print(" 2. parse_html_string() automatically handles images")
print(" 3. DocumentLayouter handles all layout and rendering")
print(" 4. NO manual TableRenderer or custom rendering code!")
# Verify images exist
print("\n Checking for cover images...")
data_path = Path(__file__).parent.parent / "tests" / "data"
cover_count = 0
for i in range(1, 5):
cover_path = data_path / f"cover {i}.png"
if cover_path.exists():
cover_count += 1
print(f" ✓ Found cover {i}.png")
if cover_count == 0:
print(" ✗ No cover images found! This example requires cover images.")
return
# Create HTML sources with <img> tags
print("\n Creating HTML sources with <img> tags...")
print(" - Book catalog HTML")
book_html = create_book_catalog_html()
print(" - Product showcase HTML")
product_html = create_product_showcase_html()
# Define table styles
book_style = 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)
)
product_style = 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)
)
# Render using DocumentLayouter - the proper way!
print("\n Rendering with DocumentLayouter (HTML → Abstract → Layout → PNG)...")
book_image = render_html_with_layouter(
book_html,
"Book Catalog",
book_style,
page_size=(700, 600)
)
product_image = render_html_with_layouter(
product_html,
"Product Showcase",
product_style,
page_size=(600, 350)
)
# Combine images side by side
print("\n Combining output 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_06_html_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"\nThe complete pipeline:")
print(f" 1. HTML with <img> tags → parse_html_string() → Abstract blocks")
print(f" 2. Abstract blocks → DocumentLayouter → Concrete objects")
print(f" 3. Page.render() → PNG output")
print(f"\n ✓ Using DocumentLayouter - NO custom rendering code!")
return combined
if __name__ == "__main__":
main()

View File

@ -5,8 +5,9 @@ from typing import List, Tuple, Optional, Union
from pyWebLayout.concrete import Page, Line, Text from pyWebLayout.concrete import Page, Line, Text
from pyWebLayout.concrete.image import RenderableImage from pyWebLayout.concrete.image import RenderableImage
from pyWebLayout.concrete.functional import LinkText from pyWebLayout.concrete.functional import LinkText
from pyWebLayout.concrete.table import TableRenderer, TableStyle
from pyWebLayout.abstract import Paragraph, Word, Link from pyWebLayout.abstract import Paragraph, Word, Link
from pyWebLayout.abstract.block import Image as AbstractImage, PageBreak from pyWebLayout.abstract.block import Image as AbstractImage, PageBreak, Table
from pyWebLayout.abstract.inline import LinkedWord from pyWebLayout.abstract.inline import LinkedWord
from pyWebLayout.style.concrete_style import ConcreteStyleRegistry, RenderingContext, StyleResolver from pyWebLayout.style.concrete_style import ConcreteStyleRegistry, RenderingContext, StyleResolver
from pyWebLayout.style import Font, Alignment from pyWebLayout.style import Font, Alignment
@ -288,14 +289,65 @@ def image_layouter(image: AbstractImage, page: Page, max_width: Optional[int] =
return True return True
def table_layouter(table: Table, page: Page, style: Optional[TableStyle] = None) -> bool:
"""
Layout a table within a given page.
This function uses the TableRenderer to render the table at the current
page position, advancing the page's y-offset after successful rendering.
Args:
table: The abstract Table object to layout
page: The page to layout the table on
style: Optional table styling configuration
Returns:
bool: True if table was successfully laid out, False if page ran out of space
"""
# Calculate available space
available_width = page.available_width
x_offset = page.border_size
y_offset = page._current_y_offset
# Access page.draw to ensure canvas is initialized
draw = page.draw
canvas = page._canvas
# Create table renderer
origin = (x_offset, y_offset)
renderer = TableRenderer(
table=table,
origin=origin,
available_width=available_width,
draw=draw,
style=style,
canvas=canvas
)
# Check if table fits on current page
table_height = renderer.size[1]
available_height = page.size[1] - y_offset - page.border_size
if table_height > available_height:
return False
# Render the table
renderer.render()
# Update page y-offset
page._current_y_offset = y_offset + table_height
return True
class DocumentLayouter: class DocumentLayouter:
""" """
Document layouter that orchestrates layout of various abstract elements. Document layouter that orchestrates layout of various abstract elements.
Delegates to specialized layouters for different content types: Delegates to specialized layouters for different content types:
- paragraph_layouter for text paragraphs - paragraph_layouter for text paragraphs
- image_layouter for images (future) - image_layouter for images
- table_layouter for tables (future) - table_layouter for tables
This class acts as a coordinator, managing the overall document flow This class acts as a coordinator, managing the overall document flow
and page context while delegating specific layout tasks to specialized and page context while delegating specific layout tasks to specialized
@ -310,7 +362,15 @@ class DocumentLayouter:
page: The page to layout content on page: The page to layout content on
""" """
self.page = page self.page = page
self.style_registry = ConcreteStyleRegistry(page.style_resolver) # Create a style resolver if page doesn't have one
if hasattr(page, 'style_resolver'):
style_resolver = page.style_resolver
else:
# Create a default rendering context and style resolver
from pyWebLayout.style.concrete_style import RenderingContext
context = RenderingContext()
style_resolver = StyleResolver(context)
self.style_registry = ConcreteStyleRegistry(style_resolver)
def layout_paragraph(self, paragraph: Paragraph, start_word: int = 0, def layout_paragraph(self, paragraph: Paragraph, start_word: int = 0,
pretext: Optional[Text] = None) -> Tuple[bool, Optional[int], Optional[Text]]: pretext: Optional[Text] = None) -> Tuple[bool, Optional[int], Optional[Text]]:
@ -342,14 +402,27 @@ class DocumentLayouter:
""" """
return image_layouter(image, self.page, max_width, max_height) return image_layouter(image, self.page, max_width, max_height)
def layout_document(self, elements: List[Union[Paragraph, AbstractImage]]) -> bool: def layout_table(self, table: Table, style: Optional[TableStyle] = None) -> bool:
""" """
Layout a list of abstract elements (paragraphs and images). Layout a table using the table_layouter.
Args:
table: The abstract Table object to layout
style: Optional table styling configuration
Returns:
bool: True if table was successfully laid out, False if page ran out of space
"""
return table_layouter(table, self.page, style)
def layout_document(self, elements: List[Union[Paragraph, AbstractImage, Table]]) -> bool:
"""
Layout a list of abstract elements (paragraphs, images, and tables).
This method delegates to specialized layouters based on element type: This method delegates to specialized layouters based on element type:
- Paragraphs are handled by layout_paragraph - Paragraphs are handled by layout_paragraph
- Images are handled by layout_image - Images are handled by layout_image
- Tables and other elements can be added in the future - Tables are handled by layout_table
Args: Args:
elements: List of abstract elements to layout elements: List of abstract elements to layout
@ -366,6 +439,9 @@ class DocumentLayouter:
success = self.layout_image(element) success = self.layout_image(element)
if not success: if not success:
return False return False
# Future: elif isinstance(element, Table): use table_layouter elif isinstance(element, Table):
success = self.layout_table(element)
if not success:
return False
# Future: elif isinstance(element, CodeBlock): use code_layouter # Future: elif isinstance(element, CodeBlock): use code_layouter
return True return True

View File

@ -9,9 +9,11 @@ import pytest
from unittest.mock import Mock, MagicMock, patch from unittest.mock import Mock, MagicMock, patch
from typing import List, Optional from typing import List, Optional
from pyWebLayout.layout.document_layouter import paragraph_layouter, DocumentLayouter from pyWebLayout.layout.document_layouter import paragraph_layouter, table_layouter, DocumentLayouter
from pyWebLayout.style.abstract_style import AbstractStyle from pyWebLayout.style.abstract_style import AbstractStyle
from pyWebLayout.style.concrete_style import ConcreteStyle, StyleResolver, RenderingContext from pyWebLayout.style.concrete_style import ConcreteStyle, StyleResolver, RenderingContext
from pyWebLayout.abstract.block import Table, TableRow, TableCell
from pyWebLayout.concrete.table import TableStyle
class TestDocumentLayouter: class TestDocumentLayouter:
@ -577,6 +579,179 @@ class TestMultiPageLayout:
print(f" - Word spacing constraints: {concrete_style.word_spacing_min}-{concrete_style.word_spacing_max}px") print(f" - Word spacing constraints: {concrete_style.word_spacing_min}-{concrete_style.word_spacing_max}px")
class TestTableLayouter:
"""Test cases for table layouter functionality."""
def setup_method(self):
"""Set up test fixtures before each test method."""
# Create mock page
self.mock_page = Mock()
self.mock_page.border_size = 20
self.mock_page._current_y_offset = 50
self.mock_page.available_width = 600
self.mock_page.size = (800, 1000)
# Create mock draw and canvas
self.mock_draw = Mock()
self.mock_canvas = Mock()
self.mock_page.draw = self.mock_draw
self.mock_page._canvas = self.mock_canvas
# Create mock table
self.mock_table = Mock(spec=Table)
# Create mock style resolver
self.mock_style_resolver = Mock()
self.mock_page.style_resolver = self.mock_style_resolver
@patch('pyWebLayout.layout.document_layouter.TableRenderer')
def test_table_layouter_success(self, mock_table_renderer_class):
"""Test table_layouter with successful table rendering."""
# Setup mock renderer
mock_renderer = Mock()
mock_table_renderer_class.return_value = mock_renderer
mock_renderer.size = (500, 200) # Table fits on page
# Call function
result = table_layouter(self.mock_table, self.mock_page)
# Verify results
assert result is True
# Verify TableRenderer was created with correct parameters
mock_table_renderer_class.assert_called_once_with(
table=self.mock_table,
origin=(20, 50), # (border_size, current_y_offset)
available_width=600,
draw=self.mock_draw,
style=None,
canvas=self.mock_canvas
)
# Verify render was called
mock_renderer.render.assert_called_once()
# Verify y_offset was updated
assert self.mock_page._current_y_offset == 250 # 50 + 200
@patch('pyWebLayout.layout.document_layouter.TableRenderer')
def test_table_layouter_with_custom_style(self, mock_table_renderer_class):
"""Test table_layouter with custom TableStyle."""
# Create custom style
custom_style = TableStyle(
border_width=2,
border_color=(100, 100, 100),
cell_padding=(10, 10, 10, 10)
)
# Setup mock renderer
mock_renderer = Mock()
mock_table_renderer_class.return_value = mock_renderer
mock_renderer.size = (500, 150)
# Call function with style
result = table_layouter(self.mock_table, self.mock_page, style=custom_style)
# Verify TableRenderer was created with custom style
assert result is True
call_args = mock_table_renderer_class.call_args
assert call_args[1]['style'] == custom_style
@patch('pyWebLayout.layout.document_layouter.TableRenderer')
def test_table_layouter_table_too_large(self, mock_table_renderer_class):
"""Test table_layouter when table doesn't fit on page."""
# Setup mock renderer with table larger than available space
mock_renderer = Mock()
mock_table_renderer_class.return_value = mock_renderer
mock_renderer.size = (500, 1000) # Table height exceeds available space
# Available height = page_size[1] - y_offset - border_size
# = 1000 - 50 - 20 = 930, but table is 1000 pixels tall
# Call function
result = table_layouter(self.mock_table, self.mock_page)
# Verify failure
assert result is False
# Verify render was NOT called
mock_renderer.render.assert_not_called()
# Verify y_offset was NOT updated
assert self.mock_page._current_y_offset == 50
@patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry')
def test_document_layouter_layout_table(self, mock_style_registry_class):
"""Test DocumentLayouter.layout_table method."""
# Setup mocks
mock_style_registry = Mock()
mock_style_registry_class.return_value = mock_style_registry
layouter = DocumentLayouter(self.mock_page)
# Mock the table_layouter function
with patch('pyWebLayout.layout.document_layouter.table_layouter') as mock_table_layouter:
mock_table_layouter.return_value = True
custom_style = TableStyle(border_width=1)
result = layouter.layout_table(self.mock_table, style=custom_style)
# Verify the function was called correctly
mock_table_layouter.assert_called_once_with(
self.mock_table, self.mock_page, custom_style
)
assert result is True
def test_document_layouter_layout_document_with_table(self):
"""Test DocumentLayouter.layout_document with Table elements."""
from pyWebLayout.abstract import Paragraph
from pyWebLayout.abstract.block import Image as AbstractImage
# Create mixed elements
elements = [
Mock(spec=Paragraph),
Mock(spec=Table),
Mock(spec=AbstractImage),
Mock(spec=Table)
]
with patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry'):
layouter = DocumentLayouter(self.mock_page)
# Mock the layout methods
layouter.layout_paragraph = Mock(return_value=(True, None, None))
layouter.layout_table = Mock(return_value=True)
layouter.layout_image = Mock(return_value=True)
result = layouter.layout_document(elements)
# Verify all elements were laid out
assert result is True
assert layouter.layout_paragraph.call_count == 1
assert layouter.layout_table.call_count == 2
assert layouter.layout_image.call_count == 1
def test_document_layouter_layout_document_table_failure(self):
"""Test DocumentLayouter.layout_document when table layout fails."""
# Create elements with table that will fail
elements = [
Mock(spec=Table),
Mock(spec=Table)
]
with patch('pyWebLayout.layout.document_layouter.ConcreteStyleRegistry'):
layouter = DocumentLayouter(self.mock_page)
# Mock layout_table: first succeeds, second fails
layouter.layout_table = Mock(side_effect=[True, False])
result = layouter.layout_document(elements)
# Verify it stopped after failure
assert result is False
assert layouter.layout_table.call_count == 2
if __name__ == "__main__": if __name__ == "__main__":
# Run specific tests for debugging # Run specific tests for debugging
test = TestDocumentLayouter() test = TestDocumentLayouter()