Executable
+296
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Demo script for TOC overlay feature.
|
||||
|
||||
This script demonstrates the complete TOC overlay workflow:
|
||||
1. Display reading page
|
||||
2. Swipe up from bottom to open TOC overlay
|
||||
3. Display TOC overlay with chapter list
|
||||
4. Tap on a chapter to navigate
|
||||
5. Close overlay and show new page
|
||||
|
||||
Generates a GIF showing all these interactions.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from dreader import EbookReader, TouchEvent, GestureType
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import time
|
||||
|
||||
|
||||
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 TOC overlay demo GIF"""
|
||||
print("=== TOC 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(f"Chapters: {len(reader.get_chapters())}")
|
||||
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 up gesture
|
||||
print("Frame 2: Swipe up gesture...")
|
||||
swipe_visual = add_swipe_arrow(page1, 1100, 900)
|
||||
annotated2 = add_gesture_annotation(swipe_visual, "Swipe up from bottom", "bottom")
|
||||
frames.append(annotated2)
|
||||
frame_duration.append(1000) # 1 second
|
||||
|
||||
# Frame 3: TOC overlay appears
|
||||
print("Frame 3: TOC overlay opens...")
|
||||
event_swipe_up = TouchEvent(gesture=GestureType.SWIPE_UP, x=400, y=1100)
|
||||
response = reader.handle_touch(event_swipe_up)
|
||||
print(f" Response: {response.action}")
|
||||
|
||||
# Get the overlay image by calling open_toc_overlay again
|
||||
# (handle_touch already opened it, but we need the image)
|
||||
overlay_image = reader.open_toc_overlay()
|
||||
annotated3 = add_gesture_annotation(overlay_image, "Table of Contents", "top")
|
||||
frames.append(annotated3)
|
||||
frame_duration.append(3000) # 3 seconds to read
|
||||
|
||||
# Frame 4: Show tap on chapter III (index 6)
|
||||
print("Frame 4: Tap on chapter III...")
|
||||
chapters = reader.get_chapters()
|
||||
if len(chapters) >= 7:
|
||||
# Calculate tap position for chapter III (7th in list, index 6)
|
||||
# Based on actual measurements from pyWebLayout link query:
|
||||
# Chapter 6 "III" link is clickable at screen position (200, 378)
|
||||
tap_x = 200
|
||||
tap_y = 378
|
||||
|
||||
tap_visual = add_tap_indicator(overlay_image, tap_x, tap_y, "III")
|
||||
annotated4 = add_gesture_annotation(tap_visual, "Tap chapter to navigate", "bottom")
|
||||
frames.append(annotated4)
|
||||
frame_duration.append(1500) # 1.5 seconds
|
||||
|
||||
# Frame 5: Navigate to chapter III
|
||||
print(f"Frame 5: Jump to chapter III (tapping at {tap_x}, {tap_y})...")
|
||||
event_tap = TouchEvent(gesture=GestureType.TAP, x=tap_x, y=tap_y)
|
||||
response = reader.handle_touch(event_tap)
|
||||
print(f" Response: {response.action}")
|
||||
|
||||
new_page = reader.get_current_page()
|
||||
|
||||
# Use the chapter title from the response data (more accurate)
|
||||
if response.action == "chapter_selected" and "chapter_title" in response.data:
|
||||
chapter_title = response.data['chapter_title']
|
||||
else:
|
||||
chapter_title = "Chapter"
|
||||
|
||||
annotated5 = add_gesture_annotation(new_page, f"Navigated to: {chapter_title}", "top")
|
||||
frames.append(annotated5)
|
||||
frame_duration.append(2000) # 2 seconds
|
||||
else:
|
||||
print(" Skipping chapter selection (not enough chapters)")
|
||||
|
||||
# Frame 6: Another page for context
|
||||
print("Frame 6: Next page...")
|
||||
reader.next_page()
|
||||
page_final = reader.get_current_page()
|
||||
annotated6 = add_gesture_annotation(page_final, "Reading continues...", "top")
|
||||
frames.append(annotated6)
|
||||
frame_duration.append(2000) # 2 seconds
|
||||
|
||||
# Save as GIF
|
||||
output_path = Path(__file__).parent.parent / 'docs' / 'images' / 'toc_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 / 'toc_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()
|
||||
Reference in New Issue
Block a user