dtourolleandClaude Opus 5 e000068384 Cache word widths and glyph bitmaps to cut page render time ~3.5x
Rendering a page re-measured and re-rasterised the same words constantly: at
1404x1872 a page issued ~2800 textlength calls and ~2500 draw.text calls for
fewer than 1000 distinct (font, string) pairs. Profiling a page turn showed
FreeType glyph rendering at 57% of total time and width measurement at 38% of
layout.

Both are now cached. Measured on Crime and Punishment at 1404x1872, one page:

    layout  35ms -> 19ms
    render  84ms -> 41ms
    total  120ms -> 60ms

Eviction ranks by use count rather than recency. Word frequency in prose is
Zipfian and stationary, so the words worth keeping are the ones used most, and
unlike recency this lets a document's own frequencies be seeded up front --
see prewarm_caches(). Two details keep the policy from costing more than it
saves, since get() runs once per word drawn:

  - counting is O(1) with no reordering, because structures that reorder on
    every hit measured 3-5ms/page slower than the hit rate they bought;
  - eviction samples 8 entries and drops the least used of those, rather than
    maintaining a global order.

Aging (halving all counts periodically) is on by default. Without it a font
size change drove the hit rate to 0% on a real access trace: every key was new
and the previous size's entries held counts nothing could beat.

Both caches are bounded, since the glyph bitmaps reach ~19MB over a long
session and the target is a 512MB Pi Zero 2. Defaults are 4MB of bitmaps and
8192 widths; configure_text_caches() tunes them. Cache size barely affects
speed (2MB is within 13% of unbounded) because a miss costs only one ~44us
rasterisation, so the bound can be set for memory, not throughput.

EreaderLayoutManager.prewarm_caches() counts the book's word frequencies and
preloads the most common ones, seeding each with its document frequency. This
moves that rasterisation to open time and cut misses by 27%, for ~15% faster
page turns at a one-off ~300ms cost. It is opt-in; nothing calls it yet.

Rendering is no longer bit-identical. PIL positions text at sub-pixel offsets,
so the cache buckets that phase, defaulting to 2 buckets per axis. Total ink
per page is unchanged and the mean pixel difference is 3.6/255 -- a fifth of
one step of a 16-level e-ink panel. subpixel_steps=4 halves that if wanted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 20:49:32 +02:00
2025-11-09 09:16:10 +01:00
2025-11-08 23:46:15 +01:00
2025-06-07 17:33:11 +02:00
2025-11-11 18:19:49 +01:00
2025-06-08 14:08:29 +02:00
2025-05-27 11:58:19 +02:00
2025-06-08 17:19:14 +02:00
2025-06-07 19:56:11 +02:00
2025-11-11 12:44:18 +01:00
2025-11-08 23:59:53 +01:00
2025-11-08 23:59:53 +01:00

PyWebLayout

Project Status

Badge Description
Test Coverage Test Coverage - Percentage of code covered by unit tests
Documentation Coverage Documentation Coverage - Percentage of code with docstrings
License 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

pip install pyWebLayout

Quick Start

Basic Page Rendering

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

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:

Page Styles
Page Rendering
Different borders, padding, and backgrounds
HTML Content
Text Layout
Parsed HTML with various text styles
Page Layouts
Page Layouts
Portrait, landscape, and square formats
Table Rendering
Table Rendering
HTML tables with headers and styling
Interactive Elements
Interactive Elements
Buttons, forms, and callback binding
🆕 Pagination & PageBreak
Pagination
Multi-page documents with explicit and automatic breaks
🆕 Link Navigation
Links
All 4 link types: Internal, External, API, Function
🆕 Comprehensive Forms
Forms
All 14 form field types with validation
🆕 Dynamic Font Family Switching
Font Switching
Switch between Sans, Serif, and Monospace fonts instantly

Examples

The examples/ directory contains working demonstrations:

Getting Started

🆕 Advanced Features (NEW)

Run any example:

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:

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 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

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 for complete documentation.

Documentation

License

MIT License

Author

Duncan Tourolle - duncan@tourolle.paris

S
Description
No description provided
Readme MIT
58 MiB
Languages
Python 100%