From 03918fc716a5253b5e6095e334a3de352dcc9b9e Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Fri, 7 Nov 2025 19:45:47 +0100 Subject: [PATCH] complete the table rendering --- README.md | 22 +- docs/images/example_04_table_rendering.png | Bin 0 -> 30859 bytes docs/images/example_05_table_with_images.png | Bin 0 -> 128494 bytes examples/04_table_rendering.py | 369 +++++++++++++++ examples/05_table_with_images.py | 252 +++++++++++ examples/README.md | 34 ++ pyWebLayout/concrete/__init__.py | 1 + pyWebLayout/concrete/table.py | 444 +++++++++++++++++++ 8 files changed, 1116 insertions(+), 6 deletions(-) create mode 100644 docs/images/example_04_table_rendering.png create mode 100644 docs/images/example_05_table_with_images.png create mode 100644 examples/04_table_rendering.py create mode 100644 examples/05_table_with_images.py create mode 100644 pyWebLayout/concrete/table.py 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
- Page Rendering
+ Page Rendering
Different borders, padding, and backgrounds
+ HTML Content
- Text Layout
+ Text Layout
Parsed HTML with various text styles
+
Page Layouts
- Page Layouts
+ Page Layouts
Portrait, landscape, and square formats
+ Table Rendering
+ 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 0000000000000000000000000000000000000000..97d4aedb6308a0af1046d8025a9631db9978fa05 GIT binary patch literal 30859 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