#!/usr/bin/env python3 """ Demo script for Settings overlay feature. This script demonstrates the complete settings overlay workflow: 1. Display reading page 2. Swipe down from top to open settings overlay 3. Display settings overlay with controls 4. Tap on font size increase button 5. Show live preview update (background page changes) 6. Tap on line spacing increase button 7. Show another live preview update 8. Close overlay and show final page with new settings Generates a GIF showing all these interactions. """ from pathlib import Path from dreader import EbookReader, TouchEvent, GestureType from PIL import Image, ImageDraw, ImageFont 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 Settings overlay demo GIF""" print("=== Settings 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() # 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 down gesture print("Frame 2: Swipe down gesture...") swipe_visual = add_swipe_arrow(page1, 100, 300) annotated2 = add_gesture_annotation(swipe_visual, "Swipe down from top", "top") frames.append(annotated2) frame_duration.append(1000) # 1 second # Frame 3: Settings overlay appears print("Frame 3: Settings overlay opens...") event_swipe_down = TouchEvent(gesture=GestureType.SWIPE_DOWN, x=400, y=100) response = reader.handle_touch(event_swipe_down) print(f" Response: {response.action}") # Get the overlay image by calling open_settings_overlay again overlay_image = reader.open_settings_overlay() annotated3 = add_gesture_annotation(overlay_image, "Settings", "top") frames.append(annotated3) frame_duration.append(3000) # 3 seconds to read # Find actual button coordinates by querying the overlay print("Querying overlay for button positions...") link_positions = {} if reader.overlay_manager._overlay_reader: page = reader.overlay_manager._overlay_reader.manager.get_current_page() # Scan for all links with very fine granularity to catch all buttons for y in range(0, 840, 3): for x in range(0, 480, 3): result = page.query_point((x, y)) if result and result.link_target: if result.link_target not in link_positions: # Translate to screen coordinates panel_x_offset = int((800 - 480) / 2) panel_y_offset = int((1200 - 840) / 2) screen_x = x + panel_x_offset screen_y = y + panel_y_offset link_positions[result.link_target] = (screen_x, screen_y) for link, (x, y) in sorted(link_positions.items()): print(f" Found: {link} at ({x}, {y})") # Frame 4: Tap on font size increase button print("Frame 4: Tap on font size increase...") if 'setting:font_increase' in link_positions: tap_x, tap_y = link_positions['setting:font_increase'] print(f" Using coordinates: ({tap_x}, {tap_y})") tap_visual = add_tap_indicator(overlay_image, tap_x, tap_y, "Increase") annotated4 = add_gesture_annotation(tap_visual, "Tap to increase font size", "bottom") frames.append(annotated4) frame_duration.append(1500) # 1.5 seconds # Frames 5-9: Font size increased (live preview) - show each tap individually print("Frames 5-9: Font size increased with live preview (5 taps, showing each)...") for i in range(5): event_tap_font = TouchEvent(gesture=GestureType.TAP, x=tap_x, y=tap_y) response = reader.handle_touch(event_tap_font) print(f" Tap {i+1}: {response.action} - Font scale: {response.data.get('font_scale', 'N/A')}") # Get updated overlay image after each tap updated_overlay = reader.get_current_page() # This gets the composited overlay annotated = add_gesture_annotation( updated_overlay, f"Font: {int(reader.base_font_scale * 100)}% (tap {i+1}/5)", "top" ) frames.append(annotated) frame_duration.append(800) # 0.8 seconds per tap # Hold on final font size for a bit longer final_font_overlay = reader.get_current_page() annotated_final = add_gesture_annotation( final_font_overlay, f"Font: {int(reader.base_font_scale * 100)}% (complete)", "top" ) frames.append(annotated_final) frame_duration.append(1500) # 1.5 seconds to see the final result else: print(" Skipping - button not found") updated_overlay = overlay_image # Get current overlay state for line spacing section current_overlay = reader.get_current_page() # Frame N: Tap on line spacing increase button print("Frame N: Tap on line spacing increase...") if 'setting:line_spacing_increase' in link_positions: tap_x2, tap_y2 = link_positions['setting:line_spacing_increase'] print(f" Using coordinates: ({tap_x2}, {tap_y2})") tap_visual2 = add_tap_indicator(current_overlay, tap_x2, tap_y2, "Increase") annotated_ls_tap = add_gesture_annotation(tap_visual2, "Tap to increase line spacing", "bottom") frames.append(annotated_ls_tap) frame_duration.append(1500) # 1.5 seconds # Frames N+1 to N+5: Line spacing increased (live preview) - show each tap individually print("Frames N+1 to N+5: Line spacing increased with live preview (5 taps, showing each)...") for i in range(5): event_tap_spacing = TouchEvent(gesture=GestureType.TAP, x=tap_x2, y=tap_y2) response = reader.handle_touch(event_tap_spacing) print(f" Tap {i+1}: {response.action} - Line spacing: {response.data.get('line_spacing', 'N/A')}") # Get updated overlay image after each tap updated_overlay2 = reader.get_current_page() annotated = add_gesture_annotation( updated_overlay2, f"Line Spacing: {reader.page_style.line_spacing}px (tap {i+1}/5)", "top" ) frames.append(annotated) frame_duration.append(800) # 0.8 seconds per tap # Hold on final line spacing for a bit longer final_spacing_overlay = reader.get_current_page() annotated_final_ls = add_gesture_annotation( final_spacing_overlay, f"Line Spacing: {reader.page_style.line_spacing}px (complete)", "top" ) frames.append(annotated_final_ls) frame_duration.append(1500) # 1.5 seconds to see the final result else: print(" Skipping - button not found") # Get current overlay state for paragraph spacing section current_overlay2 = reader.get_current_page() # Frame M: Tap on paragraph spacing increase button print("Frame M: Tap on paragraph spacing increase...") if 'setting:block_spacing_increase' in link_positions: tap_x3, tap_y3 = link_positions['setting:block_spacing_increase'] print(f" Using coordinates: ({tap_x3}, {tap_y3})") tap_visual3 = add_tap_indicator(current_overlay2, tap_x3, tap_y3, "Increase") annotated_ps_tap = add_gesture_annotation(tap_visual3, "Tap to increase paragraph spacing", "bottom") frames.append(annotated_ps_tap) frame_duration.append(1500) # 1.5 seconds # Frames M+1 to M+5: Paragraph spacing increased (live preview) - show each tap individually print("Frames M+1 to M+5: Paragraph spacing increased with live preview (5 taps, showing each)...") for i in range(5): event_tap_para = TouchEvent(gesture=GestureType.TAP, x=tap_x3, y=tap_y3) response = reader.handle_touch(event_tap_para) print(f" Tap {i+1}: {response.action} - Paragraph spacing: {response.data.get('inter_block_spacing', 'N/A')}") # Get updated overlay image after each tap updated_overlay3 = reader.get_current_page() annotated = add_gesture_annotation( updated_overlay3, f"Paragraph Spacing: {reader.page_style.inter_block_spacing}px (tap {i+1}/5)", "top" ) frames.append(annotated) frame_duration.append(800) # 0.8 seconds per tap # Hold on final paragraph spacing for a bit longer final_para_overlay = reader.get_current_page() annotated_final_ps = add_gesture_annotation( final_para_overlay, f"Paragraph Spacing: {reader.page_style.inter_block_spacing}px (complete)", "top" ) frames.append(annotated_final_ps) frame_duration.append(1500) # 1.5 seconds to see the final result else: print(" Skipping - button not found") # Frame Z: Tap outside to close print("Frame Z: Close overlay...") final_overlay_state = reader.get_current_page() tap_visual_close = add_tap_indicator(final_overlay_state, 100, 600, "Close") annotated_close = add_gesture_annotation(tap_visual_close, "Tap outside to close", "bottom") frames.append(annotated_close) frame_duration.append(1500) # 1.5 seconds # Final Frame: Back to reading with new settings applied print("Final Frame: Back to reading with new settings...") event_close = TouchEvent(gesture=GestureType.TAP, x=100, y=600) response = reader.handle_touch(event_close) print(f" Response: {response.action}") final_page = reader.get_current_page() annotated_final = add_gesture_annotation( final_page, f"Settings Applied: {int(reader.base_font_scale * 100)}% font, {reader.page_style.line_spacing}px line, {reader.page_style.inter_block_spacing}px para", "top" ) frames.append(annotated_final) frame_duration.append(3000) # 3 seconds # Save as GIF output_path = Path(__file__).parent.parent / 'docs' / 'images' / 'settings_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 / 'settings_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()