297 lines
8.8 KiB
Python
Executable File
297 lines
8.8 KiB
Python
Executable File
#!/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()
|