dreader-application/dreader/managers/highlight_coordinator.py
2025-11-12 18:52:08 +00:00

212 lines
7.1 KiB
Python

"""
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