Update coverage badges [skip ci]
This commit is contained in:
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||

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

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

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

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

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

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

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

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

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

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

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

|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
@@ -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("")
|
||||
print("```")
|
||||
print()
|
||||
Reference in New Issue
Block a user