Matches the convention used by pyPhotoAlbum and the other projects here:
runs-on: linux/amd64 with a container image from the Gitea registry,
instead of setup-python plus an ad-hoc `pip install pytest pytest-cov
flake8 coverage-badge interrogate` on a self-hosted runner.
pyWebLayout is a library, so the image carries all four interpreters
pyproject.toml claims to support - 3.10, 3.11, 3.12, 3.13 - each in its
own venv at /opt/py<version> with every dependency pre-installed. The
matrix picks one per job. A CI run now downloads nothing, and coverage
widens from 3.10/3.12/3.13 to the full declared range.
Ubuntu marks its system Python externally-managed, so per-interpreter
venvs are used rather than --break-system-packages; that also keeps the
four dependency sets isolated.
Two defects in the existing workflow are fixed while rewriting it:
- pytest runs under continue-on-error so the badge steps still execute,
but nothing afterwards checked its outcome - the job reported green on
a red suite. An explicit gate now fails the job.
- Every matrix leg ran the badge steps and force-pushed the badges
branch, so three jobs raced to publish. Badges and artifacts are now
produced by the 3.13 leg only.
setuptools is pinned below 81 in the image: that release dropped
pkg_resources, which coverage-badge imports at startup, and without the
pin the badge step dies with ModuleNotFoundError. Found by running the
workflow's own commands in the image rather than assuming they work.
Also raises the test Flask server's readiness budget from 5s to 30s.
Making that check raise instead of silently falling through (737cf07)
turned runner load into a hard failure; it showed up as 17 spurious
errors in one containerised run and did not reproduce in three repeats.
The loop still exits as soon as the server answers.
Verified locally: image builds, and tests/ passes 916 on each of 3.10,
3.11, 3.12 and 3.13 inside it. The publishing leg was run end to end -
clean-install dependency check, pytest with coverage, both badges,
coverage summary at 81.5%.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
268 lines
10 KiB
Markdown
268 lines
10 KiB
Markdown
# PyWebLayout
|
|
|
|
## Project Status
|
|
|
|
|
|
| Badge | Description |
|
|
|-------|-------------|
|
|
|  | **Test Coverage** - Percentage of code covered by unit tests |
|
|
|  | **Documentation Coverage** - Percentage of code with docstrings |
|
|
|  | **License** - Project licensing information |
|
|
A Python library for HTML-like layout and rendering.
|
|
> 📋 **Note**: Badges show results from the commit referenced in the URLs. Red "error" badges indicate build failures for that specific step.
|
|
## Description
|
|
|
|
PyWebLayout is a Python library for HTML-like layout and rendering to paginated images. It provides a flexible page rendering system with support for borders, padding, text layout, and HTML parsing.
|
|
|
|
## Key Features
|
|
|
|
### Page Rendering System
|
|
- 📄 **Flexible Page Layouts** - Create pages with customizable sizes, borders, and padding
|
|
- 🎨 **Styling System** - Control backgrounds, border colors, and spacing
|
|
- 📐 **Multiple Layouts** - Support for portrait, landscape, and square pages
|
|
- 🖼️ **Image Output** - Render pages to PIL Images (PNG, JPEG, etc.)
|
|
|
|
### Text and HTML Support
|
|
- 📝 **HTML Parsing** - Parse HTML content into structured document blocks
|
|
- 🔤 **Font Support** - Multiple font sizes, weights, and styles
|
|
- 🎨 **Dynamic Font Families** - Switch between Sans, Serif, and Monospace fonts on-the-fly
|
|
- ↔️ **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
|
|
- 🔘 **Interactive Elements** - Buttons, forms, and links with callback support
|
|
|
|
### Architecture
|
|
- **Abstract/Concrete Separation** - Clean separation between content structure and rendering
|
|
- **Extensible Design** - Easy to extend with custom renderables
|
|
- **Type-safe** - Comprehensive type hints throughout the codebase
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
pip install pyWebLayout
|
|
```
|
|
|
|
## Quick Start
|
|
|
|
### Basic Page Rendering
|
|
|
|
```python
|
|
from pyWebLayout.concrete.page import Page
|
|
from pyWebLayout.style.page_style import PageStyle
|
|
|
|
# Create a styled page
|
|
page_style = PageStyle(
|
|
border_width=2,
|
|
border_color=(200, 200, 200),
|
|
padding=(30, 30, 30, 30), # top, right, bottom, left
|
|
background_color=(255, 255, 255)
|
|
)
|
|
|
|
page = Page(size=(600, 800), style=page_style)
|
|
|
|
# Render to image
|
|
image = page.render()
|
|
image.save("my_page.png")
|
|
```
|
|
|
|
### HTML Content Parsing
|
|
|
|
```python
|
|
from pyWebLayout.io.readers.html_extraction import parse_html_string
|
|
from pyWebLayout.style import Font
|
|
|
|
# Parse HTML to structured blocks
|
|
html = """
|
|
<h1>Document Title</h1>
|
|
<p>First paragraph with <b>bold</b> text.</p>
|
|
<p>Second paragraph with more content.</p>
|
|
"""
|
|
|
|
base_font = Font(font_size=14)
|
|
blocks = parse_html_string(html, base_font=base_font)
|
|
|
|
# blocks is a list of structured content (Paragraph, Heading, etc.)
|
|
```
|
|
|
|
## Visual Examples
|
|
|
|
The library supports various page layouts and configurations:
|
|
|
|
<table>
|
|
<tr>
|
|
<td align="center" width="50%">
|
|
<b>Page Styles</b><br>
|
|
<img src="docs/images/example_01_page_rendering.png" width="300" alt="Page Rendering"><br>
|
|
<em>Different borders, padding, and backgrounds</em>
|
|
</td>
|
|
<td align="center" width="50%">
|
|
<b>HTML Content</b><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>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td align="center" width="50%">
|
|
<b>Page Layouts</b><br>
|
|
<img src="docs/images/example_03_page_layouts.png" width="300" alt="Page Layouts"><br>
|
|
<em>Portrait, landscape, and square formats</em>
|
|
</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>
|
|
<td align="center" colspan="2">
|
|
<b>Interactive Elements</b><br>
|
|
<img src="docs/images/example_06_functional_elements.png" width="300" alt="Interactive Elements"><br>
|
|
<em>Buttons, forms, and callback binding</em>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td align="center" width="50%">
|
|
<b>🆕 Pagination & PageBreak</b><br>
|
|
<img src="docs/images/example_08_pagination_explicit.png" width="300" alt="Pagination"><br>
|
|
<em>Multi-page documents with explicit and automatic breaks</em>
|
|
</td>
|
|
<td align="center" width="50%">
|
|
<b>🆕 Link Navigation</b><br>
|
|
<img src="docs/images/example_09_link_navigation.png" width="300" alt="Links"><br>
|
|
<em>All 4 link types: Internal, External, API, Function</em>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td align="center" colspan="2">
|
|
<b>🆕 Comprehensive Forms</b><br>
|
|
<img src="docs/images/example_10_forms.png" width="300" alt="Forms"><br>
|
|
<em>All 14 form field types with validation</em>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td align="center" colspan="2">
|
|
<b>🆕 Dynamic Font Family Switching</b><br>
|
|
<img src="docs/images/font_family_switching_vertical.png" width="600" alt="Font Switching"><br>
|
|
<em>Switch between Sans, Serif, and Monospace fonts instantly</em>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
## Examples
|
|
|
|
The `examples/` directory contains working demonstrations:
|
|
|
|
### Getting Started
|
|
- **[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_html_table_with_images.py](examples/05_html_table_with_images.py)** - Tables with embedded images
|
|
- **[06_functional_elements_demo.py](examples/06_functional_elements_demo.py)** - Interactive buttons and forms with callbacks
|
|
- **[08_bundled_fonts_demo.py](examples/08_bundled_fonts_demo.py)** - Using the bundled DejaVu font families
|
|
|
|
### 🆕 Advanced Features (NEW)
|
|
- **[08_pagination_demo.py](examples/08_pagination_demo.py)** - Multi-page documents with PageBreak ([11 tests](tests/examples/test_08_pagination_demo.py))
|
|
- **[09_link_navigation_demo.py](examples/09_link_navigation_demo.py)** - All link types and navigation ([10 tests](tests/examples/test_09_link_navigation_demo.py))
|
|
- **[10_forms_demo.py](examples/10_forms_demo.py)** - All 14 form field types ([9 tests](tests/examples/test_10_forms_demo.py))
|
|
- **[11_font_family_switching_demo.py](examples/11_font_family_switching_demo.py)** - 🆕 Dynamic font switching in ereader
|
|
|
|
Run any example:
|
|
```bash
|
|
cd examples
|
|
python 01_simple_page_rendering.py
|
|
python 08_pagination_demo.py # NEW: Multi-page documents
|
|
```
|
|
|
|
**All new examples include comprehensive test coverage!** Run tests with:
|
|
```bash
|
|
python -m pytest tests/examples/ -v # 30 tests, all passing ✅
|
|
```
|
|
|
|
**Coverage Impact:** The new examples fill critical documentation gaps:
|
|
- **PageBreak:** 0% → 100% (had NO examples before)
|
|
- **LinkText:** 14% → 100% (all 4 link types demonstrated)
|
|
- **FormFields:** 14% → 100% (all 14 field types demonstrated)
|
|
|
|
See **[examples/README.md](examples/README.md)** for detailed documentation.
|
|
|
|
## Font Family Switching (NEW ✨)
|
|
|
|
PyWebLayout now supports dynamic font family switching in the ereader, allowing readers to change fonts on-the-fly without losing their reading position!
|
|
|
|
### Quick Example
|
|
|
|
```python
|
|
from pyWebLayout.style.fonts import BundledFont
|
|
from pyWebLayout.layout.ereader_manager import create_ereader_manager
|
|
|
|
# Create an ereader
|
|
manager = create_ereader_manager(blocks, page_size=(600, 800))
|
|
|
|
# Switch to serif font
|
|
manager.set_font_family(BundledFont.SERIF)
|
|
|
|
# Switch to monospace font
|
|
manager.set_font_family(BundledFont.MONOSPACE)
|
|
|
|
# Restore original fonts
|
|
manager.set_font_family(None)
|
|
|
|
# Query current font
|
|
current = manager.get_font_family()
|
|
```
|
|
|
|
### Features
|
|
|
|
- **3 Bundled Fonts**: Sans, Serif, and Monospace (DejaVu font family)
|
|
- **Instant Switching**: Change fonts without recreating the document
|
|
- **Position Preservation**: Reading position maintained across font changes
|
|
- **Attribute Preservation**: Bold, italic, size, and color are preserved
|
|
- **Smart Caching**: Automatic cache invalidation for optimal performance
|
|
|
|
**Learn more**: See [FONT_SWITCHING_FEATURE.md](FONT_SWITCHING_FEATURE.md) for complete documentation.
|
|
|
|
## Documentation
|
|
|
|
- **[ARCHITECTURE.md](ARCHITECTURE.md)** - Abstract/Concrete architecture guide
|
|
- **[FONT_SWITCHING_FEATURE.md](FONT_SWITCHING_FEATURE.md)** - 🆕 Font family switching guide
|
|
- **[examples/README.md](examples/README.md)** - Complete examples guide with tests
|
|
- **[docs/images/README.md](docs/images/README.md)** - Visual documentation index
|
|
- **[pyWebLayout/layout/README_EREADER_API.md](pyWebLayout/layout/README_EREADER_API.md)** - EbookReader API reference
|
|
- **API Reference** - See docstrings in source code
|
|
|
|
## Continuous integration
|
|
|
|
CI runs in a prebuilt container image rather than installing dependencies per
|
|
job. The image carries Python 3.10, 3.11, 3.12 and 3.13, each in its own venv at
|
|
`/opt/py<version>` with every dependency installed, so a run downloads nothing
|
|
and the test matrix covers the whole range `pyproject.toml` claims to support.
|
|
|
|
Rebuild and push the image whenever `Dockerfile.ci` changes — most often
|
|
because a dependency was added to `pyproject.toml`:
|
|
|
|
```bash
|
|
docker build -f Dockerfile.ci -t gitea.tourolle.paris/dtourolle/pyweblayout-ci:latest .
|
|
docker push gitea.tourolle.paris/dtourolle/pyweblayout-ci:latest
|
|
```
|
|
|
|
To reproduce a CI job locally:
|
|
|
|
```bash
|
|
docker run --rm -v "$PWD:/src:ro" gitea.tourolle.paris/dtourolle/pyweblayout-ci:latest bash -c '
|
|
mkdir -p /work && cp -a /src/. /work/ && cd /work && rm -rf venv .git
|
|
/opt/py3.13/bin/pip install -e . --no-deps -q
|
|
/opt/py3.13/bin/python -m pytest tests/ -q'
|
|
```
|
|
|
|
The workflow is [.gitea/workflows/ci.yml](.gitea/workflows/ci.yml). Badges and
|
|
coverage artifacts are published from the 3.13 leg only.
|
|
|
|
## License
|
|
|
|
MIT License
|
|
|
|
## Author
|
|
|
|
Duncan Tourolle - duncan@tourolle.paris
|