Update coverage badges [skip ci]

This commit is contained in:
Gitea Action
2026-08-08 20:35:15 +00:00
commit 735face593
312 changed files with 91102 additions and 0 deletions
+217
View File
@@ -0,0 +1,217 @@
#!/usr/bin/env python3
"""
Simple Page Rendering Example
This example demonstrates:
- Creating pages with different styles
- Setting borders, padding, and background colors
- Understanding the page layout system
- Rendering pages to images
This is a foundational example showing the basic Page API.
"""
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.concrete.page import Page
import sys
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
# Add pyWebLayout to path
sys.path.insert(0, str(Path(__file__).parent.parent))
def draw_placeholder_content(page: Page):
"""Draw some placeholder content directly on the page to visualize the layout."""
if page.draw is None:
# Trigger canvas creation
page.render()
draw = page.draw
# Draw content area boundary (for visualization)
content_x = page.border_size + page.style.padding_left
content_y = page.border_size + page.style.padding_top
content_w = page.content_size[0]
content_h = page.content_size[1]
# Draw a light blue rectangle showing the content area
draw.rectangle(
[content_x, content_y, content_x + content_w, content_y + content_h],
outline=(100, 150, 255),
width=1
)
# Add some text labels
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 12)
except BaseException:
font = ImageFont.load_default()
# Label the areas
draw.text(
(content_x + 10,
content_y + 10),
"Content Area",
fill=(
100,
100,
100),
font=font)
draw.text(
(10, 10), f"Border: {page.border_size}px", fill=(
150, 150, 150), font=font)
draw.text(
(content_x + 10,
content_y + 30),
f"Size: {content_w}x{content_h}",
fill=(
100,
100,
100),
font=font)
def create_example_1():
"""Example 1: Default page style."""
print("\n Creating Example 1: Default style...")
page = Page(size=(400, 300))
draw_placeholder_content(page)
return page
def create_example_2():
"""Example 2: Page with visible borders."""
print(" Creating Example 2: With borders...")
page_style = PageStyle(
border_width=3,
border_color=(255, 100, 100),
padding=(20, 20, 20, 20),
background_color=(255, 250, 250)
)
page = Page(size=(400, 300), style=page_style)
draw_placeholder_content(page)
return page
def create_example_3():
"""Example 3: Page with generous padding."""
print(" Creating Example 3: With padding...")
page_style = PageStyle(
border_width=2,
border_color=(100, 100, 255),
padding=(40, 40, 40, 40),
background_color=(250, 250, 255)
)
page = Page(size=(400, 300), style=page_style)
draw_placeholder_content(page)
return page
def create_example_4():
"""Example 4: Clean, borderless design."""
print(" Creating Example 4: Borderless...")
page_style = PageStyle(
border_width=0,
padding=(30, 30, 30, 30),
background_color=(245, 245, 245)
)
page = Page(size=(400, 300), style=page_style)
draw_placeholder_content(page)
return page
def combine_into_grid(pages, title):
"""Combine multiple pages into a 2x2 grid with title."""
print("\n Combining pages into grid...")
# Render all pages
images = [page.render() for page in pages]
# Grid layout
padding = 20
title_height = 40
cols = 2
rows = 2
# Calculate dimensions
img_width = images[0].size[0]
img_height = images[0].size[1]
total_width = cols * img_width + (cols + 1) * padding
total_height = rows * img_height + (rows + 1) * padding + title_height
# Create combined image
combined = Image.new('RGB', (total_width, total_height), (250, 250, 250))
draw = ImageDraw.Draw(combined)
# Draw title
try:
title_font = ImageFont.truetype(
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 20)
except BaseException:
title_font = ImageFont.load_default()
# Center the title
bbox = draw.textbbox((0, 0), title, font=title_font)
text_width = bbox[2] - bbox[0]
title_x = (total_width - text_width) // 2
draw.text((title_x, 10), title, fill=(50, 50, 50), font=title_font)
# Place pages in grid
y_offset = title_height + padding
for row in range(rows):
x_offset = padding
for col in range(cols):
idx = row * cols + col
if idx < len(images):
combined.paste(images[idx], (x_offset, y_offset))
x_offset += img_width + padding
y_offset += img_height + padding
return combined
def main():
"""Demonstrate basic page rendering."""
print("Simple Page Rendering Example")
print("=" * 50)
# Create different page examples
pages = [
create_example_1(),
create_example_2(),
create_example_3(),
create_example_4()
]
# Combine into a single demonstration image
combined_image = combine_into_grid(pages, "Page Styles: Border & Padding Examples")
# Save output
output_dir = Path("docs/images")
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / "example_01_page_rendering.png"
combined_image.save(output_path)
print("\n✓ Example completed!")
print(f" Output saved to: {output_path}")
print(f" Image size: {combined_image.size[0]}x{combined_image.size[1]} pixels")
print(f" Created {len(pages)} page examples")
return combined_image
if __name__ == "__main__":
main()
+215
View File
@@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""
Text and Layout Example
This example demonstrates text rendering using the pyWebLayout system:
- Different text alignments
- Font sizes and styles
- Multi-line paragraphs
- Document layout and pagination
This example uses the HTML parsing system to create rich text layouts.
"""
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.concrete.page import Page
from pyWebLayout.style import Font
from pyWebLayout.io.readers.html_extraction import parse_html_string
import sys
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
# Add pyWebLayout to path
sys.path.insert(0, str(Path(__file__).parent.parent))
def create_sample_document():
"""Create different HTML samples demonstrating various features."""
samples = []
# Sample 1: Text alignment examples
samples.append((
"Text Alignment",
"""
<html><body>
<h2>Left Aligned</h2>
<p>This is left-aligned text. It is the default alignment for most text.</p>
<h2>Justified Text</h2>
<p style="text-align: justify;">This paragraph is justified. The text stretches to fill
the entire width of the line, creating clean edges on both sides.</p>
<h2>Centered</h2>
<p style="text-align: center;">This text is centered.</p>
</body></html>
"""
))
# Sample 2: Font sizes
samples.append((
"Font Sizes",
"""
<html><body>
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<p>Normal paragraph text at the default size.</p>
<p><small>Small text for fine print.</small></p>
</body></html>
"""
))
# Sample 3: Text styles
samples.append((
"Text Styles",
"""
<html><body>
<p>Normal text with <b>bold words</b> and <i>italic text</i>.</p>
<p><b>Completely bold paragraph.</b></p>
<p><i>Completely italic paragraph.</i></p>
<p>Text with <u>underlined words</u> for emphasis.</p>
</body></html>
"""
))
# Sample 4: Mixed content
samples.append((
"Mixed Content",
"""
<html><body>
<h2>Document Title</h2>
<p>A paragraph with <b>bold</b>, <i>italic</i>, and normal text all mixed together.</p>
<h3>Subsection</h3>
<p>Another paragraph demonstrating the layout system.</p>
</body></html>
"""
))
return samples
def render_html_to_image(html_content, page_size=(500, 400)):
"""Render HTML content to an image using the pyWebLayout system."""
# Create a page
page_style = PageStyle(
border_width=2,
border_color=(200, 200, 200),
padding=(30, 30, 30, 30),
background_color=(255, 255, 255)
)
page = Page(size=page_size, style=page_style)
# Parse HTML
base_font = Font(font_size=14)
blocks = parse_html_string(html_content, base_font=base_font)
# For now, just render the page structure
# (The full layout engine would place the blocks, but we'll show the page)
image = page.render()
draw = ImageDraw.Draw(image)
# Add a note that this is HTML-parsed content
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 11)
except BaseException:
font = ImageFont.load_default()
# Draw info about what was parsed
content_x = page.border_size + page.style.padding_left + 10
content_y = page.border_size + page.style.padding_top + 10
draw.text((content_x, content_y),
f"Parsed {len(blocks)} block(s) from HTML",
fill=(100, 100, 100), font=font)
# List the block types
y_offset = content_y + 25
for i, block in enumerate(blocks[:10]): # Show first 10
block_type = type(block).__name__
draw.text((content_x, y_offset),
f" {i + 1}. {block_type}",
fill=(60, 60, 60), font=font)
y_offset += 18
if y_offset > page.size[1] - 60: # Don't overflow
break
return image
def combine_samples(samples):
"""Combine multiple sample renders into a grid."""
print("\n Rendering samples...")
images = []
for title, html in samples:
print(f" - {title}")
img = render_html_to_image(html)
# Add title to image
draw = ImageDraw.Draw(img)
try:
font = ImageFont.truetype(
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 14)
except BaseException:
font = ImageFont.load_default()
draw.text((10, 10), title, fill=(50, 50, 150), font=font)
images.append(img)
# Create grid (2x2)
padding = 20
cols = 2
rows = 2
img_width = images[0].size[0]
img_height = images[0].size[1]
total_width = cols * img_width + (cols + 1) * padding
total_height = rows * img_height + (rows + 1) * padding
combined = Image.new('RGB', (total_width, total_height), (240, 240, 240))
# Place images
y_offset = padding
for row in range(rows):
x_offset = padding
for col in range(cols):
idx = row * cols + col
if idx < len(images):
combined.paste(images[idx], (x_offset, y_offset))
x_offset += img_width + padding
y_offset += img_height + padding
return combined
def main():
"""Demonstrate text and layout features."""
print("Text and Layout Example")
print("=" * 50)
# Create sample documents
samples = create_sample_document()
# Render and combine
combined_image = combine_samples(samples)
# Save output
output_dir = Path("docs/images")
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / "example_02_text_and_layout.png"
combined_image.save(output_path)
print("\n✓ Example completed!")
print(f" Output saved to: {output_path}")
print(f" Image size: {combined_image.size[0]}x{combined_image.size[1]} pixels")
print(" Note: This example demonstrates HTML parsing")
print(" Full layout rendering requires the typesetting engine")
return combined_image
if __name__ == "__main__":
main()
+247
View File
@@ -0,0 +1,247 @@
#!/usr/bin/env python3
"""
Page Layouts Example
This example demonstrates different page layout configurations:
- Various page sizes (small, medium, large)
- Different aspect ratios (portrait, landscape, square)
- Border and padding variations
- Color schemes
Shows how the pyWebLayout system handles different page dimensions.
"""
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.concrete.page import Page
import sys
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
# Add pyWebLayout to path
sys.path.insert(0, str(Path(__file__).parent.parent))
def add_page_info(page: Page, title: str):
"""Add informational text to a page showing its properties."""
if page.draw is None:
page.render()
draw = page.draw
try:
font_large = ImageFont.truetype(
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 14)
font_small = ImageFont.truetype(
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 11)
except BaseException:
font_large = ImageFont.load_default()
font_small = ImageFont.load_default()
# Title
content_x = page.border_size + page.style.padding_left + 5
content_y = page.border_size + page.style.padding_top + 5
draw.text((content_x, content_y), title, fill=(40, 40, 40), font=font_large)
# Page info
y = content_y + 25
info = [
f"Page: {page.size[0]}×{page.size[1]}px",
f"Content: {page.content_size[0]}×{page.content_size[1]}px",
f"Border: {page.border_size}px",
f"Padding: {page.style.padding}",
]
for line in info:
draw.text((content_x, y), line, fill=(80, 80, 80), font=font_small)
y += 16
# Draw content area boundary
cx = page.border_size + page.style.padding_left
cy = page.border_size + page.style.padding_top
cw = page.content_size[0]
ch = page.content_size[1]
draw.rectangle(
[cx, cy, cx + cw, cy + ch],
outline=(150, 150, 255),
width=1
)
def create_layouts():
"""Create various page layout examples."""
layouts = []
# 1. Small portrait page
print("\n Creating layout examples...")
print(" - Small portrait")
style1 = PageStyle(
border_width=2,
border_color=(100, 100, 100),
padding=(15, 15, 15, 15),
background_color=(255, 255, 255)
)
page1 = Page(size=(300, 400), style=style1)
add_page_info(page1, "Small Portrait")
layouts.append(("small_portrait", page1))
# 2. Large portrait page
print(" - Large portrait")
style2 = PageStyle(
border_width=3,
border_color=(150, 100, 100),
padding=(30, 30, 30, 30),
background_color=(255, 250, 250)
)
page2 = Page(size=(400, 600), style=style2)
add_page_info(page2, "Large Portrait")
layouts.append(("large_portrait", page2))
# 3. Landscape page
print(" - Landscape")
style3 = PageStyle(
border_width=2,
border_color=(100, 150, 100),
padding=(20, 40, 20, 40),
background_color=(250, 255, 250)
)
page3 = Page(size=(600, 350), style=style3)
add_page_info(page3, "Landscape")
layouts.append(("landscape", page3))
# 4. Square page
print(" - Square")
style4 = PageStyle(
border_width=3,
border_color=(100, 100, 150),
padding=(25, 25, 25, 25),
background_color=(250, 250, 255)
)
page4 = Page(size=(400, 400), style=style4)
add_page_info(page4, "Square")
layouts.append(("square", page4))
# 5. Minimal padding
print(" - Minimal padding")
style5 = PageStyle(
border_width=1,
border_color=(180, 180, 180),
padding=(5, 5, 5, 5),
background_color=(245, 245, 245)
)
page5 = Page(size=(350, 300), style=style5)
add_page_info(page5, "Minimal Padding")
layouts.append(("minimal", page5))
# 6. Generous padding
print(" - Generous padding")
style6 = PageStyle(
border_width=2,
border_color=(150, 120, 100),
padding=(50, 50, 50, 50),
background_color=(255, 250, 245)
)
page6 = Page(size=(400, 400), style=style6)
add_page_info(page6, "Generous Padding")
layouts.append(("generous", page6))
return layouts
def create_layout_showcase(layouts):
"""Create a showcase image displaying all layouts."""
print("\n Creating layout showcase...")
# Render all pages
images = [(name, page.render()) for name, page in layouts]
# Calculate grid layout (3×2)
padding = 15
title_height = 50
cols = 3
rows = 2
# Find max dimensions for each row/column
max_widths = []
for col in range(cols):
col_images = [images[row * cols + col][1]
for row in range(rows) if row * cols + col < len(images)]
if col_images:
max_widths.append(max(img.size[0] for img in col_images))
max_heights = []
for row in range(rows):
row_images = [images[row * cols + col][1]
for col in range(cols) if row * cols + col < len(images)]
if row_images:
max_heights.append(max(img.size[1] for img in row_images))
# Calculate total size
total_width = sum(max_widths) + padding * (cols + 1)
total_height = sum(max_heights) + padding * (rows + 1) + title_height
# Create combined image
combined = Image.new('RGB', (total_width, total_height), (235, 235, 235))
draw = ImageDraw.Draw(combined)
# Add title
try:
title_font = ImageFont.truetype(
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 24)
except BaseException:
title_font = ImageFont.load_default()
title_text = "Page Layout Examples"
bbox = draw.textbbox((0, 0), title_text, font=title_font)
text_width = bbox[2] - bbox[0]
title_x = (total_width - text_width) // 2
draw.text((title_x, 15), title_text, fill=(50, 50, 50), font=title_font)
# Place images in grid
y_offset = title_height + padding
for row in range(rows):
x_offset = padding
for col in range(cols):
idx = row * cols + col
if idx < len(images):
name, img = images[idx]
# Center image in its cell
cell_width = max_widths[col]
cell_height = max_heights[row]
img_x = x_offset + (cell_width - img.size[0]) // 2
img_y = y_offset + (cell_height - img.size[1]) // 2
combined.paste(img, (img_x, img_y))
x_offset += max_widths[col] + padding if col < len(max_widths) else 0
y_offset += max_heights[row] + padding if row < len(max_heights) else 0
return combined
def main():
"""Demonstrate page layout variations."""
print("Page Layouts Example")
print("=" * 50)
# Create different layouts
layouts = create_layouts()
# Create showcase
combined_image = create_layout_showcase(layouts)
# Save output
output_dir = Path("docs/images")
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / "example_03_page_layouts.png"
combined_image.save(output_path)
print("\n✓ Example completed!")
print(f" Output saved to: {output_path}")
print(f" Image size: {combined_image.size[0]}x{combined_image.size[1]} pixels")
print(f" Created {len(layouts)} layout examples")
return combined_image
if __name__ == "__main__":
main()
+364
View File
@@ -0,0 +1,364 @@
#!/usr/bin/env python3
"""
Table Rendering Example
This example demonstrates rendering HTML tables:
- Simple tables with headers
- Tables with multiple rows and columns
- Tables with colspan and borders
- Tables with formatted content
- Tables parsed from HTML using DocumentLayouter
Shows the HTML-first rendering pipeline.
"""
from pyWebLayout.abstract.block import Table
from pyWebLayout.style import Font
from pyWebLayout.io.readers.html_extraction import parse_html_string
from pyWebLayout.layout.document_layouter import DocumentLayouter
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.concrete.table import TableStyle
from pyWebLayout.concrete.page import Page
import sys
from pathlib import Path
from PIL import Image, ImageDraw
# Add pyWebLayout to path
sys.path.insert(0, str(Path(__file__).parent.parent))
def create_simple_table_example():
"""Create a simple table from HTML."""
print(" - Simple data table")
html = """
<table>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th>City</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice</td>
<td>28</td>
<td>Paris</td>
</tr>
<tr>
<td>Bob</td>
<td>34</td>
<td>London</td>
</tr>
<tr>
<td>Charlie</td>
<td>25</td>
<td>Tokyo</td>
</tr>
</tbody>
</table>
"""
return html, "Simple Table"
def create_styled_table_example():
"""Create a table with custom styling."""
print(" - Styled table")
html = """
<table>
<caption>Monthly Sales Report</caption>
<thead>
<tr>
<th>Month</th>
<th>Revenue</th>
<th>Expenses</th>
<th>Profit</th>
</tr>
</thead>
<tbody>
<tr>
<td>January</td>
<td>$50,000</td>
<td>$30,000</td>
<td>$20,000</td>
</tr>
<tr>
<td>February</td>
<td>$55,000</td>
<td>$32,000</td>
<td>$23,000</td>
</tr>
<tr>
<td>March</td>
<td>$60,000</td>
<td>$35,000</td>
<td>$25,000</td>
</tr>
</tbody>
</table>
"""
return html, "Styled Table"
def create_complex_table_example():
"""Create a table with colspan."""
print(" - Complex table with colspan")
html = """
<table>
<caption>Product Specifications</caption>
<thead>
<tr>
<th>Product</th>
<th>Features</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr>
<td>Laptop</td>
<td>16GB RAM, 512GB SSD</td>
<td>$1,299</td>
</tr>
<tr>
<td>Monitor</td>
<td>27 inch, 4K Resolution</td>
<td>$599</td>
</tr>
<tr>
<td>Keyboard</td>
<td>Mechanical, RGB</td>
<td>$129</td>
</tr>
</tbody>
</table>
"""
return html, "Complex Table"
def create_data_table_example():
"""Create a table with numerical data."""
print(" - Data table")
html = """
<table>
<caption>Test Results</caption>
<thead>
<tr>
<th>Test</th>
<th>Score</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>Unit Tests</td>
<td>98%</td>
<td>Pass</td>
</tr>
<tr>
<td>Integration</td>
<td>95%</td>
<td>Pass</td>
</tr>
<tr>
<td>Performance</td>
<td>87%</td>
<td>Pass</td>
</tr>
</tbody>
</table>
"""
return html, "Data Table"
def render_table_example(
html: str,
title: str,
style_variant: int = 0,
page_size=(
500,
400)):
"""Render a table from HTML to an image using DocumentLayouter."""
# Create page with varying backgrounds
bg_colors = [
(255, 255, 255), # White
(250, 255, 250), # Light green tint
(255, 250, 245), # Light orange tint
(245, 250, 255), # Light blue tint
]
page_style = PageStyle(
border_width=2,
border_color=(200, 200, 200),
padding=(20, 20, 20, 20),
background_color=bg_colors[style_variant % len(bg_colors)]
)
page = Page(size=page_size, style=page_style)
# Parse HTML to get table
base_font = Font(font_size=12)
blocks = parse_html_string(html, base_font=base_font)
# Find the table block
table = None
for block in blocks:
if isinstance(block, Table):
table = block
break
# Table styles with different themes
table_styles = [
# Style 0: Classic blue header
TableStyle(
border_width=1,
border_color=(80, 80, 80),
cell_padding=(8, 10, 8, 10),
header_bg_color=(70, 130, 180), # Steel blue
cell_bg_color=(255, 255, 255),
alternate_row_color=(240, 248, 255) # Alice blue
),
# Style 1: Green theme
TableStyle(
border_width=2,
border_color=(34, 139, 34), # Forest green
cell_padding=(10, 12, 10, 12),
header_bg_color=(144, 238, 144), # Light green
cell_bg_color=(255, 255, 255),
alternate_row_color=(240, 255, 240) # Honeydew
),
# Style 2: Minimal style
TableStyle(
border_width=0,
border_color=(200, 200, 200),
cell_padding=(6, 8, 6, 8),
header_bg_color=(245, 245, 245),
cell_bg_color=(255, 255, 255),
alternate_row_color=None # No alternating
),
# Style 3: Bold borders
TableStyle(
border_width=3,
border_color=(0, 0, 0),
cell_padding=(10, 10, 10, 10),
header_bg_color=(255, 215, 0), # Gold
cell_bg_color=(255, 255, 255),
alternate_row_color=(255, 250, 205) # Lemon chiffon
),
]
table_style = table_styles[style_variant % len(table_styles)]
if table:
# Create DocumentLayouter
layouter = DocumentLayouter(page)
# Use DocumentLayouter to layout the table
layouter.layout_table(table, style=table_style)
# Get the rendered canvas
_ = page.draw # Ensure canvas exists
image = page._canvas
else:
# No table found - create empty page
_ = page.draw # Ensure canvas exists
draw = ImageDraw.Draw(page._canvas)
draw.text((page.border_size + 10, page.border_size + 50),
"No table found in HTML",
fill=(200, 0, 0))
image = page._canvas
return image
def combine_examples(examples):
"""Combine multiple table examples into a grid."""
print("\n Rendering table examples...")
images = []
for i, (html, title) in enumerate(examples):
img = render_table_example(html, title, style_variant=i)
images.append(img)
# Create grid (2x2)
padding = 15
cols = 2
rows = 2
img_width = images[0].size[0]
img_height = images[0].size[1]
total_width = cols * img_width + (cols + 1) * padding
total_height = rows * img_height + (rows + 1) * padding + 50 # Extra for main title
combined = Image.new('RGB', (total_width, total_height), (240, 240, 240))
draw = ImageDraw.Draw(combined)
# Add main title
from PIL import ImageFont
try:
main_font = ImageFont.truetype(
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 20)
except BaseException:
main_font = ImageFont.load_default()
title_text = "Table Rendering Examples"
bbox = draw.textbbox((0, 0), title_text, font=main_font)
text_width = bbox[2] - bbox[0]
title_x = (total_width - text_width) // 2
draw.text((title_x, 15), title_text, fill=(50, 50, 50), font=main_font)
# Place images
y_offset = 50 + padding
for row in range(rows):
x_offset = padding
for col in range(cols):
idx = row * cols + col
if idx < len(images):
combined.paste(images[idx], (x_offset, y_offset))
x_offset += img_width + padding
y_offset += img_height + padding
return combined
def main():
"""Demonstrate table rendering."""
print("Table Rendering Example")
print("=" * 50)
# Create table examples
print("\n Creating table examples...")
examples = [
create_simple_table_example(),
create_styled_table_example(),
create_complex_table_example(),
create_data_table_example()
]
# Render and combine
combined_image = combine_examples(examples)
# Save output
output_dir = Path("docs/images")
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / "example_04_table_rendering.png"
combined_image.save(output_path)
print("\n✓ Example completed!")
print(f" Output saved to: {output_path}")
print(f" Image size: {combined_image.size[0]}x{combined_image.size[1]} pixels")
print(f" Created {len(examples)} table examples")
return combined_image
if __name__ == "__main__":
main()
+272
View File
@@ -0,0 +1,272 @@
#!/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!
"""
from pyWebLayout.style import Font
from pyWebLayout.concrete.table import TableStyle
from pyWebLayout.layout.document_layouter import DocumentLayouter
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.concrete.page import Page
from pyWebLayout.io.readers.html_extraction import parse_html_string
import sys
from pathlib import Path
from PIL import Image
# Add pyWebLayout to path
sys.path.insert(0, str(Path(__file__).parent.parent))
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(" ✓ 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_05_html_table_with_images.png"
combined.save(output_path)
print("\n✓ Example completed!")
print(f" Output saved to: {output_path}")
print(f" Image size: {combined.size[0]}x{combined.size[1]} pixels")
print("\nThe complete pipeline:")
print(" 1. HTML with <img> tags → parse_html_string() → Abstract blocks")
print(" 2. Abstract blocks → DocumentLayouter → Concrete objects")
print(" 3. Page.render() → PNG output")
print("\n ✓ Using DocumentLayouter - NO custom rendering code!")
return combined
if __name__ == "__main__":
main()
+292
View File
@@ -0,0 +1,292 @@
"""
Demonstration of functional elements (buttons, forms, links) with callback binding.
This example shows how to:
1. Create functional elements programmatically
2. Layout them on a page
3. Bind callbacks after layout using the CallbackRegistry
4. Simulate user interactions
This pattern is useful for:
- Manual GUI construction
- Applications where callbacks need access to runtime state
- Interactive document interfaces
"""
from pyWebLayout.concrete import Page
from pyWebLayout.abstract.functional import Button, Form, FormField, FormFieldType
from pyWebLayout.abstract import Paragraph, Word
from pyWebLayout.style import Font
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.layout.document_layouter import DocumentLayouter
import numpy as np
class SimpleApp:
"""
A simple application that demonstrates functional element usage.
This app has:
- A settings form
- Save and Cancel buttons
- Application state that callbacks can access
"""
def __init__(self):
self.settings = {
"username": "",
"theme": "light",
"notifications": True
}
self.saved = False
def on_save_click(self, point, **kwargs):
"""Callback for save button"""
print(f"Save button clicked at {point}")
self.saved = True
print("Settings saved!")
return "saved"
def on_cancel_click(self, point, **kwargs):
"""Callback for cancel button"""
print(f"Cancel button clicked at {point}")
print("Changes cancelled!")
return "cancelled"
def on_reset_click(self, point, **kwargs):
"""Callback for reset button"""
print(f"Reset button clicked at {point}")
self.settings = {
"username": "",
"theme": "light",
"notifications": True
}
print("Settings reset to defaults!")
return "reset"
def create_settings_page():
"""
Create a settings page with functional elements.
Returns:
Tuple of (page, app, element_ids) where element_ids maps
semantic names to registered callback ids
"""
# Create the application instance
app = SimpleApp()
# Create page
page = Page(size=(600, 800), style=PageStyle(border_width=10))
layouter = DocumentLayouter(page)
# Create content
font = Font(font_size=16, colour=(0, 0, 0))
# Title paragraph
title_font = Font(font_size=24, colour=(0, 0, 100))
title = Paragraph(title_font)
title.add_word(Word("Settings", title_font))
# Description paragraph
desc = Paragraph(font)
desc.add_word(Word("Configure", font))
desc.add_word(Word("your", font))
desc.add_word(Word("application", font))
desc.add_word(Word("preferences", font))
desc.add_word(Word("below.", font))
# Layout title and description
layouter.layout_paragraph(title)
page._current_y_offset += 10 # Add some spacing
layouter.layout_paragraph(desc)
page._current_y_offset += 20 # Add more spacing before form
# Create form
settings_form = Form(
form_id="settings-form",
action="/save-settings",
html_id="settings-form"
)
# Add form fields
username_field = FormField(
name="username",
field_type=FormFieldType.TEXT,
label="Username",
value="john_doe"
)
theme_field = FormField(
name="theme",
field_type=FormFieldType.SELECT,
label="Theme",
value="light",
options=[("light", "Light"), ("dark", "Dark")]
)
notifications_field = FormField(
name="notifications",
field_type=FormFieldType.CHECKBOX,
label="Enable Notifications",
value=True
)
settings_form.add_field(username_field)
settings_form.add_field(theme_field)
settings_form.add_field(notifications_field)
# Layout the form
success, field_ids = layouter.layout_form(settings_form)
if not success:
print("Warning: Form didn't fit on page!")
page._current_y_offset += 20 # Spacing before buttons
# Create buttons (NO callbacks yet - will be bound later)
save_button = Button(
label="Save Settings",
callback=None, # No callback yet!
html_id="save-btn"
)
cancel_button = Button(
label="Cancel",
callback=None, # No callback yet!
html_id="cancel-btn"
)
reset_button = Button(
label="Reset to Defaults",
callback=None, # No callback yet!
html_id="reset-btn"
)
# Layout buttons
button_font = Font(font_size=14, colour=(255, 255, 255))
success1, save_id = layouter.layout_button(save_button, font=button_font)
page._current_y_offset += 10 # Spacing between buttons
success2, cancel_id = layouter.layout_button(cancel_button, font=button_font)
page._current_y_offset += 10
success3, reset_id = layouter.layout_button(reset_button, font=button_font)
# ==============================================================
# IMPORTANT: Callbacks are bound AFTER layout is complete
# This allows callbacks to access the application instance
# ==============================================================
# Bind callbacks using the page's callback registry
page.callbacks.set_callback("save-btn", app.on_save_click)
page.callbacks.set_callback("cancel-btn", app.on_cancel_click)
page.callbacks.set_callback("reset-btn", app.on_reset_click)
# Track element ids for later reference
element_ids = {
"save_button": save_id,
"cancel_button": cancel_id,
"reset_button": reset_id,
"form_fields": field_ids
}
return page, app, element_ids
def demonstrate_callback_binding():
"""Demonstrate various callback binding patterns"""
print("=" * 60)
print("Functional Elements Demo: Manual GUI Construction")
print("=" * 60)
print()
# Create the page
page, app, element_ids = create_settings_page()
print(f"Page created with {page.callbacks.count()} interactable elements")
print()
# Show what's registered
print("Registered interactables:")
for id_name in page.callbacks.get_all_ids():
print(f" - {id_name}")
print()
# Show breakdown by type
print("Breakdown by type:")
for type_name in page.callbacks.get_all_types():
count = page.callbacks.count_by_type(type_name)
print(f" - {type_name}: {count}")
print()
# Simulate user clicking the save button
print("Simulating user interaction:")
print("-" * 60)
print()
# Get the save button
save_button = page.callbacks.get_by_id("save-btn")
print(f"Retrieved save button: {save_button}")
# Simulate a click at position (50, 200)
click_point = np.array([50, 200])
print(f"Simulating click at {click_point}...")
result = save_button.interact(click_point)
print(f"Button interaction returned: {result}")
print(f"App.saved state: {app.saved}")
print()
# Simulate clicking cancel
cancel_button = page.callbacks.get_by_id("cancel-btn")
print("Simulating cancel button click...")
result = cancel_button.interact(click_point)
print(f"Button interaction returned: {result}")
print()
# Simulate clicking reset
reset_button = page.callbacks.get_by_id("reset-btn")
print("Simulating reset button click...")
result = reset_button.interact(click_point)
print(f"Button interaction returned: {result}")
print()
# Demonstrate batch callback modification
print("-" * 60)
print("Demonstrating batch callback modification:")
print()
def log_all_clicks(point, **kwargs):
"""Generic click logger"""
print(f" [LOG] Button clicked at {point}")
return "logged"
# Set this callback for all buttons
count = page.callbacks.set_callbacks_by_type("button", log_all_clicks)
print(f"Set logging callback for {count} buttons")
print()
# Now clicking any button will just log
print("Clicking save button again (now with logging callback):")
result = save_button.interact(click_point)
print(f"Returned: {result}")
print()
# Render the page
print("-" * 60)
print("Rendering page...")
image = page.render()
print(f"Page rendered: {image.size}")
# Save to file
output_path = "functional_elements_demo.png"
image.save(output_path)
print(f"Saved to: {output_path}")
print()
print("=" * 60)
print("Demo complete!")
print("=" * 60)
if __name__ == "__main__":
demonstrate_callback_binding()
+452
View File
@@ -0,0 +1,452 @@
"""
Demonstration of pressed/depressed states for buttons and links with visual feedback.
This example shows:
1. How to use the InteractionHandler for automatic press/release cycles
2. How to manually manage states for custom event loops
3. How the dirty flag system tracks when re-rendering is needed
4. Visual differences between normal, hovered, and pressed states
The demo creates a page with buttons and links, then simulates clicking them
with proper visual feedback timing.
"""
from pyWebLayout.concrete import Page
from pyWebLayout.concrete.interaction_handler import InteractionHandler, InteractionStateManager
from pyWebLayout.abstract.functional import Button, Link, LinkType
from pyWebLayout.abstract import Paragraph, Word
from pyWebLayout.abstract.inline import LinkedWord
from pyWebLayout.style import Font
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.layout.document_layouter import DocumentLayouter
import numpy as np
import time
def create_interactive_demo_page():
"""
Create a page with various interactive elements demonstrating state changes.
"""
# Create page
page = Page(size=(600, 500), style=PageStyle(border_width=10))
layouter = DocumentLayouter(page)
# Create fonts
title_font = Font(font_size=24, colour=(0, 0, 100))
body_font = Font(font_size=16, colour=(0, 0, 0))
button_font = Font(font_size=14, colour=(255, 255, 255))
# Title
title = Paragraph(title_font)
title.add_word(Word("Interactive", title_font))
title.add_word(Word("Elements", title_font))
title.add_word(Word("Demo", title_font))
layouter.layout_paragraph(title)
page._current_y_offset += 15
# Description
desc = Paragraph(body_font)
desc.add_word(Word("Click", body_font))
desc.add_word(Word("the", body_font))
desc.add_word(Word("buttons", body_font))
desc.add_word(Word("and", body_font))
desc.add_word(Word("links", body_font))
desc.add_word(Word("below", body_font))
desc.add_word(Word("to", body_font))
desc.add_word(Word("see", body_font))
desc.add_word(Word("pressed", body_font))
desc.add_word(Word("state", body_font))
desc.add_word(Word("feedback!", body_font))
layouter.layout_paragraph(desc)
page._current_y_offset += 20
# Callback functions
def on_save():
print("💾 Save button clicked!")
return "saved"
def on_cancel():
print("❌ Cancel button clicked!")
return "cancelled"
def on_link_click(location, point):
print(f"🔗 Link clicked: {location} at {point}")
return location
# Create buttons
save_button = Button(
label="Save Document",
callback=lambda point, **kwargs: on_save(),
html_id="save-btn"
)
cancel_button = Button(
label="Cancel",
callback=lambda point, **kwargs: on_cancel(),
html_id="cancel-btn"
)
# Layout buttons
success1, save_id = layouter.layout_button(save_button, font=button_font)
page._current_y_offset += 12
success2, cancel_id = layouter.layout_button(cancel_button, font=button_font)
page._current_y_offset += 25
# Create paragraph with links
link_para = Paragraph(body_font)
link_para.add_word(Word("Visit", body_font))
link_para.add_word(Word("our", body_font))
# Add a link
internal_link = Link(
location="https://example.com",
link_type=LinkType.EXTERNAL,
callback=on_link_click,
title="Example website"
)
link_para.add_word(LinkedWord(
"website",
body_font,
location="https://example.com",
link_type=LinkType.EXTERNAL,
callback=on_link_click,
title="Example website"
))
link_para.add_word(Word("or", body_font))
# Add another link
docs_link = Link(
location="/docs",
link_type=LinkType.INTERNAL,
callback=on_link_click,
title="Documentation"
)
link_para.add_word(LinkedWord(
"documentation",
body_font,
location="/docs",
link_type=LinkType.INTERNAL,
callback=on_link_click,
title="Documentation"
))
link_para.add_word(Word("page.", body_font))
layouter.layout_paragraph(link_para)
return page, save_id, cancel_id
def demo_automatic_interaction():
"""
Demonstrate automatic interaction handling with InteractionHandler.
This shows the simplest usage pattern where InteractionHandler manages
the complete press/release cycle automatically.
"""
print("=" * 70)
print("Demo 1: Automatic Interaction with Visual Feedback")
print("=" * 70)
print()
# Create the page
page, save_id, cancel_id = create_interactive_demo_page()
# Create interaction handler
handler = InteractionHandler(page, press_duration_ms=150)
print("Initial render:")
initial_render = page.render()
initial_render.save("demo_07_initial.png")
print(f" ✓ Saved: demo_07_initial.png")
print(f" ✓ Page dirty flag: {page.is_dirty}")
print()
# Get the save button
save_button = page.callbacks.get_by_id("save-btn")
click_point = np.array([50, 150])
print("Simulating button click with automatic feedback...")
print(f" → Setting pressed state at t=0ms")
# Execute with automatic feedback
pressed_frame, released_frame, result = handler.execute_with_feedback(
save_button,
click_point
)
print(f" → Showing pressed state for 150ms")
print(f" → Executing callback")
print(f" → Result: {result}")
print(f" → Setting released state")
# Save the frames
pressed_frame.save("demo_07_pressed.png")
print(f" ✓ Saved: demo_07_pressed.png")
released_frame.save("demo_07_released.png")
print(f" ✓ Saved: demo_07_released.png")
print()
def demo_manual_state_management():
"""
Demonstrate manual state management for custom event loops.
This shows how an application with its own event loop can manage
states and check the dirty flag before re-rendering.
"""
print("=" * 70)
print("Demo 2: Manual State Management with Dirty Flag Checking")
print("=" * 70)
print()
# Create the page
page, save_id, cancel_id = create_interactive_demo_page()
# Initial render
print("Initial render:")
current_frame = page.render()
print(f" ✓ Page dirty: {page.is_dirty} (cleaned after render)")
print()
# Get the cancel button
cancel_button = page.callbacks.get_by_id("cancel-btn")
# Simulate mouse down
print("Mouse down event:")
# Set page reference if not already set
if not hasattr(cancel_button, '_page') or cancel_button._page is None:
cancel_button.set_page(page)
cancel_button.set_pressed(True)
print(f" ✓ Set pressed state")
print(f" ✓ Page dirty: {page.is_dirty} (needs re-render)")
# Check if we need to re-render
if page.is_dirty:
print(" → Re-rendering (dirty flag is set)")
current_frame = page.render()
current_frame.save("demo_07_manual_pressed.png")
print(f" ✓ Saved: demo_07_manual_pressed.png")
print(f" ✓ Page dirty: {page.is_dirty} (cleaned after render)")
print()
# Wait a bit
print("Waiting 150ms for visual feedback...")
time.sleep(0.15)
print()
# Execute callback
print("Executing callback:")
result = cancel_button.interact(np.array([50, 200]))
print(f" ✓ Result: {result}")
print()
# Simulate mouse up
print("Mouse up event:")
cancel_button.set_pressed(False)
print(f" ✓ Set released state")
print(f" ✓ Page dirty: {page.is_dirty} (needs re-render)")
# Check if we need to re-render
if page.is_dirty:
print(" → Re-rendering (dirty flag is set)")
current_frame = page.render()
current_frame.save("demo_07_manual_released.png")
print(f" ✓ Saved: demo_07_manual_released.png")
print(f" ✓ Page dirty: {page.is_dirty} (cleaned after render)")
print()
def demo_state_manager():
"""
Demonstrate the InteractionStateManager for hover/press tracking.
This shows how to use the high-level state manager that automatically
handles hover and press states based on cursor position.
"""
print("=" * 70)
print("Demo 3: InteractionStateManager for Hover and Press Tracking")
print("=" * 70)
print()
# Create the page
page, save_id, cancel_id = create_interactive_demo_page()
# Create state manager
state_mgr = InteractionStateManager(page)
# Initial render
print("Initial render:")
current_frame = page.render()
print(f" ✓ Rendered initial state")
print()
# Simulate cursor moving over a button
button_center = (150, 150)
print(f"Cursor moves to button position {button_center}:")
hover_frame = state_mgr.update_hover(button_center)
if hover_frame:
print(f" ✓ Hover state changed, page re-rendered")
hover_frame.save("demo_07_hover.png")
print(f" ✓ Saved: demo_07_hover.png")
print()
# Simulate mouse down
print(f"Mouse down at {button_center}:")
pressed_frame = state_mgr.handle_mouse_down(button_center)
if pressed_frame:
print(f" ✓ Pressed state set, page re-rendered")
pressed_frame.save("demo_07_state_mgr_pressed.png")
print(f" ✓ Saved: demo_07_state_mgr_pressed.png")
print()
# Wait for visual feedback
time.sleep(0.15)
# Simulate mouse up
print(f"Mouse up at {button_center}:")
released_frame, result = state_mgr.handle_mouse_up(button_center)
if released_frame:
print(f" ✓ Released state set, page re-rendered")
print(f" ✓ Callback result: {result}")
released_frame.save("demo_07_state_mgr_released.png")
print(f" ✓ Saved: demo_07_state_mgr_released.png")
print()
# Simulate cursor moving away
away_point = (50, 50)
print(f"Cursor moves away to {away_point}:")
away_frame = state_mgr.update_hover(away_point)
if away_frame:
print(f" ✓ Hover state cleared, page re-rendered")
away_frame.save("demo_07_no_hover.png")
print(f" ✓ Saved: demo_07_no_hover.png")
print()
def demo_performance_optimization():
"""
Demonstrate how the dirty flag prevents unnecessary re-renders.
"""
print("=" * 70)
print("Demo 4: Performance Optimization with Dirty Flag")
print("=" * 70)
print()
# Create the page
page, save_id, cancel_id = create_interactive_demo_page()
print("Scenario: Multiple state queries without changes")
print()
# Initial render
page.render()
print(f"1. After initial render - dirty: {page.is_dirty}")
# Check if dirty before rendering again
print(f"2. Check dirty flag: {page.is_dirty}")
if not page.is_dirty:
print(" → Skipping render (no changes)")
print()
# Now make a change
button = page.callbacks.get_by_id("save-btn")
print("3. Setting button to pressed state")
# Ensure page reference is set
if not hasattr(button, '_page') or button._page is None:
button.set_page(page)
button.set_pressed(True)
print(f" → dirty: {page.is_dirty}")
print()
# This time we need to render
print(f"4. Check dirty flag: {page.is_dirty}")
if page.is_dirty:
print(" → Re-rendering (state changed)")
page.render()
print(f" → dirty after render: {page.is_dirty}")
print()
print("Benefit: Only render when actual changes occur!")
print()
def create_animated_gif():
"""
Create an animated GIF showing the button press sequence.
"""
from PIL import Image
import os
print("=" * 70)
print("Creating Animated GIF")
print("=" * 70)
print()
# Check if the PNG files exist
png_files = [
"demo_07_initial.png",
"demo_07_pressed.png",
"demo_07_released.png"
]
if not all(os.path.exists(f) for f in png_files):
print(" ⚠ PNG files not found, skipping GIF creation")
return
# Load the images
initial = Image.open('demo_07_initial.png')
pressed = Image.open('demo_07_pressed.png')
released = Image.open('demo_07_released.png')
# Create animated GIF showing the button interaction sequence
# Sequence: initial (1000ms) -> pressed (200ms) -> released (500ms) -> loop
frames = [initial, pressed, released]
durations = [1000, 200, 500] # milliseconds per frame
output_path = "docs/images/example_07_button_animation.gif"
# Create docs/images directory if it doesn't exist
os.makedirs("docs/images", exist_ok=True)
# Save as animated GIF
initial.save(
output_path,
save_all=True,
append_images=[pressed, released],
duration=durations,
loop=0 # 0 means loop forever
)
print(f" ✓ Created: {output_path}")
print(f" ✓ Frames: {len(frames)}")
print(f" ✓ Sequence: initial (1000ms) → pressed (200ms) → released (500ms)")
print()
if __name__ == "__main__":
print("\n")
print("" + "" * 68 + "")
print("" + " " * 15 + "PRESSED STATE DEMONSTRATION" + " " * 26 + "")
print("" + "" * 68 + "")
print()
# Run all demos
demo_automatic_interaction()
print("\n")
demo_manual_state_management()
print("\n")
demo_state_manager()
print("\n")
demo_performance_optimization()
print("\n")
# Create animated GIF
create_animated_gif()
print("=" * 70)
print("All demos complete! Check the generated PNG files and animated GIF.")
print("=" * 70)
+227
View File
@@ -0,0 +1,227 @@
"""
Demonstration of bundled fonts in pyWebLayout.
This example shows:
1. How to use the bundled DejaVu font families
2. Different font variants (regular, bold, italic, bold-italic)
3. The three font families (Sans, Serif, Monospace)
4. Convenient Font.from_family() method for easy font selection
The demo creates a page showcasing all bundled fonts with different styles.
"""
from pyWebLayout.concrete import Page
from pyWebLayout.abstract import Paragraph, Word
from pyWebLayout.style import Font, FontWeight, FontStyle, BundledFont
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.layout.document_layouter import DocumentLayouter
def create_font_showcase_page():
"""
Create a page demonstrating all bundled fonts and variants.
"""
# Create page with some padding
page = Page(size=(800, 1000), style=PageStyle(border_width=20))
layouter = DocumentLayouter(page)
# Title
title_font = Font.from_family(
BundledFont.SANS,
font_size=32,
colour=(0, 0, 100),
weight=FontWeight.BOLD
)
title = Paragraph(title_font)
title.add_word(Word("Bundled", title_font))
title.add_word(Word("Fonts", title_font))
title.add_word(Word("Showcase", title_font))
layouter.layout_paragraph(title)
page._current_y_offset += 20
# Introduction
intro_font = Font.from_family(BundledFont.SANS, font_size=14, colour=(50, 50, 50))
intro = Paragraph(intro_font)
intro_text = "pyWebLayout bundles the DejaVu font family with three font types and four variants each."
for word in intro_text.split():
intro.add_word(Word(word, intro_font))
layouter.layout_paragraph(intro)
page._current_y_offset += 25
# --- Sans Serif Section ---
section_font = Font.from_family(
BundledFont.SANS,
font_size=20,
colour=(0, 100, 0),
weight=FontWeight.BOLD
)
sans_section = Paragraph(section_font)
sans_section.add_word(Word("Sans-Serif", section_font))
sans_section.add_word(Word("(DejaVu", section_font))
sans_section.add_word(Word("Sans)", section_font))
layouter.layout_paragraph(sans_section)
page._current_y_offset += 10
# Sans Regular
sans_regular = Font.from_family(BundledFont.SANS, font_size=16)
demo_text_paragraph(layouter, page, sans_regular, "Regular:")
# Sans Bold
sans_bold = Font.from_family(BundledFont.SANS, font_size=16, weight=FontWeight.BOLD)
demo_text_paragraph(layouter, page, sans_bold, "Bold:")
# Sans Italic
sans_italic = Font.from_family(BundledFont.SANS, font_size=16, style=FontStyle.ITALIC)
demo_text_paragraph(layouter, page, sans_italic, "Italic:")
# Sans Bold Italic
sans_bold_italic = Font.from_family(
BundledFont.SANS,
font_size=16,
weight=FontWeight.BOLD,
style=FontStyle.ITALIC
)
demo_text_paragraph(layouter, page, sans_bold_italic, "Bold Italic:")
page._current_y_offset += 20
# --- Serif Section ---
serif_section = Paragraph(section_font)
serif_section.add_word(Word("Serif", section_font))
serif_section.add_word(Word("(DejaVu", section_font))
serif_section.add_word(Word("Serif)", section_font))
layouter.layout_paragraph(serif_section)
page._current_y_offset += 10
# Serif Regular
serif_regular = Font.from_family(BundledFont.SERIF, font_size=16)
demo_text_paragraph(layouter, page, serif_regular, "Regular:")
# Serif Bold
serif_bold = Font.from_family(BundledFont.SERIF, font_size=16, weight=FontWeight.BOLD)
demo_text_paragraph(layouter, page, serif_bold, "Bold:")
# Serif Italic
serif_italic = Font.from_family(BundledFont.SERIF, font_size=16, style=FontStyle.ITALIC)
demo_text_paragraph(layouter, page, serif_italic, "Italic:")
# Serif Bold Italic
serif_bold_italic = Font.from_family(
BundledFont.SERIF,
font_size=16,
weight=FontWeight.BOLD,
style=FontStyle.ITALIC
)
demo_text_paragraph(layouter, page, serif_bold_italic, "Bold Italic:")
page._current_y_offset += 20
# --- Monospace Section ---
mono_section = Paragraph(section_font)
mono_section.add_word(Word("Monospace", section_font))
mono_section.add_word(Word("(DejaVu", section_font))
mono_section.add_word(Word("Sans", section_font))
mono_section.add_word(Word("Mono)", section_font))
layouter.layout_paragraph(mono_section)
page._current_y_offset += 10
# Mono Regular
mono_regular = Font.from_family(BundledFont.MONOSPACE, font_size=14)
demo_code_paragraph(layouter, page, mono_regular, "Regular:")
# Mono Bold
mono_bold = Font.from_family(BundledFont.MONOSPACE, font_size=14, weight=FontWeight.BOLD)
demo_code_paragraph(layouter, page, mono_bold, "Bold:")
# Mono Italic
mono_italic = Font.from_family(BundledFont.MONOSPACE, font_size=14, style=FontStyle.ITALIC)
demo_code_paragraph(layouter, page, mono_italic, "Italic:")
# Mono Bold Italic
mono_bold_italic = Font.from_family(
BundledFont.MONOSPACE,
font_size=14,
weight=FontWeight.BOLD,
style=FontStyle.ITALIC
)
demo_code_paragraph(layouter, page, mono_bold_italic, "Bold Italic:")
page._current_y_offset += 20
# Footer
footer_font = Font.from_family(BundledFont.SANS, font_size=12, colour=(100, 100, 100))
footer = Paragraph(footer_font)
footer_text = "All fonts are free and open source under the Bitstream Vera License."
for word in footer_text.split():
footer.add_word(Word(word, footer_font))
layouter.layout_paragraph(footer)
return page
def demo_text_paragraph(layouter, page, font, label):
"""Create a paragraph showing sample text with the given font."""
# Label in smaller font
label_font = Font.from_family(BundledFont.SANS, font_size=12, colour=(100, 100, 100))
label_para = Paragraph(label_font)
label_para.add_word(Word(label, label_font))
layouter.layout_paragraph(label_para)
page._current_y_offset += 5
# Sample text
para = Paragraph(font)
sample = "The quick brown fox jumps over the lazy dog. 0123456789"
for word in sample.split():
para.add_word(Word(word, font))
layouter.layout_paragraph(para)
page._current_y_offset += 8
def demo_code_paragraph(layouter, page, font, label):
"""Create a paragraph showing sample code with the given font."""
# Label in smaller font
label_font = Font.from_family(BundledFont.SANS, font_size=12, colour=(100, 100, 100))
label_para = Paragraph(label_font)
label_para.add_word(Word(label, label_font))
layouter.layout_paragraph(label_para)
page._current_y_offset += 5
# Sample code
para = Paragraph(font)
code = "def hello(): print('Hello, World!') # 0123456789"
for word in code.split():
para.add_word(Word(word, font))
layouter.layout_paragraph(para)
page._current_y_offset += 8
if __name__ == "__main__":
print("\n")
print("=" * 70)
print("Bundled Fonts Demonstration")
print("=" * 70)
print()
print("Creating font showcase page...")
page = create_font_showcase_page()
print("Rendering page...")
image = page.render()
output_file = "demo_08_bundled_fonts.png"
image.save(output_file)
print(f"Saved: {output_file}")
print()
print("=" * 70)
print("Demo complete!")
print()
print("The page showcases all bundled fonts:")
print(" - DejaVu Sans (Sans-serif)")
print(" - DejaVu Serif (Serif)")
print(" - DejaVu Sans Mono (Monospace)")
print()
print("Each family has 4 variants:")
print(" - Regular")
print(" - Bold")
print(" - Italic")
print(" - Bold Italic")
print("=" * 70)
print()
+367
View File
@@ -0,0 +1,367 @@
#!/usr/bin/env python3
"""
Pagination Example with PageBreak
This example demonstrates:
- Using PageBreak to force content onto new pages
- Multi-page document layout with automatic page creation
- Different content types across multiple pages
- Page numbering and document flow
- Combining text, images, and tables across pages
This shows how to create multi-page documents with explicit page breaks.
"""
import sys
from pathlib import Path
from PIL import Image, ImageDraw
# Add pyWebLayout to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from pyWebLayout.concrete.page import Page
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.style.fonts import Font
from pyWebLayout.abstract.inline import Word
from pyWebLayout.abstract.block import Paragraph, PageBreak, Image as AbstractImage
from pyWebLayout.layout.document_layouter import DocumentLayouter
def create_sample_paragraph(text: str, font_size: int = 14) -> Paragraph:
"""Create a paragraph from plain text."""
font = Font(font_size=font_size, colour=(50, 50, 50))
paragraph = Paragraph(style=font)
for word in text.split():
paragraph.add_word(Word(word, font))
return paragraph
def create_title_paragraph(text: str) -> Paragraph:
"""Create a title paragraph with larger font."""
font = Font(font_size=24, colour=(0, 0, 100), weight='bold')
paragraph = Paragraph(style=font)
for word in text.split():
paragraph.add_word(Word(word, font))
return paragraph
def create_heading_paragraph(text: str) -> Paragraph:
"""Create a heading paragraph."""
font = Font(font_size=18, colour=(50, 50, 100), weight='bold')
paragraph = Paragraph(style=font)
for word in text.split():
paragraph.add_word(Word(word, font))
return paragraph
def create_placeholder_image(width: int, height: int, label: str) -> AbstractImage:
"""Create a placeholder image for demonstration."""
img = Image.new('RGB', (width, height), (200, 220, 240))
draw = ImageDraw.Draw(img)
# Draw border
draw.rectangle([0, 0, width-1, height-1], outline=(100, 120, 140), width=2)
# Add label
text_bbox = draw.textbbox((0, 0), label)
text_width = text_bbox[2] - text_bbox[0]
text_height = text_bbox[3] - text_bbox[1]
text_x = (width - text_width) // 2
text_y = (height - text_height) // 2
draw.text((text_x, text_y), label, fill=(80, 80, 120))
return AbstractImage(source=img)
def create_example_document_with_pagebreaks():
"""
Example: Multi-page document with explicit page breaks.
This demonstrates how PageBreak forces content onto new pages.
"""
print("\n Creating multi-page document with PageBreaks...")
# Define common page style
page_style = PageStyle(
border_width=2,
border_color=(100, 100, 150),
padding=(30, 40, 30, 40),
background_color=(255, 255, 255),
line_spacing=6
)
# Create document content with page breaks
content = [
# Page 1: Title and Introduction
create_title_paragraph("Multi-Page Document Example"),
create_sample_paragraph(
"This document demonstrates how to use PageBreak elements to control "
"document pagination. Each PageBreak forces subsequent content to start "
"on a new page, allowing you to structure multi-page documents precisely."
),
create_sample_paragraph(
"Page breaks are particularly useful for creating chapters, sections, or "
"ensuring that important content starts at the top of a fresh page rather "
"than being split across page boundaries."
),
# Force page break - next content will be on page 2
PageBreak(),
# Page 2: First Section
create_heading_paragraph("Section 1: Text Content"),
create_sample_paragraph(
"This is the second page of our document. It starts with a clean break "
"from the previous page, ensuring the section heading appears at the top."
),
create_sample_paragraph(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod "
"tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim "
"veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea "
"commodo consequat."
),
create_sample_paragraph(
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum "
"dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non "
"proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
),
# Another page break
PageBreak(),
# Page 3: Images
create_heading_paragraph("Section 2: Visual Content"),
create_sample_paragraph(
"This page contains image content, demonstrating that page breaks work "
"correctly with different content types."
),
create_placeholder_image(300, 200, "Figure 1: Sample Image"),
create_sample_paragraph("The image above is placed on this dedicated page."),
# Final page break
PageBreak(),
# Page 4: Conclusion
create_heading_paragraph("Conclusion"),
create_sample_paragraph(
"This final page demonstrates that you can create complex multi-page "
"documents by strategically placing PageBreak elements in your content."
),
create_sample_paragraph(
"Key benefits of using PageBreak: 1) Control where pages start, "
"2) Prevent awkward content splits, 3) Create professional-looking "
"documents with proper sectioning, 4) Ensure important content gets "
"visual prominence at page tops."
),
create_sample_paragraph(
"Thank you for reviewing this pagination example. Try experimenting "
"with PageBreak placement to create your own multi-page documents!"
),
]
# Layout the document across multiple pages
pages = []
current_page = Page(size=(600, 800), style=page_style)
layouter = DocumentLayouter(current_page)
for element in content:
if isinstance(element, PageBreak):
# Save current page and create a new one
pages.append(current_page)
current_page = Page(size=(600, 800), style=page_style)
layouter = DocumentLayouter(current_page)
elif isinstance(element, Paragraph):
success, _, _ = layouter.layout_paragraph(element)
if not success:
# Page is full, create new page and retry
pages.append(current_page)
current_page = Page(size=(600, 800), style=page_style)
layouter = DocumentLayouter(current_page)
success, _, _ = layouter.layout_paragraph(element)
if not success:
print(" WARNING: Content too large for page")
elif isinstance(element, AbstractImage):
success = layouter.layout_image(element)
if not success:
# Image doesn't fit, try on new page
pages.append(current_page)
current_page = Page(size=(600, 800), style=page_style)
layouter = DocumentLayouter(current_page)
success = layouter.layout_image(element)
if not success:
print(" WARNING: Image too large for page")
# Add the final page
pages.append(current_page)
print(f" Created {len(pages)} pages")
return pages
def create_auto_pagination_example():
"""
Example: Document that automatically flows to multiple pages.
This shows the difference between automatic pagination (when content
doesn't fit) vs explicit PageBreak usage.
"""
print("\n Creating auto-paginated document (no explicit breaks)...")
page_style = PageStyle(
border_width=1,
border_color=(150, 150, 150),
padding=(20, 30, 20, 30),
background_color=(250, 250, 250),
line_spacing=5
)
# Create lots of content that will naturally overflow
content = [
create_heading_paragraph("Auto-Pagination Example"),
create_sample_paragraph(
"This document does NOT use PageBreak. Instead, it demonstrates how "
"content automatically flows to new pages when the current page is full."
),
]
# Add many paragraphs to force automatic page breaks
for i in range(1, 11):
content.append(
create_sample_paragraph(
f"Paragraph {i}: This is automatically laid out content. "
f"When this paragraph doesn't fit on the current page, the layouter "
f"will create a new page automatically. This is different from using "
f"PageBreak which forces a new page regardless of available space. "
f"Auto-pagination is useful for flowing content naturally."
)
)
# Layout across pages
pages = []
current_page = Page(size=(500, 600), style=page_style)
layouter = DocumentLayouter(current_page)
for element in content:
if isinstance(element, Paragraph):
success, _, _ = layouter.layout_paragraph(element)
if not success:
# Auto page break - content didn't fit
pages.append(current_page)
current_page = Page(size=(500, 600), style=page_style)
layouter = DocumentLayouter(current_page)
layouter.layout_paragraph(element)
pages.append(current_page)
print(f" Auto-created {len(pages)} pages")
return pages
def add_page_numbers(pages, start_number: int = 1):
"""Add page numbers to rendered pages."""
numbered_pages = []
font = Font(font_size=10, colour=(100, 100, 100))
for i, page in enumerate(pages, start=start_number):
# Render the page
img = page.render()
draw = ImageDraw.Draw(img)
# Add page number at bottom center
page_text = f"Page {i}"
bbox = draw.textbbox((0, 0), page_text)
text_width = bbox[2] - bbox[0]
x = (img.size[0] - text_width) // 2
y = img.size[1] - 20
draw.text((x, y), page_text, fill=(100, 100, 100))
numbered_pages.append(img)
return numbered_pages
def combine_pages_vertically(pages, title: str = ""):
"""Combine multiple pages into a vertical strip."""
if not pages:
return None
padding = 20
title_height = 40 if title else 0
# Calculate dimensions
page_width = pages[0].size[0]
page_height = pages[0].size[1]
total_width = page_width + 2 * padding
total_height = len(pages) * (page_height + padding) + padding + title_height
# Create combined image
combined = Image.new('RGB', (total_width, total_height), (240, 240, 240))
draw = ImageDraw.Draw(combined)
# Draw title if provided
if title:
from PIL import ImageFont
try:
title_font = ImageFont.truetype(
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 16
)
except:
title_font = ImageFont.load_default()
bbox = draw.textbbox((0, 0), title, font=title_font)
text_width = bbox[2] - bbox[0]
title_x = (total_width - text_width) // 2
draw.text((title_x, 10), title, fill=(50, 50, 50), font=title_font)
# Place pages vertically
y_offset = title_height + padding
for page_img in pages:
combined.paste(page_img, (padding, y_offset))
y_offset += page_height + padding
return combined
def main():
"""Demonstrate pagination with PageBreak."""
print("Pagination Example with PageBreak")
print("=" * 50)
# Example 1: Explicit page breaks
pages1 = create_example_document_with_pagebreaks()
rendered_pages1 = add_page_numbers(pages1)
combined1 = combine_pages_vertically(
rendered_pages1,
"Example 1: Explicit PageBreak Usage"
)
# Example 2: Auto pagination
pages2 = create_auto_pagination_example()
rendered_pages2 = add_page_numbers(pages2)
combined2 = combine_pages_vertically(
rendered_pages2,
"Example 2: Automatic Pagination"
)
# Save outputs
output_dir = Path("docs/images")
output_dir.mkdir(parents=True, exist_ok=True)
output_path1 = output_dir / "example_08_pagination_explicit.png"
output_path2 = output_dir / "example_08_pagination_auto.png"
combined1.save(output_path1)
combined2.save(output_path2)
print("\n✓ Example completed!")
print(f" Output 1 saved to: {output_path1}")
print(f" - {len(pages1)} pages with explicit PageBreaks")
print(f" Output 2 saved to: {output_path2}")
print(f" - {len(pages2)} pages with auto-pagination")
return combined1, combined2
if __name__ == "__main__":
main()
+390
View File
@@ -0,0 +1,390 @@
#!/usr/bin/env python3
"""
Link Navigation Example
This example demonstrates:
- Creating clickable links with LinkedWord
- Different link types (INTERNAL, EXTERNAL, API, FUNCTION)
- Link styling with underlines and colors
- Link callbacks and event handling
- Interactive link states (hover, pressed)
- Organizing linked content in paragraphs
This shows how to create interactive documents with hyperlinks.
"""
import sys
from pathlib import Path
from typing import List
# Add pyWebLayout to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from pyWebLayout.concrete.page import Page
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.style.fonts import Font
from pyWebLayout.abstract.inline import Word, LinkedWord
from pyWebLayout.abstract.functional import LinkType
from pyWebLayout.abstract.block import Paragraph
from pyWebLayout.layout.document_layouter import DocumentLayouter
# Track link clicks for demonstration
link_clicks = []
def link_callback(link_id: str):
"""Callback for link clicks"""
def callback():
link_clicks.append(link_id)
print(f" Link clicked: {link_id}")
return callback
def create_paragraph_with_links(
text_parts: List[tuple],
font_size: int = 14) -> Paragraph:
"""
Create a paragraph with mixed text and links.
Args:
text_parts: List of tuples where each is either:
('text', "word1 word2") for normal text
('link', "word", location, link_type, callback_id)
font_size: Base font size
Returns:
Paragraph with words and links
"""
font = Font(font_size=font_size, colour=(50, 50, 50))
paragraph = Paragraph(style=font)
for part in text_parts:
if part[0] == 'text':
# Add normal words
for word_text in part[1].split():
paragraph.add_word(Word(word_text, font))
elif part[0] == 'link':
# Add linked word
word_text, location, link_type, callback_id = part[1:]
callback = link_callback(callback_id)
linked_word = LinkedWord(
text=word_text,
style=font,
location=location,
link_type=link_type,
callback=callback,
title=f"Click to: {location}"
)
paragraph.add_word(linked_word)
return paragraph
def create_example_1_internal_links():
"""Example 1: Internal navigation links within a document."""
print("\n Creating Example 1: Internal links...")
page_style = PageStyle(
border_width=2,
border_color=(150, 150, 200),
padding=(20, 30, 20, 30),
background_color=(255, 255, 255),
line_spacing=6
)
page = Page(size=(500, 600), style=page_style)
layouter = DocumentLayouter(page)
# Title
title_font = Font(font_size=20, colour=(0, 0, 100), weight='bold')
title = Paragraph(style=title_font)
for word in "Internal Navigation Links".split():
title.add_word(Word(word, title_font))
# Content with internal links
intro = create_paragraph_with_links([
('text', "This document demonstrates"),
('link', "internal", "#section1", LinkType.INTERNAL, "goto_section1"),
('text', "navigation links that jump to different parts of the document."),
])
section1 = create_paragraph_with_links([
('text', "Jump to"),
('link', "Section 2", "#section2", LinkType.INTERNAL, "goto_section2"),
('text', "or"),
('link', "Section 3", "#section3", LinkType.INTERNAL, "goto_section3"),
('text', "within this document."),
])
section2 = create_paragraph_with_links([
('text', "You are in Section 2. Return to"),
('link', "top", "#top", LinkType.INTERNAL, "goto_top"),
('text', "or go to"),
('link', "Section 3", "#section3", LinkType.INTERNAL, "goto_section3_from2"),
])
section3 = create_paragraph_with_links([
('text', "This is Section 3. Go back to"),
('link', "Section 1", "#section1", LinkType.INTERNAL, "goto_section1_from3"),
('text', "or"),
('link', "top", "#top", LinkType.INTERNAL, "goto_top_from3"),
])
# Layout content
layouter.layout_paragraph(title)
layouter.layout_paragraph(intro)
layouter.layout_paragraph(section1)
layouter.layout_paragraph(section2)
layouter.layout_paragraph(section3)
return page
def create_example_2_external_links():
"""Example 2: External links to websites."""
print(" Creating Example 2: External links...")
page_style = PageStyle(
border_width=2,
border_color=(150, 200, 150),
padding=(20, 30, 20, 30),
background_color=(255, 255, 255),
line_spacing=6
)
page = Page(size=(500, 600), style=page_style)
layouter = DocumentLayouter(page)
# Title
title_font = Font(font_size=20, colour=(0, 100, 0), weight='bold')
title = Paragraph(style=title_font)
for word in "External Web Links".split():
title.add_word(Word(word, title_font))
# Content with external links
intro = create_paragraph_with_links([
('text', "Click"),
('link', "here", "https://example.com", LinkType.EXTERNAL, "visit_example"),
('text', "to visit an external website."),
])
resources = create_paragraph_with_links([
('text', "Useful resources:"),
('link', "Documentation", "https://docs.example.com", LinkType.EXTERNAL, "visit_docs"),
('text', "and"),
('link', "GitHub", "https://github.com/example", LinkType.EXTERNAL, "visit_github"),
])
more_links = create_paragraph_with_links([
('text', "Learn more at"),
('link', "Wikipedia", "https://wikipedia.org", LinkType.EXTERNAL, "visit_wiki"),
('text', "or check out"),
('link', "Python.org", "https://python.org", LinkType.EXTERNAL, "visit_python"),
])
# Layout content
layouter.layout_paragraph(title)
layouter.layout_paragraph(intro)
layouter.layout_paragraph(resources)
layouter.layout_paragraph(more_links)
return page
def create_example_3_api_links():
"""Example 3: API links that trigger actions."""
print(" Creating Example 3: API links...")
page_style = PageStyle(
border_width=2,
border_color=(200, 150, 150),
padding=(20, 30, 20, 30),
background_color=(255, 255, 255),
line_spacing=6
)
page = Page(size=(500, 600), style=page_style)
layouter = DocumentLayouter(page)
# Title
title_font = Font(font_size=20, colour=(150, 0, 0), weight='bold')
title = Paragraph(style=title_font)
for word in "API Action Links".split():
title.add_word(Word(word, title_font))
# Content with API links
settings = create_paragraph_with_links([
('text', "Click"),
('link', "Settings", "/api/settings", LinkType.API, "open_settings"),
('text', "to configure the application."),
])
actions = create_paragraph_with_links([
('text', "Actions:"),
('link', "Save", "/api/save", LinkType.API, "save_action"),
('text', "or"),
('link', "Export", "/api/export", LinkType.API, "export_action"),
('text', "your data."),
])
management = create_paragraph_with_links([
('text', "Manage:"),
('link', "Users", "/api/users", LinkType.API, "manage_users"),
('text', "or"),
('link', "Permissions", "/api/permissions", LinkType.API, "manage_perms"),
])
# Layout content
layouter.layout_paragraph(title)
layouter.layout_paragraph(settings)
layouter.layout_paragraph(actions)
layouter.layout_paragraph(management)
return page
def create_example_4_function_links():
"""Example 4: Function links that execute code."""
print(" Creating Example 4: Function links...")
page_style = PageStyle(
border_width=2,
border_color=(150, 200, 200),
padding=(20, 30, 20, 30),
background_color=(255, 255, 255),
line_spacing=6
)
page = Page(size=(500, 600), style=page_style)
layouter = DocumentLayouter(page)
# Title
title_font = Font(font_size=20, colour=(0, 120, 120), weight='bold')
title = Paragraph(style=title_font)
for word in "Function Execution Links".split():
title.add_word(Word(word, title_font))
# Content with function links
intro = create_paragraph_with_links([
('text', "These links execute"),
('link', "functions", "calculate()", LinkType.FUNCTION, "exec_calculate"),
('text', "directly in the application."),
])
calculations = create_paragraph_with_links([
('text', "Run:"),
('link', "analyze()", "analyze()", LinkType.FUNCTION, "exec_analyze"),
('text', "or"),
('link', "process()", "process()", LinkType.FUNCTION, "exec_process"),
])
utilities = create_paragraph_with_links([
('text', "Utilities:"),
('link', "validate()", "validate()", LinkType.FUNCTION, "exec_validate"),
('text', "and"),
('link', "cleanup()", "cleanup()", LinkType.FUNCTION, "exec_cleanup"),
])
# Layout content
layouter.layout_paragraph(title)
layouter.layout_paragraph(intro)
layouter.layout_paragraph(calculations)
layouter.layout_paragraph(utilities)
return page
def combine_pages_into_grid(pages, title):
"""Combine multiple pages into a 2x2 grid."""
from PIL import Image, ImageDraw, ImageFont
print("\n Combining pages into grid...")
# Render all pages
images = [page.render() for page in pages]
# Grid layout
padding = 20
title_height = 40
cols = 2
rows = 2
# Calculate dimensions
img_width = images[0].size[0]
img_height = images[0].size[1]
total_width = cols * img_width + (cols + 1) * padding
total_height = rows * img_height + (rows + 1) * padding + title_height
# Create combined image
combined = Image.new('RGB', (total_width, total_height), (240, 240, 240))
draw = ImageDraw.Draw(combined)
# Draw title
try:
title_font = ImageFont.truetype(
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 18
)
except:
title_font = ImageFont.load_default()
# Center the title
bbox = draw.textbbox((0, 0), title, font=title_font)
text_width = bbox[2] - bbox[0]
title_x = (total_width - text_width) // 2
draw.text((title_x, 10), title, fill=(50, 50, 50), font=title_font)
# Place pages in grid
y_offset = title_height + padding
for row in range(rows):
x_offset = padding
for col in range(cols):
idx = row * cols + col
if idx < len(images):
combined.paste(images[idx], (x_offset, y_offset))
x_offset += img_width + padding
y_offset += img_height + padding
return combined
def main():
"""Demonstrate link navigation across different link types."""
global link_clicks
link_clicks = []
print("Link Navigation Example")
print("=" * 50)
# Create examples for each link type
pages = [
create_example_1_internal_links(),
create_example_2_external_links(),
create_example_3_api_links(),
create_example_4_function_links()
]
# Combine into demonstration image
combined_image = combine_pages_into_grid(
pages,
"Link Types: Internal | External | API | Function"
)
# Save output
output_dir = Path("docs/images")
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / "example_09_link_navigation.png"
combined_image.save(output_path)
print("\n✓ Example completed!")
print(f" Output saved to: {output_path}")
print(f" Image size: {combined_image.size[0]}x{combined_image.size[1]} pixels")
print(f" Created {len(pages)} link type examples")
print(f" Total links created: {len(link_clicks)} callbacks registered")
return combined_image, link_clicks
if __name__ == "__main__":
main()
+374
View File
@@ -0,0 +1,374 @@
#!/usr/bin/env python3
"""
Comprehensive Forms Example
This example demonstrates:
- All FormFieldType variations (TEXT, PASSWORD, EMAIL, etc.)
- Form layout with multiple fields
- Field labels and validation
- Form submission callbacks
- Organizing forms on pages
This shows how to create interactive forms with all available field types.
"""
import sys
from pathlib import Path
# Add pyWebLayout to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from pyWebLayout.concrete.page import Page
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.style.fonts import Font
from pyWebLayout.abstract.functional import Form, FormField, FormFieldType
from pyWebLayout.layout.document_layouter import DocumentLayouter
from PIL import Image, ImageDraw
# Track form submissions
form_submissions = []
def form_submit_callback(form_id: str):
"""Callback for form submissions"""
def callback(data):
form_submissions.append((form_id, data))
print(f" Form submitted: {form_id} with data: {data}")
return callback
def create_example_1_text_fields():
"""Example 1: Text input fields"""
print("\n Creating Example 1: Text input fields...")
page_style = PageStyle(
border_width=2,
border_color=(150, 150, 200),
padding=(20, 30, 20, 30),
background_color=(255, 255, 255)
)
page = Page(size=(500, 600), style=page_style)
layouter = DocumentLayouter(page)
# Create form with text fields
form = Form(form_id="text_form", html_id="text_form", callback=form_submit_callback("text_form"))
# Add various text-based fields
form.add_field(FormField(
name="username",
label="Username",
field_type=FormFieldType.TEXT,
required=True
))
form.add_field(FormField(
name="email",
label="Email Address",
field_type=FormFieldType.EMAIL,
required=True
))
form.add_field(FormField(
name="password",
label="Password",
field_type=FormFieldType.PASSWORD,
required=True
))
form.add_field(FormField(
name="website",
label="Website URL",
field_type=FormFieldType.URL,
required=False
))
form.add_field(FormField(
name="bio",
label="Biography",
field_type=FormFieldType.TEXTAREA,
required=False
))
# Layout the form
font = Font(font_size=12, colour=(50, 50, 50))
success, field_ids = layouter.layout_form(form, font=font)
print(f" Laid out {len(field_ids)} text fields")
return page
def create_example_2_number_fields():
"""Example 2: Number and date/time fields"""
print(" Creating Example 2: Number and date/time fields...")
page_style = PageStyle(
border_width=2,
border_color=(150, 200, 150),
padding=(20, 30, 20, 30),
background_color=(255, 255, 255)
)
page = Page(size=(500, 600), style=page_style)
layouter = DocumentLayouter(page)
# Create form with number/date fields
form = Form(form_id="number_form", html_id="number_form", callback=form_submit_callback("number_form"))
form.add_field(FormField(
name="age",
label="Age",
field_type=FormFieldType.NUMBER,
required=True
))
form.add_field(FormField(
name="birth_date",
label="Birth Date",
field_type=FormFieldType.DATE,
required=True
))
form.add_field(FormField(
name="appointment",
label="Appointment Time",
field_type=FormFieldType.TIME,
required=False
))
form.add_field(FormField(
name="rating",
label="Rating (1-10)",
field_type=FormFieldType.RANGE,
required=False
))
form.add_field(FormField(
name="color",
label="Favorite Color",
field_type=FormFieldType.COLOR,
required=False
))
# Layout the form
font = Font(font_size=12, colour=(50, 50, 50))
success, field_ids = layouter.layout_form(form, font=font)
print(f" Laid out {len(field_ids)} number/date fields")
return page
def create_example_3_selection_fields():
"""Example 3: Checkbox, radio, and select fields"""
print(" Creating Example 3: Selection fields...")
page_style = PageStyle(
border_width=2,
border_color=(200, 150, 150),
padding=(20, 30, 20, 30),
background_color=(255, 255, 255)
)
page = Page(size=(500, 600), style=page_style)
layouter = DocumentLayouter(page)
# Create form with selection fields
form = Form(form_id="selection_form", html_id="selection_form", callback=form_submit_callback("selection_form"))
form.add_field(FormField(
name="newsletter",
label="Subscribe to Newsletter",
field_type=FormFieldType.CHECKBOX,
required=False
))
form.add_field(FormField(
name="terms",
label="Accept Terms and Conditions",
field_type=FormFieldType.CHECKBOX,
required=True
))
form.add_field(FormField(
name="gender",
label="Gender",
field_type=FormFieldType.RADIO,
required=False
))
form.add_field(FormField(
name="country",
label="Country",
field_type=FormFieldType.SELECT,
required=True
))
form.add_field(FormField(
name="hidden_token",
label="", # Hidden fields don't display labels
field_type=FormFieldType.HIDDEN,
required=False
))
# Layout the form
font = Font(font_size=12, colour=(50, 50, 50))
success, field_ids = layouter.layout_form(form, font=font)
print(f" Laid out {len(field_ids)} selection fields")
return page
def create_example_4_complete_form():
"""Example 4: Complete registration form with mixed field types"""
print(" Creating Example 4: Complete registration form...")
page_style = PageStyle(
border_width=2,
border_color=(150, 200, 200),
padding=(20, 30, 20, 30),
background_color=(255, 255, 255)
)
page = Page(size=(500, 700), style=page_style)
layouter = DocumentLayouter(page)
# Create comprehensive registration form
form = Form(form_id="registration_form", html_id="registration_form", callback=form_submit_callback("registration"))
# Personal information
form.add_field(FormField(
name="full_name",
label="Full Name",
field_type=FormFieldType.TEXT,
required=True
))
form.add_field(FormField(
name="email",
label="Email",
field_type=FormFieldType.EMAIL,
required=True
))
form.add_field(FormField(
name="password",
label="Password",
field_type=FormFieldType.PASSWORD,
required=True
))
form.add_field(FormField(
name="age",
label="Age",
field_type=FormFieldType.NUMBER,
required=True
))
# Preferences
form.add_field(FormField(
name="notifications",
label="Enable Notifications",
field_type=FormFieldType.CHECKBOX,
required=False
))
# Layout the form
font = Font(font_size=12, colour=(50, 50, 50))
success, field_ids = layouter.layout_form(form, font=font, field_spacing=15)
print(f" Laid out complete form with {len(field_ids)} fields")
return page
def combine_pages_into_grid(pages, title):
"""Combine multiple pages into a 2x2 grid."""
print("\n Combining pages into grid...")
# Render all pages
images = [page.render() for page in pages]
# Grid layout
padding = 20
title_height = 40
cols = 2
rows = 2
# Calculate dimensions
img_width = images[0].size[0]
img_height = images[0].size[1]
total_width = cols * img_width + (cols + 1) * padding
total_height = rows * img_height + (rows + 1) * padding + title_height
# Create combined image
combined = Image.new('RGB', (total_width, total_height), (240, 240, 240))
draw = ImageDraw.Draw(combined)
# Draw title
from PIL import ImageFont
try:
title_font = ImageFont.truetype(
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 18
)
except:
title_font = ImageFont.load_default()
bbox = draw.textbbox((0, 0), title, font=title_font)
text_width = bbox[2] - bbox[0]
title_x = (total_width - text_width) // 2
draw.text((title_x, 10), title, fill=(50, 50, 50), font=title_font)
# Place pages in grid
y_offset = title_height + padding
for row in range(rows):
x_offset = padding
for col in range(cols):
idx = row * cols + col
if idx < len(images):
combined.paste(images[idx], (x_offset, y_offset))
x_offset += img_width + padding
y_offset += img_height + padding
return combined
def main():
"""Demonstrate comprehensive form field types."""
global form_submissions
form_submissions = []
print("Comprehensive Forms Example")
print("=" * 50)
# Create examples for different form types
pages = [
create_example_1_text_fields(),
create_example_2_number_fields(),
create_example_3_selection_fields(),
create_example_4_complete_form()
]
# Combine into demonstration image
combined_image = combine_pages_into_grid(
pages,
"Form Field Types: Text | Numbers | Selection | Complete"
)
# Save output
output_dir = Path("docs/images")
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / "example_10_forms.png"
combined_image.save(output_path)
print("\n✓ Example completed!")
print(f" Output saved to: {output_path}")
print(f" Image size: {combined_image.size[0]}x{combined_image.size[1]} pixels")
print(f" Created {len(pages)} form examples")
print(f" Total form callbacks registered: {len(form_submissions)}")
return combined_image, form_submissions
if __name__ == "__main__":
main()
+240
View File
@@ -0,0 +1,240 @@
"""
Demonstration of dynamic font family switching in the ereader.
This example shows how to:
1. Initialize an ereader with content
2. Dynamically switch between different font families (Sans, Serif, Monospace)
3. Maintain reading position across font changes
4. Use the font family API
The ereader manager provides a high-level API for changing fonts on-the-fly
without losing your place in the document.
"""
from pyWebLayout.abstract import Paragraph, Heading, Word
from pyWebLayout.abstract.block import HeadingLevel
from pyWebLayout.style import Font
from pyWebLayout.style.fonts import BundledFont, FontWeight
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.layout.ereader_manager import create_ereader_manager
from PIL import Image
def create_sample_content():
"""Create sample document content with various text styles"""
blocks = []
# Create a default font for the content
default_font = Font.from_family(BundledFont.SANS, font_size=16)
heading_font = Font.from_family(BundledFont.SANS, font_size=24, weight=FontWeight.BOLD)
# Title
title = Heading(level=HeadingLevel.H1, style=heading_font)
for word in "Font Family Switching Demo".split():
title.add_word(Word(word, heading_font))
blocks.append(title)
# Introduction paragraph
intro_font = Font.from_family(BundledFont.SANS, font_size=16)
intro = Paragraph(intro_font)
intro_text = (
"This demonstration shows how the ereader can dynamically switch between "
"different font families while maintaining your reading position. "
"The three bundled font families (Sans, Serif, and Monospace) can be "
"changed on-the-fly without recreating the document."
)
for word in intro_text.split():
intro.add_word(Word(word, intro_font))
blocks.append(intro)
# Section 1
section1_heading = Heading(level=HeadingLevel.H2, style=heading_font)
for word in "Sans-Serif Font".split():
section1_heading.add_word(Word(word, heading_font))
blocks.append(section1_heading)
para1 = Paragraph(default_font)
text1 = (
"Sans-serif fonts like DejaVu Sans are clean and modern, making them "
"ideal for screen reading. They lack the decorative strokes (serifs) "
"found in traditional typefaces, which can improve legibility on digital displays. "
"Many ereader applications default to sans-serif fonts for this reason."
)
for word in text1.split():
para1.add_word(Word(word, default_font))
blocks.append(para1)
# Section 2
section2_heading = Heading(level=HeadingLevel.H2, style=heading_font)
for word in "Serif Font".split():
section2_heading.add_word(Word(word, heading_font))
blocks.append(section2_heading)
para2 = Paragraph(default_font)
text2 = (
"Serif fonts like DejaVu Serif have small decorative strokes at the ends "
"of letter strokes. These fonts are traditionally used in print media and "
"can give a more formal, classic appearance. Many readers prefer serif fonts "
"for long-form reading as they find them easier on the eyes."
)
for word in text2.split():
para2.add_word(Word(word, default_font))
blocks.append(para2)
# Section 3
section3_heading = Heading(level=HeadingLevel.H2, style=heading_font)
for word in "Monospace Font".split():
section3_heading.add_word(Word(word, heading_font))
blocks.append(section3_heading)
para3 = Paragraph(default_font)
text3 = (
"Monospace fonts like DejaVu Sans Mono have equal spacing between all characters. "
"They are commonly used for displaying code, technical documentation, and typewriter-style "
"text. While less common for general reading, some users prefer the uniform character "
"spacing for certain types of content."
)
for word in text3.split():
para3.add_word(Word(word, default_font))
blocks.append(para3)
# Final paragraph
conclusion = Paragraph(default_font)
conclusion_text = (
"The ability to switch fonts dynamically is a key feature of modern ereaders. "
"It allows readers to customize their reading experience based on personal preference, "
"lighting conditions, and content type. Try switching between the three font families "
"to see which one you prefer for different types of reading."
)
for word in conclusion_text.split():
conclusion.add_word(Word(word, default_font))
blocks.append(conclusion)
return blocks
def render_pages_with_different_fonts(manager, output_prefix="demo_11"):
"""Render the same page with different font families"""
print("\nRendering pages with different font families...")
print("=" * 70)
font_families = [
(None, "Original (Sans)"),
(BundledFont.SERIF, "Serif"),
(BundledFont.MONOSPACE, "Monospace"),
(BundledFont.SANS, "Sans (explicit)")
]
images = []
for font_family, name in font_families:
print(f"\nRendering with {name} font...")
# Switch font family
manager.set_font_family(font_family)
# Get current page
page = manager.get_current_page()
# Render to image
image = page.render()
filename = f"{output_prefix}_{name.lower().replace(' ', '_').replace('(', '').replace(')', '')}.png"
image.save(filename)
print(f" Saved: {filename}")
images.append((name, image))
return images
def demonstrate_font_switching():
"""Main demonstration function"""
print("\n")
print("=" * 70)
print("Font Family Switching Demonstration")
print("=" * 70)
print()
# Create sample content
print("Creating sample document...")
blocks = create_sample_content()
print(f" Created {len(blocks)} blocks")
# Initialize ereader manager
print("\nInitializing ereader manager...")
page_size = (600, 800)
manager = create_ereader_manager(
blocks,
page_size,
document_id="font_switching_demo"
)
print(f" Page size: {page_size[0]}x{page_size[1]}")
print(f" Initial font family: {manager.get_font_family()}")
# Render pages with different fonts
images = render_pages_with_different_fonts(manager)
# Show position info
print("\nPosition information after font switches:")
print(" " + "-" * 66)
pos_info = manager.get_position_info()
print(f" Current position: Block {pos_info['position']['block_index']}, "
f"Word {pos_info['position']['word_index']}")
print(f" Font family: {pos_info['font_family'] or 'Original'}")
print(f" Font scale: {pos_info['font_scale']}")
print(f" Reading progress: {pos_info['progress']:.1%}")
# Test navigation with font switching
print("\nTesting navigation with font switching...")
print(" " + "-" * 66)
# Reset to beginning
manager.jump_to_position(manager.current_position.__class__())
# Advance a few pages with serif font
manager.set_font_family(BundledFont.SERIF)
print(f" Switched to SERIF font")
for i in range(3):
next_page = manager.next_page()
if next_page:
print(f" Page {i+2}: Advanced successfully")
# Switch to monospace
manager.set_font_family(BundledFont.MONOSPACE)
print(f" Switched to MONOSPACE font")
current_page = manager.get_current_page()
print(f" Re-rendered current page with new font")
# Go back a page
prev_page = manager.previous_page()
if prev_page:
print(f" Navigated back successfully")
# Cache statistics
print("\nCache statistics:")
print(" " + "-" * 66)
stats = manager.get_cache_stats()
for key, value in stats.items():
print(f" {key}: {value}")
print()
print("=" * 70)
print("Demo complete!")
print()
print("Key features demonstrated:")
print(" ✓ Dynamic font family switching (Sans, Serif, Monospace)")
print(" ✓ Position preservation across font changes")
print(" ✓ Automatic cache invalidation on font change")
print(" ✓ Navigation with different fonts")
print(" ✓ Font family info in position tracking")
print()
print("The rendered pages show the same content in different font families.")
print("Notice how the layout adapts while maintaining readability.")
print("=" * 70)
print()
if __name__ == "__main__":
demonstrate_font_switching()
+312
View File
@@ -0,0 +1,312 @@
#!/usr/bin/env python3
"""
Table Text Wrapping Example
This example demonstrates the line wrapping functionality in table cells:
- Tables with long text that wraps across multiple lines
- Automatic word wrapping within cell boundaries
- Hyphenation support for long words
- Multiple paragraphs per cell
- Comparison of narrow vs. wide columns
Shows how the Line-based text layout system handles text overflow in tables.
"""
from pyWebLayout.io.readers.html_extraction import parse_html_string
from pyWebLayout.layout.document_layouter import DocumentLayouter
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.concrete.table import TableStyle
from pyWebLayout.concrete.page import Page
import sys
from pathlib import Path
from PIL import Image
# Add pyWebLayout to path
sys.path.insert(0, str(Path(__file__).parent.parent))
def create_narrow_columns_example():
"""Create a table with narrow columns to show aggressive wrapping."""
print(" - Narrow columns with text wrapping")
html = """
<table>
<thead>
<tr>
<th>Feature</th>
<th>Description</th>
<th>Benefits</th>
</tr>
</thead>
<tbody>
<tr>
<td>Automatic Line Wrapping</td>
<td>Text automatically wraps to fit within the available cell width, creating multiple lines as needed.</td>
<td>Improves readability and prevents horizontal overflow in tables.</td>
</tr>
<tr>
<td>Hyphenation Support</td>
<td>Long words are intelligently hyphenated using pyphen library or brute-force splitting when necessary.</td>
<td>Handles extraordinarily long words that wouldn't fit on a single line.</td>
</tr>
<tr>
<td>Multi-paragraph Cells</td>
<td>Each cell can contain multiple paragraphs or headings, all properly wrapped.</td>
<td>Allows rich content within table cells.</td>
</tr>
</tbody>
</table>
"""
return html, "Text Wrapping in Narrow Columns"
def create_mixed_content_example():
"""Create a table with both short and long content."""
print(" - Mixed content lengths")
html = """
<table>
<caption>Product Comparison</caption>
<thead>
<tr>
<th>Product</th>
<th>Short Description</th>
<th>Detailed Features</th>
</tr>
</thead>
<tbody>
<tr>
<td>Widget Pro</td>
<td>Premium</td>
<td>Advanced functionality with enterprise-grade reliability, comprehensive warranty coverage, and dedicated customer support available around the clock.</td>
</tr>
<tr>
<td>Widget Lite</td>
<td>Basic</td>
<td>Essential features for everyday use with straightforward operation and minimal learning curve.</td>
</tr>
<tr>
<td>Widget Max</td>
<td>Ultimate</td>
<td>Everything from Widget Pro plus additional customization options, API integration capabilities, and advanced analytics dashboard.</td>
</tr>
</tbody>
</table>
"""
return html, "Mixed Short and Long Content"
def create_technical_documentation_example():
"""Create a table like technical documentation."""
print(" - Technical documentation style")
html = """
<table>
<thead>
<tr>
<th>API Method</th>
<th>Parameters</th>
<th>Description</th>
<th>Return Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>render_table()</td>
<td>table, origin, width, draw, style</td>
<td>Renders a table with automatic text wrapping in cells. Uses the Line class for intelligent word placement and hyphenation.</td>
<td>Rendered table with calculated height and width properties.</td>
</tr>
<tr>
<td>add_word()</td>
<td>word, pretext</td>
<td>Attempts to add a word to the current line. If it doesn't fit, tries hyphenation strategies including pyphen and brute-force splitting.</td>
<td>Tuple of (success, overflow_text) indicating whether word was added and any remaining text.</td>
</tr>
<tr>
<td>calculate_spacing()</td>
<td>text_objects, width, min_spacing, max_spacing</td>
<td>Determines optimal spacing between words to achieve proper justification within the specified constraints.</td>
<td>Calculated spacing value and position offset for alignment.</td>
</tr>
</tbody>
</table>
"""
return html, "Technical Documentation Table"
def create_news_article_example():
"""Create a table with article-style content."""
print(" - News article layout")
html = """
<table>
<thead>
<tr>
<th>Date</th>
<th>Headline</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
<tr>
<td>2024-01-15</td>
<td>New Text Wrapping Feature</td>
<td>PyWebLayout now supports automatic line wrapping in table cells, bringing sophisticated text layout capabilities to table rendering. The implementation leverages the existing Line class infrastructure.</td>
</tr>
<tr>
<td>2024-01-10</td>
<td>Hyphenation Improvements</td>
<td>Enhanced hyphenation algorithms now include both dictionary-based pyphen hyphenation and intelligent brute-force splitting for edge cases.</td>
</tr>
<tr>
<td>2024-01-05</td>
<td>Performance Optimization</td>
<td>Table rendering performance improved through better caching and reduced font object creation overhead.</td>
</tr>
</tbody>
</table>
"""
return html, "News Article Layout"
def render_table_example(html, title, style_variant=0):
"""Render a single table example."""
from pyWebLayout.style import Font
from pyWebLayout.abstract.block import Table
# Parse HTML
base_font = Font(font_size=12)
blocks = parse_html_string(html, base_font=base_font)
# Find the table block
table = None
for block in blocks:
if isinstance(block, Table):
table = block
break
if not table:
print(f" Warning: No table found in {title}")
return None
# Create page style
page_style = PageStyle(
padding=(20, 20, 20, 20),
background_color=(255, 255, 255)
)
# Create page
page_size = (900, 600)
page = Page(size=page_size, style=page_style)
# Create table style variants
table_styles = [
# Default style
TableStyle(
border_width=1,
border_color=(0, 0, 0),
cell_padding=(8, 8, 8, 8),
header_bg_color=(240, 240, 240),
cell_bg_color=(255, 255, 255)
),
# Blue header style
TableStyle(
border_width=2,
border_color=(70, 130, 180),
cell_padding=(10, 10, 10, 10),
header_bg_color=(176, 196, 222),
cell_bg_color=(245, 250, 255)
),
# Minimal style
TableStyle(
border_width=1,
border_color=(200, 200, 200),
cell_padding=(6, 6, 6, 6),
header_bg_color=(250, 250, 250),
cell_bg_color=(255, 255, 255)
),
]
table_style = table_styles[style_variant % len(table_styles)]
# Create layouter and render table
layouter = DocumentLayouter(page)
layouter.layout_table(table, style=table_style)
# Get the rendered canvas
_ = page.draw # Ensure canvas exists
img = page._canvas
return img
def combine_examples(examples):
"""Combine multiple example images into one."""
images = []
titles = []
for html, title in examples:
img = render_table_example(html, title)
if img:
images.append(img)
titles.append(title)
if not images:
return None
# Calculate combined image size
max_width = max(img.width for img in images)
total_height = sum(img.height for img in images) + 40 * len(images) # Extra space between images
# Create combined image
combined = Image.new('RGB', (max_width, total_height), color=(255, 255, 255))
# Paste images
y_offset = 20
for img in images:
combined.paste(img, (0, y_offset))
y_offset += img.height + 40
return combined
def main():
"""Run the table text wrapping example."""
print("\nTable Text Wrapping Example")
print("=" * 50)
# Create examples
print("\n Creating table examples...")
examples = [
create_narrow_columns_example(),
create_mixed_content_example(),
create_technical_documentation_example(),
create_news_article_example(),
]
print("\n Rendering table examples...")
combined_image = combine_examples(examples)
if combined_image:
# Save the output
output_dir = Path(__file__).parent.parent / 'docs' / 'images'
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / 'example_11_table_text_wrapping.png'
combined_image.save(str(output_path))
print("\n✓ Example completed!")
print(f" Output saved to: {output_path}")
print(f" Image size: {combined_image.width}x{combined_image.height} pixels")
print(f" Created {len(examples)} table examples with text wrapping")
else:
print("\n✗ Failed to generate examples")
if __name__ == '__main__':
main()
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""
Simple Table Text Wrapping Example
A minimal example showing text wrapping in table cells.
Perfect for quick testing and demonstration.
"""
import sys
from pathlib import Path
# 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.layout.document_layouter import DocumentLayouter
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.style import Font
from pyWebLayout.concrete.table import TableStyle
from pyWebLayout.concrete.page import Page
from pyWebLayout.abstract.block import Table
def main():
"""Create a simple table with text wrapping."""
print("\nSimple Table Text Wrapping Example")
print("=" * 50)
# HTML with a table containing long text
html = """
<table>
<caption>Text Wrapping Demonstration</caption>
<thead>
<tr>
<th>Column 1</th>
<th>Column 2</th>
<th>Column 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>This is a cell with quite a lot of text that will need to wrap across multiple lines.</td>
<td>Short text</td>
<td>Another cell with enough content to demonstrate the automatic line wrapping functionality.</td>
</tr>
<tr>
<td>Cell A</td>
<td>This middle cell contains a paragraph with several words that should wrap nicely within the available space.</td>
<td>Cell C</td>
</tr>
<tr>
<td>Words like supercalifragilisticexpialidocious might need hyphenation.</td>
<td>Normal text</td>
<td>The wrapping algorithm handles both regular word wrapping and hyphenation seamlessly.</td>
</tr>
</tbody>
</table>
"""
print("\n Parsing HTML and creating table...")
# Parse HTML
base_font = Font(font_size=12)
blocks = parse_html_string(html, base_font=base_font)
# Find table
table = None
for block in blocks:
if isinstance(block, Table):
table = block
break
if not table:
print(" ✗ No table found!")
return
print(" ✓ Table parsed successfully")
# Create page
page_style = PageStyle(
padding=(30, 30, 30, 30),
background_color=(255, 255, 255)
)
page = Page(size=(800, 600), style=page_style)
# Create table style
table_style = TableStyle(
border_width=2,
border_color=(70, 130, 180),
cell_padding=(10, 10, 10, 10),
header_bg_color=(176, 196, 222),
cell_bg_color=(245, 250, 255)
)
print(" Rendering table with text wrapping...")
# Layout and render
layouter = DocumentLayouter(page)
layouter.layout_table(table, style=table_style)
# Get rendered image
_ = page.draw
img = page._canvas
# Save output
output_path = Path(__file__).parent.parent / 'docs' / 'images' / 'example_11b_simple_wrapping.png'
output_path.parent.mkdir(parents=True, exist_ok=True)
img.save(str(output_path))
print(f"\n✓ Example completed!")
print(f" Output saved to: {output_path}")
print(f" Image size: {img.width}x{img.height} pixels")
print(f"\n The table demonstrates:")
print(f" • Automatic line wrapping in cells")
print(f" • Proper word spacing and alignment")
print(f" • Hyphenation for very long words")
print(f" • Multi-line text within cell boundaries")
if __name__ == '__main__':
main()
+254
View File
@@ -0,0 +1,254 @@
#!/usr/bin/env python3
"""
Demo: Optimized Table Column Width Layout
This example demonstrates the intelligent table column width optimization:
- Automatic width distribution based on content
- HTML width overrides (fixed column widths)
- Sampling for performance (large tables)
- Comparison: before (equal distribution) vs after (optimized)
The optimizer:
1. Samples first ~5 rows from each section
2. Measures minimum and preferred widths for each column
3. Distributes available space proportionally
4. Respects HTML width attributes
"""
from pyWebLayout.concrete.page import Page
from pyWebLayout.concrete.table import TableRenderer, TableStyle
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.style import Font
from pyWebLayout.abstract.block import Table, TableRow, TableCell, Paragraph
from pyWebLayout.abstract.inline import Word
from PIL import ImageDraw
def create_demo_table_1():
"""Create a table with varying content lengths (shows optimization)."""
table = Table()
table.caption = "Example 1: Optimized Width Distribution"
font = Font(font_size=11)
# Header
header_row = TableRow()
for text in ["ID", "Name", "Description"]:
cell = TableCell(is_header=True)
para = Paragraph(font)
para.add_word(Word(text, font))
cell.add_block(para)
header_row.add_cell(cell)
table.add_row(header_row, section="header")
# Body rows with varying content lengths
data = [
("1", "Alice", "Short description"),
("2", "Bob", "This is a much longer description that demonstrates how the optimizer allocates more space to columns with longer content"),
("3", "Charlie", "Medium length description here"),
("4", "Diana", "Another longer description that shows the column width optimization working effectively for content-heavy cells"),
]
for row_data in data:
row = TableRow()
for text in row_data:
cell = TableCell()
para = Paragraph(font)
for word in text.split():
para.add_word(Word(word, font))
cell.add_block(para)
row.add_cell(cell)
table.add_row(row, section="body")
return table
def create_demo_table_2():
"""Create a table with HTML width overrides."""
table = Table()
table.caption = "Example 2: Fixed Column Widths (HTML override)"
font = Font(font_size=11)
# Header with width attributes
header_row = TableRow()
# Fixed width column
cell1 = TableCell(is_header=True)
cell1.width = "80px" # HTML width override!
para1 = Paragraph(font)
para1.add_word(Word("ID", font))
para1.add_word(Word("(Fixed", font))
para1.add_word(Word("80px)", font))
cell1.add_block(para1)
header_row.add_cell(cell1)
# Auto-width columns
for text in ["Name (Auto)", "Description (Auto)"]:
cell = TableCell(is_header=True)
para = Paragraph(font)
for word in text.split():
para.add_word(Word(word, font))
cell.add_block(para)
header_row.add_cell(cell)
table.add_row(header_row, section="header")
# Body rows
data = [
("1", "Alice", "The first two columns adapt to remaining space"),
("2", "Bob", "ID column stays fixed at 80px width"),
("3", "Charlie", "Name and Description share the remaining width proportionally"),
]
for row_data in data:
row = TableRow()
# First cell also has fixed width
cell = TableCell()
cell.width = "80px"
para = Paragraph(font)
para.add_word(Word(row_data[0], font))
cell.add_block(para)
row.add_cell(cell)
# Other cells auto-width
for text in row_data[1:]:
cell = TableCell()
para = Paragraph(font)
for word in text.split():
para.add_word(Word(word, font))
cell.add_block(para)
row.add_cell(cell)
table.add_row(row, section="body")
return table
def create_demo_table_3():
"""Create a large table (demonstrates sampling)."""
table = Table()
table.caption = "Example 3: Large Table (uses sampling for performance)"
font = Font(font_size=10)
# Header
header_row = TableRow()
for text in ["Index", "Data A", "Data B", "Data C"]:
cell = TableCell(is_header=True)
para = Paragraph(font)
para.add_word(Word(text, font))
cell.add_block(para)
header_row.add_cell(cell)
table.add_row(header_row, section="header")
# Many body rows (only first ~5 will be sampled for measurement)
for i in range(50):
row = TableRow()
# Index
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(str(i + 1), font))
cell.add_block(para)
row.add_cell(cell)
# Data columns with varying content
if i % 3 == 0:
data = ["Short", "Medium length", "Longer content here"]
elif i % 3 == 1:
data = ["Medium", "Short", "Also longer content"]
else:
data = ["Longer text", "Short", "Medium"]
for text in data:
cell = TableCell()
para = Paragraph(font)
for word in text.split():
para.add_word(Word(word, font))
cell.add_block(para)
row.add_cell(cell)
table.add_row(row, section="body")
return table
def main():
# Create page
page_style = PageStyle(
border_width=1,
padding=(20, 20, 20, 20),
background_color=(255, 255, 255)
)
page = Page(size=(800, 2200), style=page_style)
# Get canvas and draw
canvas = page._create_canvas()
page._canvas = canvas
page._draw = ImageDraw.Draw(canvas)
current_y = 30
# Table style
table_style = TableStyle(
border_width=1,
border_color=(100, 100, 100),
cell_padding=(8, 8, 8, 8),
header_bg_color=(220, 230, 240),
cell_bg_color=(255, 255, 255),
alternate_row_color=(248, 248, 248)
)
# Render Example 1: Optimized distribution
table1 = create_demo_table_1()
renderer1 = TableRenderer(
table1,
origin=(20, current_y),
available_width=760,
draw=page._draw,
style=table_style,
canvas=canvas
)
renderer1.render()
current_y += renderer1.height + 40
# Render Example 2: Fixed widths
table2 = create_demo_table_2()
renderer2 = TableRenderer(
table2,
origin=(20, current_y),
available_width=760,
draw=page._draw,
style=table_style,
canvas=canvas
)
renderer2.render()
current_y += renderer2.height + 40
# Render Example 3: Large table with sampling
table3 = create_demo_table_3()
renderer3 = TableRenderer(
table3,
origin=(20, current_y),
available_width=760,
draw=page._draw,
style=table_style,
canvas=canvas
)
renderer3.render()
# Save
output_path = "docs/images/example_12_optimized_table_layout.png"
canvas.save(output_path)
print(f"✓ Optimized table layout demo created!")
print(f" Output: {output_path}")
print(f" Image size: {canvas.size}")
print(f"\nExamples demonstrated:")
print(f" 1. Content-aware width distribution")
print(f" 2. HTML width overrides (80px fixed column)")
print(f" 3. Large table with sampling (50 rows, only ~5 measured)")
if __name__ == "__main__":
main()
+291
View File
@@ -0,0 +1,291 @@
#!/usr/bin/env python3
"""
Demo: Table Pagination
This example demonstrates table pagination when content exceeds page height:
- Large table that spans multiple pages
- Automatic row-level pagination (entire rows move to next page)
- Continuation markers ("continued on next page", "continued from previous page")
- Headers repeated on each page
The pagination system:
1. Renders rows sequentially until page height limit reached
2. Moves entire row to next page if it doesn't fit
3. Repeats header row on continuation pages
4. Adds visual markers to show table continues
"""
from pyWebLayout.concrete.page import Page
from pyWebLayout.concrete.table import TableRenderer, TableStyle
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.style import Font
from pyWebLayout.abstract.block import Table, TableRow, TableCell, Paragraph
from pyWebLayout.abstract.inline import Word
from PIL import Image, ImageDraw
def create_large_table():
"""Create a table with many rows that will require pagination."""
table = Table()
table.caption = "Employee Directory (Paginated)"
font = Font(font_size=11)
# Header
header_row = TableRow()
for text in ["ID", "Name", "Department", "Email", "Phone"]:
cell = TableCell(is_header=True)
para = Paragraph(font)
para.add_word(Word(text, font))
cell.add_block(para)
header_row.add_cell(cell)
table.add_row(header_row, section="header")
# Many body rows (will span multiple pages)
departments = ["Engineering", "Sales", "Marketing", "HR", "Finance", "Operations", "Support"]
for i in range(60):
row = TableRow()
# ID
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(f"EMP{i+1001}", font))
cell.add_block(para)
row.add_cell(cell)
# Name
cell = TableCell()
para = Paragraph(font)
names = ["Alice Johnson", "Bob Smith", "Charlie Brown", "Diana Lee",
"Eve Wilson", "Frank Miller", "Grace Davis", "Henry Taylor"]
para.add_word(Word(names[i % len(names)], font))
cell.add_block(para)
row.add_cell(cell)
# Department
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(departments[i % len(departments)], font))
cell.add_block(para)
row.add_cell(cell)
# Email
cell = TableCell()
para = Paragraph(font)
email = f"{names[i % len(names)].lower().replace(' ', '.')}@company.com"
para.add_word(Word(email, font))
cell.add_block(para)
row.add_cell(cell)
# Phone
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(f"+1-555-{(i*17)%1000:04d}", font))
cell.add_block(para)
row.add_cell(cell)
table.add_row(row, section="body")
return table
def render_table_with_pagination(table, page_size, max_pages=3):
"""
Render a table across multiple pages.
Args:
table: The table to render
page_size: Tuple of (width, height) for each page
max_pages: Maximum number of pages to render
Returns:
List of PIL Images (one per page)
"""
pages = []
page_style = PageStyle(
border_width=1,
padding=(20, 20, 20, 20),
background_color=(255, 255, 255)
)
table_style = TableStyle(
border_width=1,
border_color=(100, 100, 100),
cell_padding=(6, 8, 6, 8),
header_bg_color=(220, 230, 240),
cell_bg_color=(255, 255, 255),
alternate_row_color=(248, 248, 248)
)
# Get all rows
all_rows = list(table.all_rows())
header_rows = [row for section, row in all_rows if section == "header"]
body_rows = [row for section, row in all_rows if section == "body"]
# Calculate header height once
temp_page = Page(size=page_size, style=page_style)
temp_canvas = temp_page._create_canvas()
temp_draw = ImageDraw.Draw(temp_canvas)
# Create temporary table with just header to measure
header_table = Table()
header_table.caption = table.caption
for header_row in header_rows:
header_table.add_row(header_row, section="header")
header_renderer = TableRenderer(
header_table,
origin=(20, 20),
available_width=page_size[0] - 40,
draw=temp_draw,
style=table_style,
canvas=temp_canvas
)
header_height = header_renderer.height
# Available height for body rows
available_body_height = page_size[1] - 60 - header_height # margins + header
# Paginate body rows
current_page_rows = []
current_height = 0
page_num = 0
for i, body_row in enumerate(body_rows):
if page_num >= max_pages:
break
# Estimate row height (simplified - actual would measure each row)
# For this demo, assume ~30px per row
row_height = 35
if current_height + row_height > available_body_height and current_page_rows:
# Render current page
page_canvas = render_page(
table,
header_rows,
current_page_rows,
page_size,
page_style,
table_style,
page_num,
is_last=False
)
pages.append(page_canvas)
# Start new page
page_num += 1
current_page_rows = []
current_height = 0
current_page_rows.append(body_row)
current_height += row_height
# Render final page
if current_page_rows and page_num < max_pages:
page_canvas = render_page(
table,
header_rows,
current_page_rows,
page_size,
page_style,
table_style,
page_num,
is_last=(i == len(body_rows) - 1)
)
pages.append(page_canvas)
return pages
def render_page(table, header_rows, body_rows, page_size, page_style, table_style, page_num, is_last):
"""Render a single page with header and body rows."""
page = Page(size=page_size, style=page_style)
canvas = page._create_canvas()
page._canvas = canvas
page._draw = ImageDraw.Draw(canvas)
# Create table for this page
page_table = Table()
if page_num == 0:
page_table.caption = table.caption
else:
page_table.caption = f"{table.caption} (continued)"
# Add header rows
for header_row in header_rows:
page_table.add_row(header_row, section="header")
# Add body rows for this page
for body_row in body_rows:
page_table.add_row(body_row, section="body")
# Render table
renderer = TableRenderer(
page_table,
origin=(20, 20),
available_width=page_size[0] - 40,
draw=page._draw,
style=table_style,
canvas=canvas
)
renderer.render()
# Add continuation marker at bottom
if not is_last:
font = Font(font_size=10)
y_pos = page_size[1] - 30
page._draw.text(
(page_size[0] // 2 - 100, y_pos),
"(continued on next page)",
fill=(100, 100, 100),
font=font.font
)
# Add page number
page._draw.text(
(page_size[0] // 2 - 20, page_size[1] - 15),
f"Page {page_num + 1}",
fill=(150, 150, 150),
font=Font(font_size=9).font
)
return canvas
def main():
# Create large table
table = create_large_table()
# Render with pagination (3 pages max for demo)
page_size = (900, 700)
pages = render_table_with_pagination(table, page_size, max_pages=3)
# Combine pages side-by-side for visualization
total_width = page_size[0] * len(pages) + (len(pages) - 1) * 20 # 20px spacing
combined = Image.new('RGB', (total_width, page_size[1]), (240, 240, 240))
x_offset = 0
for i, page_canvas in enumerate(pages):
combined.paste(page_canvas, (x_offset, 0))
x_offset += page_size[0] + 20
# Save
output_path = "docs/images/example_13_table_pagination.png"
combined.save(output_path)
print(f"✓ Table pagination demo created!")
print(f" Output: {output_path}")
print(f" Pages rendered: {len(pages)}")
print(f" Image size: {combined.size}")
print(f"\nDemonstrates:")
print(f" - Large table (60 rows) paginated across {len(pages)} pages")
print(f" - Header repeated on each page")
print(f" - Continuation markers")
print(f" - Page numbers")
if __name__ == "__main__":
main()
+312
View File
@@ -0,0 +1,312 @@
#!/usr/bin/env python3
"""
Demo: Working Interactive Table with Buttons
This example shows a fully working interactive table where buttons are
actually rendered inside table cells and can handle click events.
This uses a hybrid approach:
1. Tables are rendered normally for structure
2. Buttons are rendered on top at calculated positions
3. Click detection maps coordinates to button callbacks
"""
from pyWebLayout.concrete.page import Page
from pyWebLayout.concrete.table import TableRenderer, TableStyle
from pyWebLayout.concrete.functional import ButtonText
from pyWebLayout.abstract.functional import Button
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.style import Font
from pyWebLayout.abstract.block import Table, TableRow, TableCell, Paragraph
from pyWebLayout.abstract.inline import Word
from PIL import Image, ImageDraw
import numpy as np
def create_interactive_table():
"""Create a table structure (buttons will be overlaid)."""
table = Table()
table.caption = "User Management with Interactive Buttons"
font = Font(font_size=11)
# Header
header_row = TableRow()
for i, text in enumerate(["ID", "Name", "Email", "Actions"]):
cell = TableCell(is_header=True)
# Set width for Actions column
if text == "Actions":
cell.width = "220px" # Enough for 3 buttons
para = Paragraph(font)
para.add_word(Word(text, font))
cell.add_block(para)
header_row.add_cell(cell)
table.add_row(header_row, section="header")
# Body rows
users = [
("U001", "Alice Johnson", "alice@example.com"),
("U002", "Bob Smith", "bob@example.com"),
("U003", "Charlie Brown", "charlie@example.com"),
]
for user_id, name, email in users:
row = TableRow()
# ID
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(user_id, font))
cell.add_block(para)
row.add_cell(cell)
# Name
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(name, font))
cell.add_block(para)
row.add_cell(cell)
# Email
cell = TableCell()
para = Paragraph(font)
para.add_word(Word(email, font))
cell.add_block(para)
row.add_cell(cell)
# Actions - leave empty for buttons to be overlaid
# Set width hint to ensure space for 3 buttons
cell = TableCell()
cell.width = "220px" # Enough for 3 buttons (3 × 65px + padding)
para = Paragraph(font)
para.add_word(Word("", font)) # Empty placeholder
cell.add_block(para)
row.add_cell(cell)
table.add_row(row, section="body")
return table
def render_buttons_in_table(canvas, draw, table_origin, column_widths, row_heights, users):
"""
Render interactive buttons inside the table cells.
This calculates the exact position of each button based on the table
layout and renders ButtonText objects at those positions.
Args:
canvas: PIL Image canvas
draw: PIL ImageDraw object
table_origin: (x, y) position of table top-left
column_widths: List of column widths
row_heights: Dict with 'header', 'body', 'footer' keys
users: User data for button labels
Returns:
List of (button, bounds) for click detection
"""
button_font = Font(font_size=10)
buttons_with_bounds = []
# Calculate Actions column position (column 3, index 3)
actions_col_x = table_origin[0] + sum(column_widths[:3]) + 3 * 2 # +borders
actions_col_width = column_widths[3]
# Start after caption and header row
# Caption takes 20px + 10px spacing = 30px
caption_height = 30
header_height = row_heights.get("header", 30)
current_y = table_origin[1] + caption_height + header_height + 2 # +caption +header +border
for i, (user_id, name, email) in enumerate(users):
row_height = row_heights.get("body", 30) # All body rows have same height
# Position buttons horizontally in the Actions cell
button_x = actions_col_x + 10 # Padding from cell edge
button_y = current_y + (row_height - 30) // 2 # Center vertically
# Create buttons for this row
# Note: Button callbacks receive click point as first argument
buttons = [
("View", lambda point, uid=user_id: print(f"View {uid}")),
("Edit", lambda point, uid=user_id: print(f"Edit {uid}")),
("Delete", lambda point, uid=user_id: print(f"Delete {uid}"))
]
for label, callback in buttons:
# Create button
abstract_button = Button(label=label, callback=callback)
button_text = ButtonText(
button=abstract_button,
font=button_font,
draw=draw,
padding=(6, 12, 6, 12)
)
# Set position
button_text._origin = np.array([button_x, button_y])
# Render button
button_text.render()
# Store bounds for click detection
button_width = 60 # Approximate
button_height = 25
bounds = (button_x, button_y, button_x + button_width, button_y + button_height)
buttons_with_bounds.append((abstract_button, bounds))
# Move to next button position
button_x += 65
# Move to next row
current_y += row_height + 1 # +border
return buttons_with_bounds
def handle_click(click_pos, buttons_with_bounds):
"""
Handle a click event by checking if it's inside any button bounds.
Args:
click_pos: (x, y) tuple of click position
buttons_with_bounds: List of (button, bounds) tuples
Returns:
True if a button was clicked, False otherwise
"""
click_x, click_y = click_pos
for button, (x1, y1, x2, y2) in buttons_with_bounds:
if x1 <= click_x <= x2 and y1 <= click_y <= y2:
# Click is inside this button!
button.execute(click_pos)
return True
return False
def main():
# Create page
page_size = (900, 500) # Increased height to fit instructions
page_style = PageStyle(
border_width=1,
padding=(20, 20, 20, 20),
background_color=(255, 255, 255)
)
page = Page(size=page_size, style=page_style)
canvas = page._create_canvas()
page._canvas = canvas
page._draw = ImageDraw.Draw(canvas)
# Table style
table_style = TableStyle(
border_width=1,
border_color=(100, 100, 100),
cell_padding=(8, 10, 8, 10),
header_bg_color=(220, 230, 240),
cell_bg_color=(255, 255, 255),
alternate_row_color=(248, 248, 248)
)
# User data
users = [
("U001", "Alice Johnson", "alice@example.com"),
("U002", "Bob Smith", "bob@example.com"),
("U003", "Charlie Brown", "charlie@example.com"),
]
# Create and render table
table = create_interactive_table()
table_origin = (20, 30)
renderer = TableRenderer(
table,
origin=table_origin,
available_width=860,
draw=page._draw,
style=table_style,
canvas=canvas
)
renderer.render()
# Get table dimensions for button positioning
column_widths = renderer._column_widths
row_heights = renderer._row_heights
# Render interactive buttons on top of table
buttons_with_bounds = render_buttons_in_table(
canvas, page._draw, table_origin,
column_widths, row_heights, users
)
# Add instructions (position below the table)
# Calculate actual table height based on rows
header_height = row_heights.get("header", 30)
body_height = row_heights.get("body", 30) * len(users)
actual_table_height = header_height + body_height + (len(users) + 2) * 2 # +borders
inst_font = Font(font_size=12)
y_offset = table_origin[1] + actual_table_height + 50
page._draw.text(
(20, y_offset),
"Interactive Table Demo:",
fill=(50, 50, 50),
font=inst_font.font
)
note_font = Font(font_size=10)
page._draw.text(
(20, y_offset + 25),
"• Buttons are rendered at calculated positions within table cells",
fill=(80, 80, 80),
font=note_font.font
)
page._draw.text(
(20, y_offset + 45),
"• Click detection maps coordinates to button callbacks",
fill=(80, 80, 80),
font=note_font.font
)
page._draw.text(
(20, y_offset + 65),
"• Try simulated clicks below:",
fill=(80, 80, 80),
font=note_font.font
)
# Save
output_path = "docs/images/example_14_interactive_table.png"
canvas.save(output_path)
print(f"✓ Working interactive table demo created!")
print(f" Output: {output_path}")
print(f" Image size: {canvas.size}")
print(f"\nDemonstrating button click detection:")
# Simulate some clicks to demonstrate functionality
actions_col_x = table_origin[0] + sum(column_widths[:3]) + 6
caption_height = 30
header_height = row_heights.get("header", 30)
body_row_height = row_heights.get("body", 30)
# Calculate first body row position (after caption + header + border)
first_row_y = table_origin[1] + caption_height + header_height + 2
test_clicks = [
(100, 100, "Click outside table"),
(actions_col_x + 10, first_row_y + 15, "View button - Alice"),
(actions_col_x + 75, first_row_y + 15, "Edit button - Alice"),
(actions_col_x + 140, first_row_y + 15, "Delete button - Alice"),
(actions_col_x + 10, first_row_y + body_row_height + 17, "View button - Bob"),
]
for x, y, desc in test_clicks:
print(f"\n Click at ({x}, {y}) - {desc}:")
clicked = handle_click((x, y), buttons_with_bounds)
if not clicked:
print(f" No button at this position")
if __name__ == "__main__":
main()
+310
View File
@@ -0,0 +1,310 @@
# PyWebLayout Examples
This directory contains example scripts demonstrating the pyWebLayout library.
## Getting Started Examples
These examples demonstrate the core rendering capabilities of pyWebLayout:
### 01. Simple Page Rendering
**`01_simple_page_rendering.py`** - Introduction to the Page system
```bash
python 01_simple_page_rendering.py
```
Demonstrates:
- Creating pages with different styles
- Setting borders, padding, and backgrounds
- Understanding page layout structure
- Basic rendering to images
![Page Rendering Example](../docs/images/example_01_page_rendering.png)
### 02. Text and Layout
**`02_text_and_layout.py`** - HTML parsing and text rendering
```bash
python 02_text_and_layout.py
```
Demonstrates:
- Parsing HTML content
- Text alignment options
- Font sizes and styles
- Document structure
![Text and Layout Example](../docs/images/example_02_text_and_layout.png)
### 03. Page Layouts
**`03_page_layouts.py`** - Different page configurations
```bash
python 03_page_layouts.py
```
Demonstrates:
- Various page sizes (portrait, landscape, square)
- Different aspect ratios
- Border and padding variations
- Color schemes
![Page Layouts Example](../docs/images/example_03_page_layouts.png)
### 04. Table Rendering
**`04_table_rendering.py`** - HTML table rendering with styling
```bash
python 04_table_rendering.py
```
Demonstrates:
- Rendering HTML tables
- Table headers and body rows
- Cell borders and padding
- Caption support
- Custom table styling
![Table Rendering Example](../docs/images/example_04_table_rendering.png)
### 05. Tables with Images
**`05_html_table_with_images.py`** - Tables containing images and mixed content
```bash
python 05_html_table_with_images.py
```
Demonstrates:
- Creating tables programmatically
- Adding images to table cells
- Book catalog and product showcase tables
- Mixed content (images and text) in cells
- Using cover images from test data
- HTML table parsing with `<img>` tags
![Table with Images Example](../docs/images/example_05_html_table_with_images.png)
### 06. Functional Elements (Interactive)
**`06_functional_elements_demo.py`** - Interactive buttons and forms with callbacks
```bash
python 06_functional_elements_demo.py
```
Demonstrates:
- Creating interactive buttons
- Building forms with multiple field types
- Post-layout callback binding
- CallbackRegistry system for managing interactables
- Accessing application state from callbacks
- Batch callback operations
- Simulating user interactions
![Functional Elements Example](../docs/images/example_06_functional_elements.png)
### 07. Button Pressed States (Interactive)
**`07_pressed_state_demo.py`** - Visual feedback for button interactions
```bash
python 07_pressed_state_demo.py
```
Demonstrates:
- Button pressed/released state management
- Visual feedback timing (150ms press duration)
- Automatic interaction handling with `InteractionHandler`
- Manual state management for custom event loops
- Dirty flag system for optimized re-rendering
- State tracking with `InteractionStateManager`
![Button Pressed State Animation](../docs/images/example_07_button_animation.gif)
*Animated GIF showing button press sequence: initial → pressed → released*
---
## 🆕 New Examples (2024-11)
These examples address critical coverage gaps and demonstrate advanced features:
### 08. Bundled Fonts Showcase
**`08_bundled_fonts_demo.py`** - Demonstration of all bundled fonts
```bash
python 08_bundled_fonts_demo.py
```
Demonstrates:
- DejaVu Sans (Sans-serif)
- DejaVu Serif (Serif)
- DejaVu Sans Mono (Monospace)
- All font variants: Regular, Bold, Italic, Bold Italic
![Bundled Fonts Example](../docs/images/demo_08_bundled_fonts.png)
### 08. Pagination with PageBreak ✅
**`08_pagination_demo.py`** - Multi-page documents with explicit and automatic pagination
```bash
python 08_pagination_demo.py
```
**Test Coverage:** [tests/examples/test_08_pagination_demo.py](../tests/examples/test_08_pagination_demo.py) - 11 tests
Demonstrates:
- Using `PageBreak` to force content onto new pages
- Multi-page document layout with explicit breaks
- Automatic pagination when content overflows
- Page numbering functionality
- Document flow control
- Combining pages into vertical strips
**Coverage Impact:** Fills critical gap - PageBreak layouter had NO examples before this!
![Pagination Example](../docs/images/example_08_pagination_explicit.png)
### 09. Link Navigation (NEW) ✅
**`09_link_navigation_demo.py`** - All link types and interactive navigation
```bash
python 09_link_navigation_demo.py
```
**Test Coverage:** [tests/examples/test_09_link_navigation_demo.py](../tests/examples/test_09_link_navigation_demo.py) - 10 tests
Demonstrates:
- **Internal links** - Document navigation (`#section1`, `#section2`)
- **External links** - Web URLs (`https://example.com`)
- **API links** - API endpoints (`/api/settings`, `/api/save`)
- **Function links** - Direct function calls (`calculate()`, `process()`)
- Link styling (underlined, color-coded by type)
- Link callbacks and interactivity
- Mixed text and link paragraphs
**Coverage Impact:** Comprehensive - All 4 LinkType variations demonstrated!
![Link Navigation Example](../docs/images/example_09_link_navigation.png)
### 10. Comprehensive Forms (NEW) ✅
**`10_forms_demo.py`** - All 14 form field types with validation
```bash
python 10_forms_demo.py
```
**Test Coverage:** [tests/examples/test_10_forms_demo.py](../tests/examples/test_10_forms_demo.py) - 9 tests
Demonstrates all 14 FormFieldType variations:
**Text-Based Fields:**
- TEXT, EMAIL, PASSWORD, URL, TEXTAREA
**Number/Date/Time Fields:**
- NUMBER, DATE, TIME, RANGE, COLOR
**Selection Fields:**
- CHECKBOX, RADIO, SELECT, HIDDEN
**Coverage Impact:** Complete - All 14 field types across 4 practical form examples!
![Comprehensive Forms Example](../docs/images/example_10_forms.png)
### 11. Table Text Wrapping (NEW) ✅
**`11_table_text_wrapping_demo.py`** - Automatic line wrapping in table cells
```bash
python 11_table_text_wrapping_demo.py
```
**Simple Version:** `11b_simple_table_wrapping.py` - Quick demonstration
Demonstrates:
- **Automatic line wrapping** - Text wraps across multiple lines within cells
- **Word hyphenation** - Long words are intelligently hyphenated
- **Narrow columns** - Aggressive wrapping for tight spaces
- **Mixed content** - Both short and long text in the same table
- **Technical documentation** - API reference style tables
- **News layouts** - Article-style table content
**Implementation:** Uses the Line class from `pyWebLayout.concrete.text` with:
- Word-by-word fitting with intelligent spacing
- Pyphen-based dictionary hyphenation
- Brute-force splitting for edge cases
- Proper baseline alignment and metrics
![Table Text Wrapping Example](../docs/images/example_11_table_text_wrapping.png)
---
## Running the Examples
All examples can be run directly from the examples directory:
```bash
cd examples
# Getting Started (01-07)
python 01_simple_page_rendering.py # Page layouts
python 02_text_and_layout.py # Text alignment with justified text
python 03_page_layouts.py # Various page sizes
python 04_table_rendering.py # Table styles
python 05_html_table_with_images.py # HTML tables with images
python 06_functional_elements_demo.py # Interactive buttons and forms
python 07_pressed_state_demo.py # Button pressed states (generates GIF)
# Advanced Features (08-11)
python 08_bundled_fonts_demo.py # Bundled font showcase
python 08_pagination_demo.py # Multi-page documents
python 09_link_navigation_demo.py # All link types
python 10_forms_demo.py # All form field types
python 11_table_text_wrapping_demo.py # Table text wrapping
python 11b_simple_table_wrapping.py # Simple wrapping demo
```
Output images are saved to the `docs/images/` directory.
## Recent Improvements
### ✅ Justified Text Fix (2024-11-10)
Lines using justified alignment now properly fill the entire width by:
- Calculating base spacing and remainder pixels
- Distributing remainder across word gaps to eliminate short lines
- Removing max_spacing constraint for true justification
**Affected examples:** 02, 11, 11b - All text now perfectly justified!
### ✅ Animated Button States (2024-11-10)
Example 07 now automatically generates an animated GIF showing button interactions:
- Initial state (1000ms)
- Pressed state (200ms)
- Released state (500ms)
- Loops continuously
**Output:** `docs/images/example_07_button_animation.gif`
### Running Tests
All new examples (08, 09, 10) include comprehensive test coverage:
```bash
# Run all example tests
python -m pytest tests/examples/ -v
# Run specific test file
python -m pytest tests/examples/test_08_pagination_demo.py -v
python -m pytest tests/examples/test_09_link_navigation_demo.py -v
python -m pytest tests/examples/test_10_forms_demo.py -v
```
**Total Test Coverage:** 30 tests (11 + 10 + 9), all passing ✅
## Additional Documentation
- `README_HTML_MULTIPAGE.md` - HTML multi-page rendering guide
- `../ARCHITECTURE.md` - Detailed explanation of the Abstract/Concrete architecture
- `../docs/images/README.md` - Visual documentation index with all examples
- `../pyWebLayout/layout/README_EREADER_API.md` - EbookReader API reference
## Debug/Development Scripts
Low-level debug and rendering scripts have been moved to the `scripts/` directory.
+252
View File
@@ -0,0 +1,252 @@
"""
Generate a demo image for README.md showing font family switching feature.
Creates a side-by-side comparison of the same content rendered in
Sans, Serif, and Monospace fonts.
"""
from pyWebLayout.abstract import Paragraph, Heading, Word
from pyWebLayout.abstract.block import HeadingLevel
from pyWebLayout.style import Font
from pyWebLayout.style.fonts import BundledFont, FontWeight
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.layout.ereader_manager import create_ereader_manager
from PIL import Image, ImageDraw, ImageFont
def create_demo_content():
"""Create concise demo content that fits nicely on a small page"""
blocks = []
# Title
title_font = Font.from_family(BundledFont.SANS, font_size=28, weight=FontWeight.BOLD)
title = Heading(level=HeadingLevel.H1, style=title_font)
for word in "The Adventure Begins".split():
title.add_word(Word(word, title_font))
blocks.append(title)
# Paragraph
body_font = Font.from_family(BundledFont.SANS, font_size=14)
para = Paragraph(body_font)
text = (
"In the quiet village of Millbrook, young Emma discovered an ancient map "
"hidden in her grandmother's attic. The parchment revealed a mysterious "
"forest path marked with symbols she had never seen before. With courage "
"in her heart and the map in her pocket, she set out at dawn to uncover "
"the secrets that lay beyond the old oak trees."
)
for word in text.split():
para.add_word(Word(word, body_font))
blocks.append(para)
return blocks
def render_with_font_family(blocks, page_size, font_family, family_name):
"""Render a page with a specific font family"""
manager = create_ereader_manager(
blocks,
page_size,
document_id=f"demo_{family_name.lower()}"
)
# Set font family (None means original/default)
manager.set_font_family(font_family)
# Get the first page
page = manager.get_current_page()
return page.render()
def create_comparison_image():
"""Create a side-by-side comparison of all three font families"""
# Page size for each panel
page_width = 400
page_height = 300
# Create demo content
print("Creating demo content...")
blocks = create_demo_content()
# Render with each font family
print("Rendering with Sans font...")
sans_image = render_with_font_family(
blocks, (page_width, page_height), BundledFont.SANS, "Sans"
)
print("Rendering with Serif font...")
serif_image = render_with_font_family(
blocks, (page_width, page_height), BundledFont.SERIF, "Serif"
)
print("Rendering with Monospace font...")
mono_image = render_with_font_family(
blocks, (page_width, page_height), BundledFont.MONOSPACE, "Monospace"
)
# Create a composite image with all three side by side
spacing = 20
label_height = 30
total_width = page_width * 3 + spacing * 4
total_height = page_height + label_height + spacing * 2
composite = Image.new('RGB', (total_width, total_height), color='#f5f5f5')
# Paste the three images
x_positions = [
spacing,
spacing * 2 + page_width,
spacing * 3 + page_width * 2
]
for img, x_pos in zip([sans_image, serif_image, mono_image], x_positions):
composite.paste(img, (x_pos, label_height + spacing))
# Add labels
draw = ImageDraw.Draw(composite)
# Try to use a nice font, fallback to default if not available
try:
label_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 20)
except:
label_font = ImageFont.load_default()
labels = ["Sans-Serif", "Serif", "Monospace"]
for label, x_pos in zip(labels, x_positions):
# Calculate text position to center it
bbox = draw.textbbox((0, 0), label, font=label_font)
text_width = bbox[2] - bbox[0]
text_x = x_pos + (page_width - text_width) // 2
draw.text((text_x, 5), label, fill='#333333', font=label_font)
# Save the image
output_path = "docs/images/font_family_switching.png"
composite.save(output_path, quality=95)
print(f"\n✓ Saved demo image to: {output_path}")
print(f" Image size: {total_width}x{total_height}")
return output_path
def create_single_vertical_comparison():
"""Create a vertical comparison that's better for README"""
# Page size for each panel
page_width = 700
page_height = 280
# Create demo content
print("\nCreating vertical comparison for README...")
blocks = create_demo_content()
# Render with each font family
print(" Rendering Sans...")
sans_image = render_with_font_family(
blocks, (page_width, page_height), BundledFont.SANS, "Sans"
)
print(" Rendering Serif...")
serif_image = render_with_font_family(
blocks, (page_width, page_height), BundledFont.SERIF, "Serif"
)
print(" Rendering Monospace...")
mono_image = render_with_font_family(
blocks, (page_width, page_height), BundledFont.MONOSPACE, "Monospace"
)
# Create a composite image stacked vertically
spacing = 15
label_width = 120
total_width = page_width + label_width + spacing * 2
total_height = page_height * 3 + spacing * 4
composite = Image.new('RGB', (total_width, total_height), color='#ffffff')
# Add a subtle border
draw = ImageDraw.Draw(composite)
draw.rectangle([(0, 0), (total_width-1, total_height-1)], outline='#e0e0e0', width=1)
# Paste the three images vertically
y_positions = [
spacing,
spacing * 2 + page_height,
spacing * 3 + page_height * 2
]
images_data = [
(sans_image, "Sans-Serif", "#4A90E2"),
(serif_image, "Serif", "#E94B3C"),
(mono_image, "Monospace", "#50C878")
]
# Try to use a nice font
try:
label_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 16)
small_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 11)
except:
label_font = ImageFont.load_default()
small_font = ImageFont.load_default()
for (img, label, color), y_pos in zip(images_data, y_positions):
# Paste the page image
composite.paste(img, (label_width + spacing, y_pos))
# Draw label background
draw.rectangle(
[(spacing, y_pos + 10), (label_width, y_pos + 40)],
fill=color
)
# Draw label text
draw.text(
(spacing + 10, y_pos + 17),
label,
fill='#ffffff',
font=label_font
)
# Draw font description
descriptions = {
"Sans-Serif": "Clean & Modern",
"Serif": "Classic & Formal",
"Monospace": "Code & Technical"
}
draw.text(
(spacing + 5, y_pos + 50),
descriptions[label],
fill='#666666',
font=small_font
)
# Save the image
output_path = "docs/images/font_family_switching_vertical.png"
composite.save(output_path, quality=95)
print(f" ✓ Saved: {output_path}")
print(f" Size: {total_width}x{total_height}")
return output_path
if __name__ == "__main__":
print("=" * 70)
print("Generating README Demo Images")
print("=" * 70)
# Create both versions
horizontal_path = create_comparison_image()
vertical_path = create_single_vertical_comparison()
print("\n" + "=" * 70)
print("Demo images generated successfully!")
print("=" * 70)
print(f"\nHorizontal comparison: {horizontal_path}")
print(f"Vertical comparison: {vertical_path}")
print("\nRecommended for README: vertical version")
print("\nMarkdown snippet:")
print("```markdown")
print("![Font Family Switching](docs/images/font_family_switching_vertical.png)")
print("```")
print()