#!/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()