diff --git a/doc/images/library_reading_demo.gif b/doc/images/library_reading_demo.gif new file mode 100644 index 0000000..4ae28f0 Binary files /dev/null and b/doc/images/library_reading_demo.gif differ diff --git a/examples/generate_library_demo_gif.py b/examples/generate_library_demo_gif.py new file mode 100755 index 0000000..cea4860 --- /dev/null +++ b/examples/generate_library_demo_gif.py @@ -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()