dreader-application/examples/generate_library_demo_gif.py
2025-11-12 18:52:08 +00:00

342 lines
11 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Generate demo GIF showing the complete library ↔ reading workflow.
This script creates an animated GIF demonstrating:
1. Library view with multiple books
2. Selecting a book by tapping
3. Reading the book (showing 5 pages)
4. Closing the book (back to library)
5. Reopening the same book
6. Auto-resuming at the saved position
Usage:
python generate_library_demo_gif.py path/to/library/directory output.gif
"""
import sys
import os
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from dreader.library import LibraryManager
from dreader.application import EbookReader
from dreader.gesture import TouchEvent, GestureType
def add_annotation(image: Image.Image, text: str, position: str = "top") -> Image.Image:
"""
Add annotation text to an image.
Args:
image: PIL Image to annotate
text: Annotation text
position: "top" or "bottom"
Returns:
New image with annotation
"""
# Create a copy
img = image.copy()
draw = ImageDraw.Draw(img)
# 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()
# Get text size
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# Calculate position
x = (img.width - text_width) // 2
if position == "top":
y = 20
else:
y = img.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 img
def add_tap_indicator(image: Image.Image, x: int, y: int, label: str = "TAP") -> Image.Image:
"""
Add a visual tap indicator at coordinates.
Args:
image: PIL Image
x, y: Tap coordinates
label: Label text
Returns:
New image with tap indicator
"""
img = image.copy()
draw = ImageDraw.Draw(img)
# Draw circle at tap location
radius = 30
draw.ellipse(
[x - radius, y - radius, x + radius, y + radius],
outline=(255, 0, 0),
width=4
)
# Draw crosshair
cross_size = 10
draw.line([x - cross_size, y, x + cross_size, y], fill=(255, 0, 0), width=3)
draw.line([x, y - cross_size, x, y + cross_size], fill=(255, 0, 0), width=3)
# Draw 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]
text_height = bbox[3] - bbox[1]
# Position label above tap
label_x = x - text_width // 2
label_y = y - radius - text_height - 10
# Background for label
padding = 5
draw.rectangle(
[label_x - padding, label_y - padding,
label_x + text_width + padding, label_y + text_height + padding],
fill=(255, 0, 0)
)
draw.text((label_x, label_y), label, fill=(255, 255, 255), font=font)
return img
def generate_library_demo_gif(library_path: str, output_path: str):
"""
Generate the demo GIF.
Args:
library_path: Path to directory containing EPUB files
output_path: Output GIF file path
"""
frames = []
frame_durations = [] # Duration for each frame in milliseconds
print("Generating library demo GIF...")
print("=" * 70)
# ===================================================================
# FRAME 1: Library view
# ===================================================================
print("\n1. Rendering library view...")
library = LibraryManager(
library_path=library_path,
page_size=(800, 1200)
)
books = library.scan_library()
print(f" Found {len(books)} books")
if len(books) == 0:
print("Error: No books found in library")
sys.exit(1)
library_image = library.render_library()
annotated = add_annotation(library_image, "📚 My Library - Select a book", "top")
frames.append(annotated)
frame_durations.append(2000) # Hold for 2 seconds
# ===================================================================
# FRAME 2: Show tap on first book
# ===================================================================
print("2. Showing book selection...")
tap_x, tap_y = 400, 150 # Approximate position of first book
tap_frame = add_tap_indicator(library_image, tap_x, tap_y, "SELECT BOOK")
annotated = add_annotation(tap_frame, "📚 Tap to open book", "top")
frames.append(annotated)
frame_durations.append(1500)
# Get the selected book
selected_book = library.handle_library_tap(tap_x, tap_y)
if not selected_book:
selected_book = books[0]['path']
print(f" Selected: {selected_book}")
# ===================================================================
# FRAME 3-7: Reading pages
# ===================================================================
print("3. Opening book and reading pages...")
reader = EbookReader(
page_size=(800, 1200),
margin=40,
background_color=(255, 255, 255)
)
reader.load_epub(selected_book)
book_info = reader.get_book_info()
print(f" Title: {book_info['title']}")
# First page
page = reader.get_current_page()
annotated = add_annotation(page, f"📖 {book_info['title']} - Page 1", "top")
frames.append(annotated)
frame_durations.append(1500)
# Turn 4 more pages (total 5 pages)
for i in range(2, 6):
print(f" Reading page {i}...")
reader.next_page()
page = reader.get_current_page()
annotated = add_annotation(page, f"📖 Reading - Page {i}", "top")
frames.append(annotated)
frame_durations.append(1000) # Faster page turns
# ===================================================================
# FRAME 8: Show settings overlay with "Back to Library"
# ===================================================================
print("4. Opening settings overlay...")
settings_overlay = reader.open_settings_overlay()
if settings_overlay:
annotated = add_annotation(settings_overlay, "⚙️ Settings - Tap 'Back to Library'", "top")
# Show where to tap (estimated position of back button)
tap_frame = add_tap_indicator(annotated, 400, 950, "BACK")
frames.append(tap_frame)
frame_durations.append(2000)
# ===================================================================
# FRAME 9: Save position and return to library
# ===================================================================
print("5. Saving position and returning to library...")
# Save current position for resume
reader.save_position("__auto_resume__")
pos_info = reader.get_position_info()
saved_progress = pos_info['progress'] * 100
print(f" Saved at {saved_progress:.1f}% progress")
# Close reader
reader.close()
# Re-render library
library_image = library.render_library()
annotated = add_annotation(library_image, "📚 Back to Library (position saved)", "top")
frames.append(annotated)
frame_durations.append(2000)
# ===================================================================
# FRAME 10: Tap same book again
# ===================================================================
print("6. Re-selecting same book...")
tap_frame = add_tap_indicator(library_image, tap_x, tap_y, "REOPEN")
annotated = add_annotation(tap_frame, "📚 Tap to reopen book", "top")
frames.append(annotated)
frame_durations.append(1500)
# ===================================================================
# FRAME 11: Reopen book and auto-resume
# ===================================================================
print("7. Reopening book with auto-resume...")
reader2 = EbookReader(
page_size=(800, 1200),
margin=40,
background_color=(255, 255, 255)
)
reader2.load_epub(selected_book)
# Load saved position
resumed_page = reader2.load_position("__auto_resume__")
if resumed_page:
pos_info = reader2.get_position_info()
progress = pos_info['progress'] * 100
print(f" ✓ Resumed at {progress:.1f}% progress")
annotated = add_annotation(resumed_page, f"✅ Auto-resumed at {progress:.1f}%", "top")
frames.append(annotated)
frame_durations.append(3000) # Hold final frame longer
reader2.close()
# ===================================================================
# Save GIF
# ===================================================================
print("\n8. Saving GIF...")
print(f" Total frames: {len(frames)}")
print(f" Output: {output_path}")
# Save as GIF with variable durations
frames[0].save(
output_path,
save_all=True,
append_images=frames[1:],
duration=frame_durations,
loop=0, # Loop forever
optimize=False # Keep quality
)
print(f"\n✓ Demo GIF created: {output_path}")
print(f" Size: {os.path.getsize(output_path) / 1024 / 1024:.1f} MB")
# Cleanup
library.cleanup()
print("\n" + "=" * 70)
print("Demo complete!")
print("\nThe GIF demonstrates:")
print(" 1. Library view with book selection")
print(" 2. Opening a book and reading 5 pages")
print(" 3. Settings overlay with 'Back to Library' button")
print(" 4. Returning to library (with position saved)")
print(" 5. Reopening the same book")
print(" 6. Auto-resuming at saved position")
def main():
"""Main entry point."""
if len(sys.argv) < 2:
print("Usage: python generate_library_demo_gif.py path/to/library [output.gif]")
print("\nExample:")
print(" python generate_library_demo_gif.py tests/data/library-epub/")
print(" python generate_library_demo_gif.py tests/data/library-epub/ doc/images/custom_demo.gif")
sys.exit(1)
library_path = sys.argv[1]
output_path = sys.argv[2] if len(sys.argv) > 2 else "doc/images/library_reading_demo.gif"
if not os.path.exists(library_path):
print(f"Error: Directory not found: {library_path}")
sys.exit(1)
if not os.path.isdir(library_path):
print(f"Error: Not a directory: {library_path}")
sys.exit(1)
try:
generate_library_demo_gif(library_path, output_path)
except Exception as e:
print(f"\nError generating demo: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()