""" Highlight operations coordination. This module coordinates highlight operations with the highlight manager. """ from __future__ import annotations from typing import List, Tuple, Optional, TYPE_CHECKING from PIL import Image import numpy as np from pyWebLayout.core.highlight import Highlight, HighlightManager, HighlightColor, create_highlight_from_query_result if TYPE_CHECKING: from pyWebLayout.layout.ereader_manager import EreaderLayoutManager class HighlightCoordinator: """ Coordinates highlight operations. This class provides a simplified interface for highlighting operations, coordinating between the layout manager and highlight manager. """ def __init__(self, document_id: str, highlights_dir: str): """ Initialize the highlight coordinator. Args: document_id: Unique document identifier highlights_dir: Directory to store highlights """ self.highlight_manager = HighlightManager( document_id=document_id, highlights_dir=highlights_dir ) self.layout_manager: Optional['EreaderLayoutManager'] = None def set_layout_manager(self, manager: 'EreaderLayoutManager'): """Set the layout manager.""" self.layout_manager = manager def highlight_word(self, x: int, y: int, color: Tuple[int, int, int, int] = None, note: Optional[str] = None, tags: Optional[List[str]] = None) -> Optional[str]: """ Highlight a word at the given pixel location. Args: x: X coordinate y: Y coordinate color: RGBA color tuple (defaults to yellow) note: Optional annotation for this highlight tags: Optional categorization tags Returns: Highlight ID if successful, None otherwise """ if not self.layout_manager: return None try: # Query the pixel to find the word page = self.layout_manager.get_current_page() result = page.query_point((x, y)) if not result or not result.text: return None # Use default color if not provided if color is None: color = HighlightColor.YELLOW.value # Create highlight from query result highlight = create_highlight_from_query_result( result, color=color, note=note, tags=tags ) # Add to manager self.highlight_manager.add_highlight(highlight) return highlight.id except Exception as e: print(f"Error highlighting word: {e}") return None def highlight_selection(self, start: Tuple[int, int], end: Tuple[int, int], color: Tuple[int, int, int, int] = None, note: Optional[str] = None, tags: Optional[List[str]] = None) -> Optional[str]: """ Highlight a range of words between two points. Args: start: Starting (x, y) coordinates end: Ending (x, y) coordinates color: RGBA color tuple (defaults to yellow) note: Optional annotation tags: Optional categorization tags Returns: Highlight ID if successful, None otherwise """ if not self.layout_manager: return None try: page = self.layout_manager.get_current_page() selection_range = page.query_range(start, end) if not selection_range.results: return None # Use default color if not provided if color is None: color = HighlightColor.YELLOW.value # Create highlight from selection range highlight = create_highlight_from_query_result( selection_range, color=color, note=note, tags=tags ) # Add to manager self.highlight_manager.add_highlight(highlight) return highlight.id except Exception as e: print(f"Error highlighting selection: {e}") return None def remove_highlight(self, highlight_id: str) -> bool: """Remove a highlight by ID.""" return self.highlight_manager.remove_highlight(highlight_id) def list_highlights(self) -> List[Highlight]: """Get all highlights for the current document.""" return self.highlight_manager.list_highlights() def get_highlights_for_page(self, page_bounds: Tuple[int, int, int, int]) -> List[Highlight]: """Get highlights that appear on a specific page.""" return self.highlight_manager.get_highlights_for_page(page_bounds) def clear_all(self) -> None: """Remove all highlights from the current document.""" self.highlight_manager.clear_all() def render_highlights(self, image: Image.Image, highlights: List[Highlight]) -> Image.Image: """ Render highlight overlays on an image using multiply blend mode. Args: image: Base PIL Image to draw on highlights: List of Highlight objects to render Returns: New PIL Image with highlights overlaid """ # Convert to RGB for processing original_mode = image.mode if image.mode == 'RGBA': rgb_image = image.convert('RGB') alpha_channel = image.split()[-1] else: rgb_image = image.convert('RGB') alpha_channel = None # Convert to numpy array for efficient processing img_array = np.array(rgb_image, dtype=np.float32) # Process each highlight for highlight in highlights: # Extract RGB components from highlight color (ignore alpha) h_r, h_g, h_b = highlight.color[0], highlight.color[1], highlight.color[2] # Create highlight multiplier (normalize to 0-1 range) highlight_color = np.array([h_r / 255.0, h_g / 255.0, h_b / 255.0], dtype=np.float32) for hx, hy, hw, hh in highlight.bounds: # Ensure bounds are within image hx, hy = max(0, hx), max(0, hy) x2, y2 = min(rgb_image.width, hx + hw), min(rgb_image.height, hy + hh) if x2 <= hx or y2 <= hy: continue # Extract the region to highlight region = img_array[hy:y2, hx:x2, :] # Multiply with highlight color (like a real highlighter) highlighted = region * highlight_color # Put the highlighted region back img_array[hy:y2, hx:x2, :] = highlighted # Convert back to uint8 and create PIL Image img_array = np.clip(img_array, 0, 255).astype(np.uint8) result = Image.fromarray(img_array, mode='RGB') # Restore alpha channel if original had one if alpha_channel is not None and original_mode == 'RGBA': result = result.convert('RGBA') result.putalpha(alpha_channel) return result