This commit is contained in:
parent
fe140ba91f
commit
cda1ec3363
BIN
doc/images/library_reading_demo.gif
Normal file
BIN
doc/images/library_reading_demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 591 KiB |
341
examples/generate_library_demo_gif.py
Executable file
341
examples/generate_library_demo_gif.py
Executable file
@ -0,0 +1,341 @@
|
||||
#!/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()
|
||||
Loading…
x
Reference in New Issue
Block a user