""" Overlay management for dreader application. Handles rendering and compositing of overlay screens (TOC, Settings, Bookmarks) on top of the base reading page. """ from __future__ import annotations from typing import Optional, List, Dict, Any, Tuple from pathlib import Path from PIL import Image from .state import OverlayState from .html_generator import ( generate_toc_overlay, generate_settings_overlay, generate_bookmarks_overlay ) class OverlayManager: """ Manages overlay rendering and interaction. Handles: - Generating overlay HTML - Rendering HTML to images using pyWebLayout - Compositing overlays on top of base pages - Tracking current overlay state """ def __init__(self, page_size: Tuple[int, int] = (800, 1200)): """ Initialize overlay manager. Args: page_size: Size of the page/overlay (width, height) """ self.page_size = page_size self.current_overlay = OverlayState.NONE self._cached_base_page: Optional[Image.Image] = None self._cached_overlay_image: Optional[Image.Image] = None self._overlay_reader = None # Will be EbookReader instance for rendering overlays self._overlay_panel_offset: Tuple[int, int] = (0, 0) # Panel position on screen def render_html_to_image(self, html: str, size: Optional[Tuple[int, int]] = None) -> Image.Image: """ Render HTML content to a PIL Image using pyWebLayout. This creates a temporary EbookReader instance to render the HTML, then extracts the rendered page as an image. Args: html: HTML string to render size: Optional (width, height) for rendering size. Defaults to self.page_size Returns: PIL Image of the rendered HTML """ # Import here to avoid circular dependency from .application import EbookReader render_size = size if size else self.page_size # Create a temporary reader for rendering this HTML temp_reader = EbookReader( page_size=render_size, margin=15, background_color=(255, 255, 255) ) # Load the HTML content success = temp_reader.load_html( html_string=html, title="Overlay", author="", document_id="temp_overlay" ) if not success: raise ValueError("Failed to load HTML for overlay rendering") # Get the rendered page image = temp_reader.get_current_page() # Clean up temp_reader.close() return image def composite_overlay(self, base_image: Image.Image, overlay_panel: Image.Image) -> Image.Image: """ Composite overlay panel on top of base image with darkened background. Creates a popup effect by: 1. Darkening the base image (multiply by 0.5) 2. Placing the overlay panel (60% size) centered on top Args: base_image: Base page image (reading page) overlay_panel: Rendered overlay panel (TOC, settings, etc.) Returns: Composited PIL Image with popup overlay effect """ from PIL import ImageDraw, ImageEnhance import numpy as np # Convert base image to RGB result = base_image.convert('RGB').copy() # Lighten the background slightly (70% brightness for e-ink visibility) enhancer = ImageEnhance.Brightness(result) result = enhancer.enhance(0.7) # Convert overlay panel to RGB if overlay_panel.mode != 'RGB': overlay_panel = overlay_panel.convert('RGB') # Calculate centered position for the panel panel_x = int((self.page_size[0] - overlay_panel.width) / 2) panel_y = int((self.page_size[1] - overlay_panel.height) / 2) # Add a thick black border around the panel for e-ink clarity draw = ImageDraw.Draw(result) border_width = 3 draw.rectangle( [panel_x - border_width, panel_y - border_width, panel_x + overlay_panel.width + border_width, panel_y + overlay_panel.height + border_width], outline=(0, 0, 0), width=border_width ) # Paste the panel onto the dimmed background result.paste(overlay_panel, (panel_x, panel_y)) return result def open_toc_overlay(self, chapters: List[Tuple[str, int]], base_page: Image.Image) -> Image.Image: """ Open the table of contents overlay. Args: chapters: List of (chapter_title, chapter_index) tuples base_page: Current reading page to show underneath Returns: Composited image with TOC overlay on top """ # Import here to avoid circular dependency from .application import EbookReader # Calculate panel size (60% of screen) panel_width = int(self.page_size[0] * 0.6) panel_height = int(self.page_size[1] * 0.7) # Convert chapters to format expected by HTML generator chapter_data = [ {"index": idx, "title": title} for title, idx in chapters ] # Generate TOC HTML with clickable links html = generate_toc_overlay(chapter_data, page_size=(panel_width, panel_height)) # Create reader for overlay and keep it alive for querying if self._overlay_reader: self._overlay_reader.close() self._overlay_reader = EbookReader( page_size=(panel_width, panel_height), margin=15, background_color=(255, 255, 255) ) # Load the HTML content success = self._overlay_reader.load_html( html_string=html, title="Table of Contents", author="", document_id="toc_overlay" ) if not success: raise ValueError("Failed to load TOC overlay HTML") # Get the rendered page overlay_panel = self._overlay_reader.get_current_page() # Calculate and store panel position for coordinate translation panel_x = int((self.page_size[0] - panel_width) / 2) panel_y = int((self.page_size[1] - panel_height) / 2) self._overlay_panel_offset = (panel_x, panel_y) # Cache for later use self._cached_base_page = base_page.copy() self._cached_overlay_image = overlay_panel self.current_overlay = OverlayState.TOC # Composite and return return self.composite_overlay(base_page, overlay_panel) def open_settings_overlay(self, base_page: Image.Image) -> Image.Image: """ Open the settings overlay. Args: base_page: Current reading page to show underneath Returns: Composited image with settings overlay on top """ # Generate settings HTML html = generate_settings_overlay() # Render HTML to image overlay_image = self.render_html_to_image(html) # Cache for later use self._cached_base_page = base_page.copy() self._cached_overlay_image = overlay_image self.current_overlay = OverlayState.SETTINGS # Composite and return return self.composite_overlay(base_page, overlay_image) def open_bookmarks_overlay(self, bookmarks: List[Dict[str, Any]], base_page: Image.Image) -> Image.Image: """ Open the bookmarks overlay. Args: bookmarks: List of bookmark dictionaries with 'name' and 'position' keys base_page: Current reading page to show underneath Returns: Composited image with bookmarks overlay on top """ # Generate bookmarks HTML html = generate_bookmarks_overlay(bookmarks) # Render HTML to image overlay_image = self.render_html_to_image(html) # Cache for later use self._cached_base_page = base_page.copy() self._cached_overlay_image = overlay_image self.current_overlay = OverlayState.BOOKMARKS # Composite and return return self.composite_overlay(base_page, overlay_image) def close_overlay(self) -> Optional[Image.Image]: """ Close the current overlay and return to base page. Returns: Base page image (without overlay), or None if no overlay was open """ if self.current_overlay == OverlayState.NONE: return None self.current_overlay = OverlayState.NONE base_page = self._cached_base_page # Clear caches self._cached_base_page = None self._cached_overlay_image = None self._overlay_panel_offset = (0, 0) # Close overlay reader if self._overlay_reader: self._overlay_reader.close() self._overlay_reader = None return base_page def is_overlay_open(self) -> bool: """Check if an overlay is currently open.""" return self.current_overlay != OverlayState.NONE def get_current_overlay_type(self) -> OverlayState: """Get the type of currently open overlay.""" return self.current_overlay def query_overlay_pixel(self, x: int, y: int) -> Optional[Dict[str, Any]]: """ Query a pixel in the current overlay to detect interactions. Uses pyWebLayout's query_point() to detect which element was tapped, including link targets and data attributes. Args: x, y: Pixel coordinates to query (in screen space) Returns: Dictionary with query result data (text, link_target, is_interactive), or None if no overlay open or query failed """ if not self.is_overlay_open() or not self._overlay_reader: return None # Translate screen coordinates to overlay panel coordinates panel_x, panel_y = self._overlay_panel_offset overlay_x = x - panel_x overlay_y = y - panel_y # Check if coordinates are within the overlay panel if overlay_x < 0 or overlay_y < 0: return None # Get the current page from the overlay reader if not self._overlay_reader.manager: return None current_page = self._overlay_reader.manager.get_current_page() if not current_page: return None # Query the point result = current_page.query_point((overlay_x, overlay_y)) if not result: return None # Extract relevant data from QueryResult return { "text": result.text, "link_target": result.link_target, "is_interactive": result.is_interactive, "bounds": result.bounds, "object_type": result.object_type }