212 lines
7.1 KiB
Python
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
|