dreader-application/examples/demo_settings_overlay.py
Duncan Tourolle 1993b0bf48
All checks were successful
Python CI / test (push) Successful in 4m10s
Updated demo and fixed bug in spacing
2025-11-08 18:51:33 +01:00

426 lines
15 KiB
Python

#!/usr/bin/env python3
"""
Demo script for Settings overlay feature.
This script demonstrates the complete settings overlay workflow:
1. Display reading page
2. Swipe down from top to open settings overlay
3. Display settings overlay with controls
4. Tap on font size increase button
5. Show live preview update (background page changes)
6. Tap on line spacing increase button
7. Show another live preview update
8. Close overlay and show final page with new settings
Generates a GIF showing all these interactions.
"""
from pathlib import Path
from dreader import EbookReader, TouchEvent, GestureType
from PIL import Image, ImageDraw, ImageFont
def add_gesture_annotation(image: Image.Image, text: str, position: str = "top") -> Image.Image:
"""
Add a text annotation to an image showing what gesture is being performed.
Args:
image: Base image
text: Annotation text
position: "top" or "bottom"
Returns:
Image with annotation
"""
# Create a copy
annotated = image.copy()
draw = ImageDraw.Draw(annotated)
# Try to use a nice font, fall back to default
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 24)
except:
font = ImageFont.load_default()
# Calculate text position
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
x = (image.width - text_width) // 2
if position == "top":
y = 20
else:
y = image.height - text_height - 20
# Draw background rectangle
padding = 10
draw.rectangle(
[x - padding, y - padding, x + text_width + padding, y + text_height + padding],
fill=(0, 0, 0, 200)
)
# Draw text
draw.text((x, y), text, fill=(255, 255, 255), font=font)
return annotated
def add_swipe_arrow(image: Image.Image, start_y: int, end_y: int) -> Image.Image:
"""
Add a visual swipe arrow to show gesture direction.
Args:
image: Base image
start_y: Starting Y position
end_y: Ending Y position
Returns:
Image with arrow overlay
"""
annotated = image.copy()
draw = ImageDraw.Draw(annotated)
# Draw arrow in center of screen
x = image.width // 2
# Draw line
draw.line([(x, start_y), (x, end_y)], fill=(255, 100, 100), width=5)
# Draw arrowhead
arrow_size = 20
if end_y < start_y: # Upward arrow
draw.polygon([
(x, end_y),
(x - arrow_size, end_y + arrow_size),
(x + arrow_size, end_y + arrow_size)
], fill=(255, 100, 100))
else: # Downward arrow
draw.polygon([
(x, end_y),
(x - arrow_size, end_y - arrow_size),
(x + arrow_size, end_y - arrow_size)
], fill=(255, 100, 100))
return annotated
def add_tap_indicator(image: Image.Image, x: int, y: int, label: str = "") -> Image.Image:
"""
Add a visual tap indicator to show where user tapped.
Args:
image: Base image
x, y: Tap coordinates
label: Optional label for the tap
Returns:
Image with tap indicator
"""
annotated = image.copy()
draw = ImageDraw.Draw(annotated)
# Draw circle at tap location
radius = 30
draw.ellipse(
[x - radius, y - radius, x + radius, y + radius],
outline=(255, 100, 100),
width=5
)
# Draw crosshair
draw.line([(x - radius - 10, y), (x + radius + 10, y)], fill=(255, 100, 100), width=3)
draw.line([(x, y - radius - 10), (x, y + radius + 10)], fill=(255, 100, 100), width=3)
# Add label if provided
if label:
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 18)
except:
font = ImageFont.load_default()
bbox = draw.textbbox((0, 0), label, font=font)
text_width = bbox[2] - bbox[0]
# Position label above tap point
label_x = x - text_width // 2
label_y = y - radius - 40
draw.text((label_x, label_y), label, fill=(255, 100, 100), font=font)
return annotated
def main():
"""Generate Settings overlay demo GIF"""
print("=== Settings Overlay Demo ===")
print()
# Find a test EPUB
epub_dir = Path(__file__).parent.parent / 'tests' / 'data' / 'library-epub'
epubs = list(epub_dir.glob('*.epub'))
if not epubs:
print("Error: No test EPUB files found!")
print(f"Looked in: {epub_dir}")
return
epub_path = epubs[0]
print(f"Using book: {epub_path.name}")
# Create reader
reader = EbookReader(page_size=(800, 1200))
# Load book
print("Loading book...")
success = reader.load_epub(str(epub_path))
if not success:
print("Error: Failed to load EPUB!")
return
print(f"Loaded: {reader.book_title} by {reader.book_author}")
print()
# Prepare frames for GIF
frames = []
frame_duration = [] # Duration in milliseconds for each frame
# Frame 1: Initial reading page
print("Frame 1: Initial reading page...")
page1 = reader.get_current_page()
annotated1 = add_gesture_annotation(page1, f"Reading: {reader.book_title}", "top")
frames.append(annotated1)
frame_duration.append(2000) # 2 seconds
# Frame 2: Show swipe down gesture
print("Frame 2: Swipe down gesture...")
swipe_visual = add_swipe_arrow(page1, 100, 300)
annotated2 = add_gesture_annotation(swipe_visual, "Swipe down from top", "top")
frames.append(annotated2)
frame_duration.append(1000) # 1 second
# Frame 3: Settings overlay appears
print("Frame 3: Settings overlay opens...")
event_swipe_down = TouchEvent(gesture=GestureType.SWIPE_DOWN, x=400, y=100)
response = reader.handle_touch(event_swipe_down)
print(f" Response: {response.action}")
# Get the overlay image by calling open_settings_overlay again
overlay_image = reader.open_settings_overlay()
annotated3 = add_gesture_annotation(overlay_image, "Settings", "top")
frames.append(annotated3)
frame_duration.append(3000) # 3 seconds to read
# Find actual button coordinates by querying the overlay
print("Querying overlay for button positions...")
link_positions = {}
if reader.overlay_manager._overlay_reader:
page = reader.overlay_manager._overlay_reader.manager.get_current_page()
# Scan for all links with very fine granularity to catch all buttons
for y in range(0, 840, 3):
for x in range(0, 480, 3):
result = page.query_point((x, y))
if result and result.link_target:
if result.link_target not in link_positions:
# Translate to screen coordinates
panel_x_offset = int((800 - 480) / 2)
panel_y_offset = int((1200 - 840) / 2)
screen_x = x + panel_x_offset
screen_y = y + panel_y_offset
link_positions[result.link_target] = (screen_x, screen_y)
for link, (x, y) in sorted(link_positions.items()):
print(f" Found: {link} at ({x}, {y})")
# Frame 4: Tap on font size increase button
print("Frame 4: Tap on font size increase...")
if 'setting:font_increase' in link_positions:
tap_x, tap_y = link_positions['setting:font_increase']
print(f" Using coordinates: ({tap_x}, {tap_y})")
tap_visual = add_tap_indicator(overlay_image, tap_x, tap_y, "Increase")
annotated4 = add_gesture_annotation(tap_visual, "Tap to increase font size", "bottom")
frames.append(annotated4)
frame_duration.append(1500) # 1.5 seconds
# Frames 5-9: Font size increased (live preview) - show each tap individually
print("Frames 5-9: Font size increased with live preview (5 taps, showing each)...")
for i in range(5):
event_tap_font = TouchEvent(gesture=GestureType.TAP, x=tap_x, y=tap_y)
response = reader.handle_touch(event_tap_font)
print(f" Tap {i+1}: {response.action} - Font scale: {response.data.get('font_scale', 'N/A')}")
# Get updated overlay image after each tap
updated_overlay = reader.get_current_page() # This gets the composited overlay
annotated = add_gesture_annotation(
updated_overlay,
f"Font: {int(reader.base_font_scale * 100)}% (tap {i+1}/5)",
"top"
)
frames.append(annotated)
frame_duration.append(800) # 0.8 seconds per tap
# Hold on final font size for a bit longer
final_font_overlay = reader.get_current_page()
annotated_final = add_gesture_annotation(
final_font_overlay,
f"Font: {int(reader.base_font_scale * 100)}% (complete)",
"top"
)
frames.append(annotated_final)
frame_duration.append(1500) # 1.5 seconds to see the final result
else:
print(" Skipping - button not found")
updated_overlay = overlay_image
# Get current overlay state for line spacing section
current_overlay = reader.get_current_page()
# Frame N: Tap on line spacing increase button
print("Frame N: Tap on line spacing increase...")
if 'setting:line_spacing_increase' in link_positions:
tap_x2, tap_y2 = link_positions['setting:line_spacing_increase']
print(f" Using coordinates: ({tap_x2}, {tap_y2})")
tap_visual2 = add_tap_indicator(current_overlay, tap_x2, tap_y2, "Increase")
annotated_ls_tap = add_gesture_annotation(tap_visual2, "Tap to increase line spacing", "bottom")
frames.append(annotated_ls_tap)
frame_duration.append(1500) # 1.5 seconds
# Frames N+1 to N+5: Line spacing increased (live preview) - show each tap individually
print("Frames N+1 to N+5: Line spacing increased with live preview (5 taps, showing each)...")
for i in range(5):
event_tap_spacing = TouchEvent(gesture=GestureType.TAP, x=tap_x2, y=tap_y2)
response = reader.handle_touch(event_tap_spacing)
print(f" Tap {i+1}: {response.action} - Line spacing: {response.data.get('line_spacing', 'N/A')}")
# Get updated overlay image after each tap
updated_overlay2 = reader.get_current_page()
annotated = add_gesture_annotation(
updated_overlay2,
f"Line Spacing: {reader.page_style.line_spacing}px (tap {i+1}/5)",
"top"
)
frames.append(annotated)
frame_duration.append(800) # 0.8 seconds per tap
# Hold on final line spacing for a bit longer
final_spacing_overlay = reader.get_current_page()
annotated_final_ls = add_gesture_annotation(
final_spacing_overlay,
f"Line Spacing: {reader.page_style.line_spacing}px (complete)",
"top"
)
frames.append(annotated_final_ls)
frame_duration.append(1500) # 1.5 seconds to see the final result
else:
print(" Skipping - button not found")
# Get current overlay state for paragraph spacing section
current_overlay2 = reader.get_current_page()
# Frame M: Tap on paragraph spacing increase button
print("Frame M: Tap on paragraph spacing increase...")
if 'setting:block_spacing_increase' in link_positions:
tap_x3, tap_y3 = link_positions['setting:block_spacing_increase']
print(f" Using coordinates: ({tap_x3}, {tap_y3})")
tap_visual3 = add_tap_indicator(current_overlay2, tap_x3, tap_y3, "Increase")
annotated_ps_tap = add_gesture_annotation(tap_visual3, "Tap to increase paragraph spacing", "bottom")
frames.append(annotated_ps_tap)
frame_duration.append(1500) # 1.5 seconds
# Frames M+1 to M+5: Paragraph spacing increased (live preview) - show each tap individually
print("Frames M+1 to M+5: Paragraph spacing increased with live preview (5 taps, showing each)...")
for i in range(5):
event_tap_para = TouchEvent(gesture=GestureType.TAP, x=tap_x3, y=tap_y3)
response = reader.handle_touch(event_tap_para)
print(f" Tap {i+1}: {response.action} - Paragraph spacing: {response.data.get('inter_block_spacing', 'N/A')}")
# Get updated overlay image after each tap
updated_overlay3 = reader.get_current_page()
annotated = add_gesture_annotation(
updated_overlay3,
f"Paragraph Spacing: {reader.page_style.inter_block_spacing}px (tap {i+1}/5)",
"top"
)
frames.append(annotated)
frame_duration.append(800) # 0.8 seconds per tap
# Hold on final paragraph spacing for a bit longer
final_para_overlay = reader.get_current_page()
annotated_final_ps = add_gesture_annotation(
final_para_overlay,
f"Paragraph Spacing: {reader.page_style.inter_block_spacing}px (complete)",
"top"
)
frames.append(annotated_final_ps)
frame_duration.append(1500) # 1.5 seconds to see the final result
else:
print(" Skipping - button not found")
# Frame Z: Tap outside to close
print("Frame Z: Close overlay...")
final_overlay_state = reader.get_current_page()
tap_visual_close = add_tap_indicator(final_overlay_state, 100, 600, "Close")
annotated_close = add_gesture_annotation(tap_visual_close, "Tap outside to close", "bottom")
frames.append(annotated_close)
frame_duration.append(1500) # 1.5 seconds
# Final Frame: Back to reading with new settings applied
print("Final Frame: Back to reading with new settings...")
event_close = TouchEvent(gesture=GestureType.TAP, x=100, y=600)
response = reader.handle_touch(event_close)
print(f" Response: {response.action}")
final_page = reader.get_current_page()
annotated_final = add_gesture_annotation(
final_page,
f"Settings Applied: {int(reader.base_font_scale * 100)}% font, {reader.page_style.line_spacing}px line, {reader.page_style.inter_block_spacing}px para",
"top"
)
frames.append(annotated_final)
frame_duration.append(3000) # 3 seconds
# Save as GIF
output_path = Path(__file__).parent.parent / 'docs' / 'images' / 'settings_overlay_demo.gif'
output_path.parent.mkdir(parents=True, exist_ok=True)
print()
print(f"Saving GIF with {len(frames)} frames...")
frames[0].save(
output_path,
save_all=True,
append_images=frames[1:],
duration=frame_duration,
loop=0,
optimize=False
)
print(f"✓ GIF saved to: {output_path}")
print(f" Size: {output_path.stat().st_size / 1024:.1f} KB")
print(f" Frames: {len(frames)}")
print(f" Total duration: {sum(frame_duration) / 1000:.1f}s")
# Also save individual frames for documentation
frames_dir = output_path.parent / 'settings_overlay_frames'
frames_dir.mkdir(exist_ok=True)
for i, frame in enumerate(frames):
frame_path = frames_dir / f'frame_{i+1:02d}.png'
frame.save(frame_path)
print(f"✓ Individual frames saved to: {frames_dir}")
# Cleanup
reader.close()
print()
print("=== Demo Complete ===")
if __name__ == '__main__':
main()