This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
Managers module for dreader application.
|
||||
|
||||
This module contains business logic managers that handle specific responsibilities:
|
||||
- DocumentManager: Document loading and metadata
|
||||
- SettingsManager: Font size, spacing, and rendering settings
|
||||
- HighlightCoordinator: Highlight operations coordination
|
||||
"""
|
||||
|
||||
from .document import DocumentManager
|
||||
from .settings import SettingsManager
|
||||
from .highlight_coordinator import HighlightCoordinator
|
||||
|
||||
__all__ = ['DocumentManager', 'SettingsManager', 'HighlightCoordinator']
|
||||
@@ -0,0 +1,137 @@
|
||||
"""
|
||||
Document loading and metadata management.
|
||||
|
||||
This module handles EPUB and HTML loading, extracting blocks and metadata.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import List, Tuple, Dict, Any, Optional
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
from pyWebLayout.io.readers.epub_reader import read_epub
|
||||
from pyWebLayout.io.readers.html_extraction import parse_html_string
|
||||
from pyWebLayout.abstract.block import Block
|
||||
|
||||
|
||||
class DocumentManager:
|
||||
"""
|
||||
Handles document loading and metadata extraction.
|
||||
|
||||
Responsibilities:
|
||||
- Load EPUB files
|
||||
- Load HTML content
|
||||
- Extract document metadata (title, author, etc.)
|
||||
- Extract content blocks for rendering
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the document manager."""
|
||||
self.document_id: Optional[str] = None
|
||||
self.title: Optional[str] = None
|
||||
self.author: Optional[str] = None
|
||||
self.blocks: Optional[List[Block]] = None
|
||||
|
||||
def load_epub(self, epub_path: str) -> bool:
|
||||
"""
|
||||
Load an EPUB file and extract content.
|
||||
|
||||
Args:
|
||||
epub_path: Path to the EPUB file
|
||||
|
||||
Returns:
|
||||
True if loaded successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Validate path
|
||||
if not os.path.exists(epub_path):
|
||||
raise FileNotFoundError(f"EPUB file not found: {epub_path}")
|
||||
|
||||
# Load the EPUB
|
||||
book = read_epub(epub_path)
|
||||
|
||||
# Extract metadata
|
||||
self.title = book.get_title() or "Unknown Title"
|
||||
self.author = book.get_metadata('AUTHOR') or "Unknown Author"
|
||||
|
||||
# Create document ID from filename
|
||||
self.document_id = Path(epub_path).stem
|
||||
|
||||
# Extract all blocks from chapters
|
||||
self.blocks = []
|
||||
for chapter in book.chapters:
|
||||
if hasattr(chapter, '_blocks'):
|
||||
self.blocks.extend(chapter._blocks)
|
||||
|
||||
if not self.blocks:
|
||||
raise ValueError("No content blocks found in EPUB")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading EPUB: {e}")
|
||||
return False
|
||||
|
||||
def load_html(self, html_string: str, title: str = "HTML Document",
|
||||
author: str = "Unknown", document_id: str = "html_doc") -> bool:
|
||||
"""
|
||||
Load HTML content directly.
|
||||
|
||||
This is useful for rendering library screens, menus, or other HTML-based UI elements.
|
||||
|
||||
Args:
|
||||
html_string: HTML content to render
|
||||
title: Document title (for metadata)
|
||||
author: Document author (for metadata)
|
||||
document_id: Unique identifier for this HTML document
|
||||
|
||||
Returns:
|
||||
True if loaded successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Parse HTML into blocks
|
||||
blocks = parse_html_string(html_string)
|
||||
|
||||
if not blocks:
|
||||
raise ValueError("No content blocks parsed from HTML")
|
||||
|
||||
# Set metadata
|
||||
self.title = title
|
||||
self.author = author
|
||||
self.document_id = document_id
|
||||
self.blocks = blocks
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading HTML: {e}")
|
||||
return False
|
||||
|
||||
def is_loaded(self) -> bool:
|
||||
"""Check if a document is currently loaded."""
|
||||
return self.blocks is not None and len(self.blocks) > 0
|
||||
|
||||
def get_metadata(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get document metadata.
|
||||
|
||||
Returns:
|
||||
Dictionary with metadata (title, author, document_id, total_blocks)
|
||||
"""
|
||||
return {
|
||||
'title': self.title,
|
||||
'author': self.author,
|
||||
'document_id': self.document_id,
|
||||
'total_blocks': len(self.blocks) if self.blocks else 0
|
||||
}
|
||||
|
||||
def get_blocks(self) -> Optional[List[Block]]:
|
||||
"""Get the list of content blocks."""
|
||||
return self.blocks
|
||||
|
||||
def clear(self):
|
||||
"""Clear the currently loaded document."""
|
||||
self.document_id = None
|
||||
self.title = None
|
||||
self.author = None
|
||||
self.blocks = None
|
||||
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
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
|
||||
@@ -0,0 +1,250 @@
|
||||
"""
|
||||
Settings and rendering configuration management.
|
||||
|
||||
This module handles font size, spacing, and other rendering settings.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Dict, Any, Optional
|
||||
from PIL import Image
|
||||
|
||||
from pyWebLayout.layout.ereader_manager import EreaderLayoutManager
|
||||
|
||||
|
||||
class SettingsManager:
|
||||
"""
|
||||
Manages font size, spacing, and rendering settings.
|
||||
|
||||
Responsibilities:
|
||||
- Font scale adjustment
|
||||
- Line spacing control
|
||||
- Inter-block spacing control
|
||||
- Word spacing control
|
||||
- Settings persistence helpers
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the settings manager."""
|
||||
self.font_scale = 1.0
|
||||
self.font_scale_step = 0.1 # 10% change per step
|
||||
self.manager: Optional[EreaderLayoutManager] = None
|
||||
|
||||
def set_manager(self, manager: EreaderLayoutManager):
|
||||
"""
|
||||
Set the layout manager to control.
|
||||
|
||||
Args:
|
||||
manager: EreaderLayoutManager instance to manage settings for
|
||||
"""
|
||||
self.manager = manager
|
||||
self.font_scale = manager.font_scale
|
||||
|
||||
def set_font_size(self, scale: float) -> Optional[Image.Image]:
|
||||
"""
|
||||
Set the font size scale and re-render current page.
|
||||
|
||||
Args:
|
||||
scale: Font scale factor (1.0 = normal, 2.0 = double size, 0.5 = half size)
|
||||
|
||||
Returns:
|
||||
Rendered page with new font size, or None if no manager
|
||||
"""
|
||||
if not self.manager:
|
||||
return None
|
||||
|
||||
try:
|
||||
self.font_scale = max(0.5, min(3.0, scale)) # Clamp between 0.5x and 3.0x
|
||||
page = self.manager.set_font_scale(self.font_scale)
|
||||
return page.render() if page else None
|
||||
except Exception as e:
|
||||
print(f"Error setting font size: {e}")
|
||||
return None
|
||||
|
||||
def increase_font_size(self) -> Optional[Image.Image]:
|
||||
"""
|
||||
Increase font size by one step and re-render.
|
||||
|
||||
Returns:
|
||||
Rendered page with increased font size
|
||||
"""
|
||||
new_scale = self.font_scale + self.font_scale_step
|
||||
return self.set_font_size(new_scale)
|
||||
|
||||
def decrease_font_size(self) -> Optional[Image.Image]:
|
||||
"""
|
||||
Decrease font size by one step and re-render.
|
||||
|
||||
Returns:
|
||||
Rendered page with decreased font size
|
||||
"""
|
||||
new_scale = self.font_scale - self.font_scale_step
|
||||
return self.set_font_size(new_scale)
|
||||
|
||||
def get_font_size(self) -> float:
|
||||
"""
|
||||
Get the current font size scale.
|
||||
|
||||
Returns:
|
||||
Current font scale factor
|
||||
"""
|
||||
return self.font_scale
|
||||
|
||||
def set_line_spacing(self, spacing: int) -> Optional[Image.Image]:
|
||||
"""
|
||||
Set line spacing using pyWebLayout's native support.
|
||||
|
||||
Args:
|
||||
spacing: Line spacing in pixels
|
||||
|
||||
Returns:
|
||||
Rendered page with new line spacing
|
||||
"""
|
||||
if not self.manager:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Calculate delta from current spacing
|
||||
current_spacing = self.manager.page_style.line_spacing
|
||||
target_spacing = max(0, spacing)
|
||||
delta = target_spacing - current_spacing
|
||||
|
||||
# Use pyWebLayout's built-in methods to adjust spacing
|
||||
if delta > 0:
|
||||
self.manager.increase_line_spacing(abs(delta))
|
||||
elif delta < 0:
|
||||
self.manager.decrease_line_spacing(abs(delta))
|
||||
|
||||
# Get re-rendered page
|
||||
page = self.manager.get_current_page()
|
||||
return page.render() if page else None
|
||||
except Exception as e:
|
||||
print(f"Error setting line spacing: {e}")
|
||||
return None
|
||||
|
||||
def set_inter_block_spacing(self, spacing: int) -> Optional[Image.Image]:
|
||||
"""
|
||||
Set inter-block spacing using pyWebLayout's native support.
|
||||
|
||||
Args:
|
||||
spacing: Inter-block spacing in pixels
|
||||
|
||||
Returns:
|
||||
Rendered page with new inter-block spacing
|
||||
"""
|
||||
if not self.manager:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Calculate delta from current spacing
|
||||
current_spacing = self.manager.page_style.inter_block_spacing
|
||||
target_spacing = max(0, spacing)
|
||||
delta = target_spacing - current_spacing
|
||||
|
||||
# Use pyWebLayout's built-in methods to adjust spacing
|
||||
if delta > 0:
|
||||
self.manager.increase_inter_block_spacing(abs(delta))
|
||||
elif delta < 0:
|
||||
self.manager.decrease_inter_block_spacing(abs(delta))
|
||||
|
||||
# Get re-rendered page
|
||||
page = self.manager.get_current_page()
|
||||
return page.render() if page else None
|
||||
except Exception as e:
|
||||
print(f"Error setting inter-block spacing: {e}")
|
||||
return None
|
||||
|
||||
def set_word_spacing(self, spacing: int) -> Optional[Image.Image]:
|
||||
"""
|
||||
Set word spacing using pyWebLayout's native support.
|
||||
|
||||
Args:
|
||||
spacing: Word spacing in pixels
|
||||
|
||||
Returns:
|
||||
Rendered page with new word spacing
|
||||
"""
|
||||
if not self.manager:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Calculate delta from current spacing
|
||||
current_spacing = self.manager.page_style.word_spacing
|
||||
target_spacing = max(0, spacing)
|
||||
delta = target_spacing - current_spacing
|
||||
|
||||
# Use pyWebLayout's built-in methods to adjust spacing
|
||||
if delta > 0:
|
||||
self.manager.increase_word_spacing(abs(delta))
|
||||
elif delta < 0:
|
||||
self.manager.decrease_word_spacing(abs(delta))
|
||||
|
||||
# Get re-rendered page
|
||||
page = self.manager.get_current_page()
|
||||
return page.render() if page else None
|
||||
except Exception as e:
|
||||
print(f"Error setting word spacing: {e}")
|
||||
return None
|
||||
|
||||
def get_current_settings(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get current rendering settings.
|
||||
|
||||
Returns:
|
||||
Dictionary with all current settings
|
||||
"""
|
||||
if not self.manager:
|
||||
return {
|
||||
'font_scale': self.font_scale,
|
||||
'line_spacing': 5,
|
||||
'inter_block_spacing': 15,
|
||||
'word_spacing': 0
|
||||
}
|
||||
|
||||
return {
|
||||
'font_scale': self.font_scale,
|
||||
'line_spacing': self.manager.page_style.line_spacing,
|
||||
'inter_block_spacing': self.manager.page_style.inter_block_spacing,
|
||||
'word_spacing': self.manager.page_style.word_spacing
|
||||
}
|
||||
|
||||
def apply_settings(self, settings: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Apply rendering settings from a settings dictionary.
|
||||
|
||||
This should be called after loading a book to restore user preferences.
|
||||
|
||||
Args:
|
||||
settings: Dictionary with settings (font_scale, line_spacing, etc.)
|
||||
|
||||
Returns:
|
||||
True if settings applied successfully, False otherwise
|
||||
"""
|
||||
if not self.manager:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Apply font scale
|
||||
font_scale = settings.get('font_scale', 1.0)
|
||||
if font_scale != self.font_scale:
|
||||
self.set_font_size(font_scale)
|
||||
|
||||
# Apply line spacing
|
||||
line_spacing = settings.get('line_spacing', 5)
|
||||
if line_spacing != self.manager.page_style.line_spacing:
|
||||
self.set_line_spacing(line_spacing)
|
||||
|
||||
# Apply inter-block spacing
|
||||
inter_block_spacing = settings.get('inter_block_spacing', 15)
|
||||
if inter_block_spacing != self.manager.page_style.inter_block_spacing:
|
||||
self.set_inter_block_spacing(inter_block_spacing)
|
||||
|
||||
# Apply word spacing
|
||||
word_spacing = settings.get('word_spacing', 0)
|
||||
if word_spacing != self.manager.page_style.word_spacing:
|
||||
self.set_word_spacing(word_spacing)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error applying settings: {e}")
|
||||
return False
|
||||
Reference in New Issue
Block a user