dreader-application/examples/demo_settings_overlay.py
Duncan Tourolle 4b4e9612a0
Some checks failed
Python CI / test (3.13) (push) Has been cancelled
Python CI / test (3.12) (push) Has been cancelled
update demo files
2025-11-10 18:22:44 +01:00

471 lines
17 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()
# Use Alice in Wonderland test book (has actual content)
epub_path = Path(__file__).parent.parent / 'tests' / 'data' / 'test.epub'
if not epub_path.exists():
print("Error: test.epub not found!")
print(f"Looked in: {epub_path}")
return
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()
# Skip to a page with actual content (past cover/title pages)
for _ in range(3):
reader.next_page()
# 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._active_overlay and reader._active_overlay._overlay_reader:
page = reader._active_overlay._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")
# Get current overlay state for word spacing section
current_overlay3 = reader.get_current_page()
# Frame W: Tap on word spacing increase button
print("Frame W: Tap on word spacing increase...")
if 'setting:word_spacing_increase' in link_positions:
tap_x4, tap_y4 = link_positions['setting:word_spacing_increase']
print(f" Using coordinates: ({tap_x4}, {tap_y4})")
tap_visual4 = add_tap_indicator(current_overlay3, tap_x4, tap_y4, "Increase")
annotated_ws_tap = add_gesture_annotation(tap_visual4, "Tap to increase word spacing", "bottom")
frames.append(annotated_ws_tap)
frame_duration.append(1500) # 1.5 seconds
# Frames W+1 to W+5: Word spacing increased (live preview) - show each tap individually
print("Frames W+1 to W+5: Word spacing increased with live preview (5 taps, showing each)...")
for i in range(5):
event_tap_word = TouchEvent(gesture=GestureType.TAP, x=tap_x4, y=tap_y4)
response = reader.handle_touch(event_tap_word)
print(f" Tap {i+1}: {response.action} - Word spacing: {response.data.get('word_spacing', 'N/A')}")
# Get updated overlay image after each tap
updated_overlay4 = reader.get_current_page()
annotated = add_gesture_annotation(
updated_overlay4,
f"Word Spacing: {reader.page_style.word_spacing}px (tap {i+1}/5)",
"top"
)
frames.append(annotated)
frame_duration.append(800) # 0.8 seconds per tap
# Hold on final word spacing for a bit longer
final_word_overlay = reader.get_current_page()
annotated_final_ws = add_gesture_annotation(
final_word_overlay,
f"Word Spacing: {reader.page_style.word_spacing}px (complete)",
"top"
)
frames.append(annotated_final_ws)
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, {reader.page_style.word_spacing}px word",
"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()