diff --git a/docs/images/example_06_html_table_with_images.png b/docs/images/example_06_html_table_with_images.png
new file mode 100644
index 0000000..3384603
Binary files /dev/null and b/docs/images/example_06_html_table_with_images.png differ
diff --git a/examples/05_table_with_images_html.py b/examples/05_table_with_images_html.py
new file mode 100644
index 0000000..419742d
--- /dev/null
+++ b/examples/05_table_with_images_html.py
@@ -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"""
+
+
+
+
+ {title}
+
+
+
+
+
{title}
+"""
+
+
+def create_html_footer() -> str:
+ """Create HTML document footer."""
+ return """
+
+
+
+
+"""
+
+
+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 = '\n'
+ html += '
Book Catalog
\n'
+ html += '
\n'
+ html += ' \n'
+ html += ' \n'
+ html += ' | Cover | \n'
+ html += ' Title | \n'
+ html += ' Author | \n'
+ html += ' Price | \n'
+ html += '
\n'
+ html += ' \n'
+ html += ' \n'
+
+ for cover_file, title, author, price in books:
+ html += ' \n'
+
+ # Cover cell
+ html += ' \n'
+ if cover_file in cover_images:
+ html += f' \n'
+ html += ' | \n'
+
+ # Title cell
+ html += f' {title} | \n'
+
+ # Author cell
+ html += f' {author} | \n'
+
+ # Price cell
+ html += f' {price} | \n'
+
+ html += '
\n'
+
+ html += ' \n'
+ html += '
\n'
+ html += '
\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 = '\n'
+ html += '
Product Showcase
\n'
+ html += '
\n'
+ html += ' \n'
+ html += ' \n'
+ html += ' | Product | \n'
+ html += ' Description | \n'
+ html += '
\n'
+ html += ' \n'
+ html += ' \n'
+
+ for cover_file, description in products:
+ html += ' \n'
+
+ # Product cell with image
+ html += ' \n'
+ if cover_file in cover_images:
+ html += f' \n'
+ html += ' | \n'
+
+ # Description cell
+ html += f' {description} | \n'
+
+ html += '
\n'
+
+ html += ' \n'
+ html += '
\n'
+ html += '
\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()
diff --git a/examples/06_html_table_with_images.py b/examples/06_html_table_with_images.py
new file mode 100644
index 0000000..f8af442
--- /dev/null
+++ b/examples/06_html_table_with_images.py
@@ -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
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
tags."""
+ # Get base path for images - use absolute paths for the img src
+ data_path = Path(__file__).parent.parent / "tests" / "data"
+
+ html = f"""
+
+
+
+
+
+ | Cover |
+ Title |
+ Author |
+ Price |
+
+
+
+
+  |
+ The Great Adventure |
+ John Smith |
+ $19.99 |
+
+
+  |
+ Mystery of the Ages |
+ Jane Doe |
+ $24.99 |
+
+
+  |
+ Science Today |
+ Dr. Brown |
+ $29.99 |
+
+
+  |
+ Art & Design |
+ M. Artist |
+ $34.99 |
+
+
+
+
+
+ """
+ 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"""
+
+
+
+
+
+ | Product |
+ Description |
+
+
+
+
+  |
+ Premium Edition - Hardcover with gold embossing |
+
+
+  |
+ Collector's Item - Limited print run |
+
+
+
+
+
+ """
+ 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
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
tags inside 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 tags
+ print("\n Creating HTML sources with 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 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()
diff --git a/pyWebLayout/layout/document_layouter.py b/pyWebLayout/layout/document_layouter.py
index a72b614..a35446b 100644
--- a/pyWebLayout/layout/document_layouter.py
+++ b/pyWebLayout/layout/document_layouter.py
@@ -5,8 +5,9 @@ from typing import List, Tuple, Optional, Union
from pyWebLayout.concrete import Page, Line, Text
from pyWebLayout.concrete.image import RenderableImage
from pyWebLayout.concrete.functional import LinkText
+from pyWebLayout.concrete.table import TableRenderer, TableStyle
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.style.concrete_style import ConcreteStyleRegistry, RenderingContext, StyleResolver
from pyWebLayout.style import Font, Alignment
@@ -288,14 +289,65 @@ def image_layouter(image: AbstractImage, page: Page, max_width: Optional[int] =
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:
"""
Document layouter that orchestrates layout of various abstract elements.
Delegates to specialized layouters for different content types:
- paragraph_layouter for text paragraphs
- - image_layouter for images (future)
- - table_layouter for tables (future)
+ - image_layouter for images
+ - table_layouter for tables
This class acts as a coordinator, managing the overall document flow
and page context while delegating specific layout tasks to specialized
@@ -305,12 +357,20 @@ class DocumentLayouter:
def __init__(self, page: Page):
"""
Initialize the document layouter with a page.
-
+
Args:
page: The page to layout content on
"""
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,
pretext: Optional[Text] = None) -> Tuple[bool, Optional[int], Optional[Text]]:
@@ -327,33 +387,46 @@ class DocumentLayouter:
"""
return paragraph_layouter(paragraph, self.page, start_word, pretext)
- def layout_image(self, image: AbstractImage, max_width: Optional[int] = None,
+ def layout_image(self, image: AbstractImage, max_width: Optional[int] = None,
max_height: Optional[int] = None) -> bool:
"""
Layout an image using the image_layouter.
-
+
Args:
image: The abstract Image object to layout
max_width: Maximum width constraint (defaults to page available width)
max_height: Maximum height constraint (defaults to remaining page height)
-
+
Returns:
bool: True if image was successfully laid out, False if page ran out of space
"""
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:
- Paragraphs are handled by layout_paragraph
- Images are handled by layout_image
- - Tables and other elements can be added in the future
-
+ - Tables are handled by layout_table
+
Args:
elements: List of abstract elements to layout
-
+
Returns:
True if all elements were successfully laid out, False otherwise
"""
@@ -366,6 +439,9 @@ class DocumentLayouter:
success = self.layout_image(element)
if not success:
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
return True
diff --git a/tests/layouter/test_document_layouter.py b/tests/layouter/test_document_layouter.py
index deacefd..2182653 100644
--- a/tests/layouter/test_document_layouter.py
+++ b/tests/layouter/test_document_layouter.py
@@ -9,9 +9,11 @@ import pytest
from unittest.mock import Mock, MagicMock, patch
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.concrete_style import ConcreteStyle, StyleResolver, RenderingContext
+from pyWebLayout.abstract.block import Table, TableRow, TableCell
+from pyWebLayout.concrete.table import TableStyle
class TestDocumentLayouter:
@@ -577,6 +579,179 @@ class TestMultiPageLayout:
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__":
# Run specific tests for debugging
test = TestDocumentLayouter()
|