Files
pyWebLayout/pyWebLayout/layout/ereader_manager.py
T
dtourolle a57da8011e fix(ereader): keep resume position when a block spans a page (S11)
render_page_forward discarded new_pos on the failure path, but a block that
only partially fitted has still advanced the position: paragraph_layouter
reports the word it stopped at, and _layout_paragraph_on_page packs it into
new_pos. Dropping it told the caller no progress was made, so navigation
dead-ended on any paragraph larger than a single page - the reader saw "end of
document" mid-book.

A 2877-word paragraph at 800x600 rendered 26 lines and reported the start
position back; it now paginates across 12 pages.

Also guard the navigation loop: EreaderManager.next_page treats no-progress as
end-of-document, which is only correct at the actual end. Anywhere else it now
logs the offending block index and skips that block, so a future layout bug
costs one block rather than the rest of the book.
2026-08-06 21:06:29 +02:00

968 lines
34 KiB
Python

"""
High-performance ereader layout manager with sub-second page rendering.
This module provides the main interface for ereader applications, combining
position tracking, font scaling, chapter navigation, and intelligent page buffering
into a unified, easy-to-use API.
"""
from __future__ import annotations
from typing import List, Dict, Optional, Tuple, Any, Callable
import json
import logging
from pathlib import Path
from .ereader_layout import RenderingPosition, ChapterNavigator, ChapterInfo
from .page_buffer import BufferedPageRenderer
from pyWebLayout.abstract.block import Block, HeadingLevel, Image, BlockType
from pyWebLayout.concrete.page import Page
from pyWebLayout.concrete.image import RenderableImage
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.style.fonts import BundledFont
from pyWebLayout.layout.document_layouter import image_layouter
logger = logging.getLogger(__name__)
class BookmarkManager:
"""
Manages bookmarks and reading position persistence for ereader applications.
"""
def __init__(self, document_id: str, bookmarks_dir: str = "bookmarks"):
"""
Initialize bookmark manager.
Args:
document_id: Unique identifier for the document
bookmarks_dir: Directory to store bookmark files
"""
self.document_id = document_id
self.bookmarks_dir = Path(bookmarks_dir)
self.bookmarks_dir.mkdir(exist_ok=True)
self.bookmarks_file = self.bookmarks_dir / f"{document_id}_bookmarks.json"
self.position_file = self.bookmarks_dir / f"{document_id}_position.json"
self._bookmarks: Dict[str, RenderingPosition] = {}
self._load_bookmarks()
def _load_bookmarks(self):
"""Load bookmarks from file"""
if self.bookmarks_file.exists():
try:
with open(self.bookmarks_file, 'r') as f:
data = json.load(f)
self._bookmarks = {
name: RenderingPosition.from_dict(pos_data)
for name, pos_data in data.items()
}
except Exception as e:
print(f"Failed to load bookmarks: {e}")
self._bookmarks = {}
def _save_bookmarks(self):
"""Save bookmarks to file"""
try:
data = {
name: position.to_dict()
for name, position in self._bookmarks.items()
}
with open(self.bookmarks_file, 'w') as f:
json.dump(data, f, indent=2)
except Exception as e:
print(f"Failed to save bookmarks: {e}")
def add_bookmark(self, name: str, position: RenderingPosition):
"""
Add a bookmark at the given position.
Args:
name: Bookmark name
position: Position to bookmark
"""
self._bookmarks[name] = position
self._save_bookmarks()
def remove_bookmark(self, name: str) -> bool:
"""
Remove a bookmark.
Args:
name: Bookmark name to remove
Returns:
True if bookmark was removed, False if not found
"""
if name in self._bookmarks:
del self._bookmarks[name]
self._save_bookmarks()
return True
return False
def get_bookmark(self, name: str) -> Optional[RenderingPosition]:
"""
Get a bookmark position.
Args:
name: Bookmark name
Returns:
Bookmark position or None if not found
"""
return self._bookmarks.get(name)
def list_bookmarks(self) -> List[Tuple[str, RenderingPosition]]:
"""
Get all bookmarks.
Returns:
List of (name, position) tuples
"""
return list(self._bookmarks.items())
def save_reading_position(self, position: RenderingPosition):
"""
Save the current reading position.
Args:
position: Current reading position
"""
try:
with open(self.position_file, 'w') as f:
json.dump(position.to_dict(), f, indent=2)
except Exception as e:
print(f"Failed to save reading position: {e}")
def load_reading_position(self) -> Optional[RenderingPosition]:
"""
Load the last reading position.
Returns:
Last reading position or None if not found
"""
if self.position_file.exists():
try:
with open(self.position_file, 'r') as f:
data = json.load(f)
return RenderingPosition.from_dict(data)
except Exception as e:
print(f"Failed to load reading position: {e}")
return None
class EreaderLayoutManager:
"""
High-level ereader layout manager providing a complete interface for ereader applications.
Features:
- Sub-second page rendering with intelligent buffering
- Font scaling support
- Dynamic font family switching (Sans, Serif, Monospace)
- Chapter navigation
- Bookmark management
- Position persistence
- Progress tracking
"""
def __init__(self,
blocks: List[Block],
page_size: Tuple[int, int],
document_id: str = "default",
buffer_size: int = 5,
page_style: Optional[PageStyle] = None,
bookmarks_dir: str = "bookmarks"):
"""
Initialize the ereader layout manager.
Args:
blocks: Document blocks to render
page_size: Page size (width, height) in pixels
document_id: Unique identifier for the document (for bookmarks/position)
buffer_size: Number of pages to cache in each direction
page_style: Custom page styling (uses default if None)
bookmarks_dir: Directory to store bookmark files
"""
self.blocks = blocks
self.page_size = page_size
self.document_id = document_id
# Initialize page style
if page_style is None:
page_style = PageStyle()
self.page_style = page_style
# Initialize core components
self.renderer = BufferedPageRenderer(blocks, page_style, buffer_size, page_size)
self.chapter_navigator = ChapterNavigator(blocks)
self.bookmark_manager = BookmarkManager(document_id, bookmarks_dir)
# Current state
self.current_position = RenderingPosition()
self.font_scale = 1.0
# Cover page handling
self._has_cover = self._detect_cover()
self._on_cover_page = self._has_cover # Start on cover if one exists
# Page position history for fast backward navigation
# List of (position, font_scale) tuples representing the start of each page visited
self._page_history: List[Tuple[RenderingPosition, float]] = []
self._max_history_size = 50 # Keep last 50 page positions
# Load last reading position if available
saved_position = self.bookmark_manager.load_reading_position()
if saved_position:
self.current_position = saved_position
self._on_cover_page = False # If we have a saved position, we're past the cover
# Callbacks for UI updates
self.position_changed_callback: Optional[Callable[[
RenderingPosition], None]] = None
self.chapter_changed_callback: Optional[Callable[[
Optional[ChapterInfo]], None]] = None
def prewarm_caches(self, max_words: int = 2000,
budget_bytes: Optional[int] = None) -> Tuple[int, int]:
"""
Preload the text caches with this document's most frequent words.
Counts how often each word occurs in the book and rasterises the most
common ones ahead of time, so that the work lands at open time rather than
on the first page turns. Entries are seeded with their document frequency,
which is what keeps them resident under usage-ranked eviction.
Safe to call again after a font change; the fonts differ, so the new
entries simply take their place in the eviction order alongside the old.
Args:
max_words: Maximum distinct words to preload.
budget_bytes: Bytes of glyph cache to fill. Defaults to half the budget.
Returns:
Tuple of (words preloaded, bytes preloaded).
"""
from collections import Counter
from pyWebLayout.concrete.text import prewarm_text_caches
from .ereader_layout import FontScaler
override = getattr(self.renderer.layouter, 'font_family_override', None)
# Count by (style, text): the same word in a heading and in body text is a
# different rasterisation, and both are worth counting separately.
counts: Dict[Tuple[int, str], int] = Counter()
styles: Dict[int, Any] = {}
for block in self.blocks:
words = getattr(block, '_words', None)
if not words:
continue
for word in words:
style = word.style
if style is None:
continue
key = id(style)
styles.setdefault(key, style)
counts[(key, word.text)] += 1
# Resolve each distinct style once through the same scaling the layouter
# applies, so the preloaded keys match what rendering will look up.
scaled: Dict[int, Any] = {}
for key, style in styles.items():
try:
scaled[key] = FontScaler.scale_font(style, self.font_scale, override)
except Exception:
continue
entries = []
for (style_key, text), count in counts.items():
font = scaled.get(style_key)
if font is None:
continue
entries.append((font.font, text, font.colour, count))
return prewarm_text_caches(entries, budget_bytes=budget_bytes,
max_words=max_words)
def set_position_changed_callback(
self, callback: Callable[[RenderingPosition], None]):
"""Set callback for position changes"""
self.position_changed_callback = callback
def set_chapter_changed_callback(
self, callback: Callable[[Optional[ChapterInfo]], None]):
"""Set callback for chapter changes"""
self.chapter_changed_callback = callback
def _detect_cover(self) -> bool:
"""
Detect if the document has a cover page.
A cover is detected if:
1. The first block is an Image block, OR
2. The document has cover metadata (future enhancement)
Returns:
True if a cover page should be rendered
"""
if not self.blocks:
return False
# Check if first block is an image - treat it as a cover
first_block = self.blocks[0]
if isinstance(first_block, Image):
return True
return False
def _render_cover_page(self) -> Page:
"""
Render a dedicated cover page.
The cover page displays the first image block (if it exists)
using the standard image layouter with maximum dimensions to fill the page.
Returns:
Rendered cover page
"""
# Create a new page for the cover
page = Page(self.page_size, self.page_style)
if not self.blocks or not isinstance(self.blocks[0], Image):
# No cover image, return blank page
return page
cover_image_block = self.blocks[0]
# Use the image layouter to render the cover image
# Use full page dimensions (minus borders/padding) for cover
try:
max_width = self.page_size[0] - 2 * self.page_style.border_width
max_height = self.page_size[1] - 2 * self.page_style.border_width
# Layout the image on the page
success = image_layouter(
image=cover_image_block,
page=page,
max_width=max_width,
max_height=max_height
)
if not success:
print("Warning: Failed to layout cover image")
except Exception as e:
# If image loading fails, just return the blank page
print(f"Warning: Failed to load cover image: {e}")
return page
def _notify_position_changed(self):
"""Notify UI of position change"""
if self.position_changed_callback:
self.position_changed_callback(self.current_position)
# Check if chapter changed
current_chapter = self.chapter_navigator.get_current_chapter(
self.current_position)
if self.chapter_changed_callback:
self.chapter_changed_callback(current_chapter)
# Auto-save reading position
self.bookmark_manager.save_reading_position(self.current_position)
def get_current_page(self) -> Page:
"""
Get the page at the current reading position.
If on the cover page, returns the rendered cover.
Otherwise, returns the regular content page.
Returns:
Rendered page
"""
# Check if we're on the cover page
if self._on_cover_page and self._has_cover:
return self._render_cover_page()
page, _ = self.renderer.render_page(self.current_position, self.font_scale)
return page
def next_page(self) -> Optional[Page]:
"""
Advance to the next page.
If currently on the cover page, advances to the first content page.
Otherwise, advances to the next content page.
Returns:
Next page or None if at end of document
"""
# Special case: transitioning from cover to first content page
if self._on_cover_page and self._has_cover:
self._on_cover_page = False
# If first block is an image (the cover), skip it and start from block 1
if self.blocks and isinstance(self.blocks[0], Image):
self.current_position = RenderingPosition(chapter_index=0, block_index=1)
else:
self.current_position = RenderingPosition()
self._notify_position_changed()
return self.get_current_page()
# Save current position to history before moving forward
self._add_to_history(self.current_position, self.font_scale)
page, next_position = self.renderer.render_page(
self.current_position, self.font_scale)
# Check if we made progress
if next_position != self.current_position:
self.current_position = next_position
self._notify_position_changed()
return self.get_current_page()
# No progress. That is the correct answer only at the end of the
# document; anywhere else a block has failed to lay out and would trap
# the reader on this page. Skipping the block costs one block, not the
# rest of the book.
if self.current_position.block_index < len(self.blocks):
logger.error(
"Block %d made no layout progress; skipping it. This is a layout "
"bug - the block placed nothing and reported no resume point.",
self.current_position.block_index)
self.current_position = RenderingPosition(
chapter_index=self.current_position.chapter_index,
block_index=self.current_position.block_index + 1)
self._notify_position_changed()
return self.get_current_page()
return None # At end of document
def previous_page(self) -> Optional[Page]:
"""
Go to the previous page.
Uses cached page history for instant navigation when available,
falls back to iterative refinement algorithm when needed.
Can navigate back to the cover page if it exists.
Returns:
Previous page or None if at beginning of document (or on cover)
"""
# Special case: if at the beginning of content and there's a cover, go back to it
if self._has_cover and self._is_at_beginning() and not self._on_cover_page:
self._on_cover_page = True
self._notify_position_changed()
return self.get_current_page()
# Can't go before the cover
if self._on_cover_page:
return None
if self._is_at_beginning():
return None
# Fast path: Check if we have this position in history
previous_position = self._get_from_history(self.current_position, self.font_scale)
if previous_position is not None:
# Cache hit! Use the cached position for instant navigation
self.current_position = previous_position
self._notify_position_changed()
return self.get_current_page()
# Slow path: Use backward rendering to find the previous page
# This uses the iterative refinement algorithm we just fixed
page, start_position = self.renderer.render_page_backward(
self.current_position, self.font_scale)
if start_position != self.current_position:
# Save this calculated position to history for future use
self._add_to_history(start_position, self.font_scale)
self.current_position = start_position
self._notify_position_changed()
return page
return None # At beginning of document
def _is_at_beginning(self) -> bool:
"""
Check if we're at the beginning of the document content.
If a cover exists (first block is an Image), the beginning of content
is at block_index=1. Otherwise, it's at block_index=0.
"""
# Determine the first content block index
first_content_block = 1 if (self._has_cover and self.blocks and isinstance(self.blocks[0], Image)) else 0
return (self.current_position.chapter_index == 0 and
self.current_position.block_index == first_content_block and
self.current_position.word_index == 0)
def jump_to_position(self, position: RenderingPosition) -> Page:
"""
Jump to a specific position in the document.
Args:
position: Position to jump to
Returns:
Page at the new position
"""
self.current_position = position
self._on_cover_page = False # Jumping to a position means we're past the cover
self._notify_position_changed()
return self.get_current_page()
def jump_to_chapter(self, chapter_title: str) -> Optional[Page]:
"""
Jump to a specific chapter by title.
Args:
chapter_title: Title of the chapter to jump to
Returns:
Page at chapter start or None if chapter not found
"""
position = self.chapter_navigator.get_chapter_position(chapter_title)
if position:
return self.jump_to_position(position)
return None
def jump_to_chapter_index(self, chapter_index: int) -> Optional[Page]:
"""
Jump to a chapter by index.
Args:
chapter_index: Index of the chapter (0-based)
Returns:
Page at chapter start or None if index invalid
"""
chapters = self.chapter_navigator.chapters
if 0 <= chapter_index < len(chapters):
return self.jump_to_position(chapters[chapter_index].position)
return None
def _add_to_history(self, position: RenderingPosition, font_scale: float):
"""
Add a page position to the navigation history.
Args:
position: The page start position to remember
font_scale: The font scale at this position
"""
# Only add if it's different from the last entry
if not self._page_history or \
self._page_history[-1][0] != position or \
self._page_history[-1][1] != font_scale:
self._page_history.append((position.copy(), font_scale))
# Trim history if it exceeds max size
if len(self._page_history) > self._max_history_size:
self._page_history.pop(0)
def _get_from_history(
self,
current_position: RenderingPosition,
current_font_scale: float) -> Optional[RenderingPosition]:
"""
Get the previous page position from history.
Searches backward through history to find the last position that
comes before the current position at the same font scale.
Args:
current_position: Current page position
current_font_scale: Current font scale
Returns:
Previous page position or None if not found in history
"""
# Search backward through history
for i in range(len(self._page_history) - 1, -1, -1):
hist_position, hist_font_scale = self._page_history[i]
# Must match font scale
if hist_font_scale != current_font_scale:
continue
# Must be before current position
if (hist_position.chapter_index < current_position.chapter_index or
(hist_position.chapter_index == current_position.chapter_index and
hist_position.block_index < current_position.block_index) or
(hist_position.chapter_index == current_position.chapter_index and
hist_position.block_index == current_position.block_index and
hist_position.word_index < current_position.word_index)):
# Found a previous position - remove it and everything after from history
# since we're navigating backward
self._page_history = self._page_history[:i]
return hist_position.copy()
return None
def _clear_history(self):
"""Clear the page navigation history."""
self._page_history.clear()
def set_font_scale(self, scale: float) -> Page:
"""
Change the font scale and re-render current page.
Clears page history since font changes invalidate all cached positions.
Args:
scale: Font scaling factor (1.0 = normal, 2.0 = double size, etc.)
Returns:
Re-rendered page with new font scale
"""
if scale != self.font_scale:
self.font_scale = scale
# Clear history since font scale changes invalidate all cached positions
self._clear_history()
# The renderer will handle cache invalidation
return self.get_current_page()
def get_font_scale(self) -> float:
"""Get the current font scale"""
return self.font_scale
def set_font_family(self, family: Optional[BundledFont]) -> Page:
"""
Change the font family and re-render current page.
Switches all text in the document to use the specified bundled font family
while preserving font weights, styles, sizes, and other attributes.
Clears page history and cache since font changes invalidate all cached positions.
Args:
family: Bundled font family to use (SANS, SERIF, MONOSPACE, or None for original fonts)
Returns:
Re-rendered page with new font family
Example:
>>> from pyWebLayout.style.fonts import BundledFont
>>> manager.set_font_family(BundledFont.SERIF) # Switch to serif
>>> manager.set_font_family(BundledFont.SANS) # Switch to sans
>>> manager.set_font_family(None) # Restore original fonts
"""
# Update the renderer's font family
self.renderer.set_font_family(family)
# Clear history since font changes invalidate all cached positions
self._clear_history()
return self.get_current_page()
def get_font_family(self) -> Optional[BundledFont]:
"""
Get the current font family override.
Returns:
Current font family (SANS, SERIF, MONOSPACE) or None if using original fonts
"""
return self.renderer.get_font_family()
def increase_line_spacing(self, amount: int = 2) -> Page:
"""
Increase line spacing and re-render current page.
Clears page history since spacing changes invalidate all cached positions.
Args:
amount: Pixels to add to line spacing (default: 2)
Returns:
Re-rendered page with increased line spacing
"""
self.page_style.line_spacing += amount
self.renderer.page_style = self.page_style # Update renderer's reference
self.renderer.buffer.invalidate_all() # Clear cache to force re-render
self._clear_history() # Clear position history
return self.get_current_page()
def decrease_line_spacing(self, amount: int = 2) -> Page:
"""
Decrease line spacing and re-render current page.
Clears page history since spacing changes invalidate all cached positions.
Args:
amount: Pixels to remove from line spacing (default: 2)
Returns:
Re-rendered page with decreased line spacing
"""
self.page_style.line_spacing = max(0, self.page_style.line_spacing - amount)
self.renderer.page_style = self.page_style # Update renderer's reference
self.renderer.buffer.invalidate_all() # Clear cache to force re-render
self._clear_history() # Clear position history
return self.get_current_page()
def increase_inter_block_spacing(self, amount: int = 5) -> Page:
"""
Increase spacing between blocks and re-render current page.
Clears page history since spacing changes invalidate all cached positions.
Args:
amount: Pixels to add to inter-block spacing (default: 5)
Returns:
Re-rendered page with increased block spacing
"""
self.page_style.inter_block_spacing += amount
self.renderer.page_style = self.page_style # Update renderer's reference
self.renderer.buffer.invalidate_all() # Clear cache to force re-render
self._clear_history() # Clear position history
return self.get_current_page()
def decrease_inter_block_spacing(self, amount: int = 5) -> Page:
"""
Decrease spacing between blocks and re-render current page.
Clears page history since spacing changes invalidate all cached positions.
Args:
amount: Pixels to remove from inter-block spacing (default: 5)
Returns:
Re-rendered page with decreased block spacing
"""
self.page_style.inter_block_spacing = max(
0, self.page_style.inter_block_spacing - amount)
self.renderer.page_style = self.page_style # Update renderer's reference
self.renderer.buffer.invalidate_all() # Clear cache to force re-render
self._clear_history() # Clear position history
return self.get_current_page()
def increase_word_spacing(self, amount: int = 2) -> Page:
"""
Increase spacing between words and re-render current page.
Clears page history since spacing changes invalidate all cached positions.
Args:
amount: Pixels to add to word spacing (default: 2)
Returns:
Re-rendered page with increased word spacing
"""
self.page_style.word_spacing += amount
self.renderer.page_style = self.page_style # Update renderer's reference
self.renderer.buffer.invalidate_all() # Clear cache to force re-render
self._clear_history() # Clear position history
return self.get_current_page()
def decrease_word_spacing(self, amount: int = 2) -> Page:
"""
Decrease spacing between words and re-render current page.
Clears page history since spacing changes invalidate all cached positions.
Args:
amount: Pixels to remove from word spacing (default: 2)
Returns:
Re-rendered page with decreased word spacing
"""
self.page_style.word_spacing = max(0, self.page_style.word_spacing - amount)
self.renderer.page_style = self.page_style # Update renderer's reference
self.renderer.buffer.invalidate_all() # Clear cache to force re-render
self._clear_history() # Clear position history
return self.get_current_page()
def get_table_of_contents(
self) -> List[Tuple[str, HeadingLevel, RenderingPosition]]:
"""
Get the table of contents.
Returns:
List of (title, level, position) tuples
"""
return self.chapter_navigator.get_table_of_contents()
def get_current_chapter(self) -> Optional[ChapterInfo]:
"""
Get information about the current chapter.
Returns:
Current chapter info or None if no chapters
"""
return self.chapter_navigator.get_current_chapter(self.current_position)
def add_bookmark(self, name: str) -> bool:
"""
Add a bookmark at the current position.
Args:
name: Bookmark name
Returns:
True if bookmark was added successfully
"""
try:
self.bookmark_manager.add_bookmark(name, self.current_position)
return True
except Exception:
return False
def remove_bookmark(self, name: str) -> bool:
"""
Remove a bookmark.
Args:
name: Bookmark name
Returns:
True if bookmark was removed
"""
return self.bookmark_manager.remove_bookmark(name)
def jump_to_bookmark(self, name: str) -> Optional[Page]:
"""
Jump to a bookmark.
Args:
name: Bookmark name
Returns:
Page at bookmark position or None if bookmark not found
"""
position = self.bookmark_manager.get_bookmark(name)
if position:
return self.jump_to_position(position)
return None
def list_bookmarks(self) -> List[Tuple[str, RenderingPosition]]:
"""
Get all bookmarks.
Returns:
List of (name, position) tuples
"""
return self.bookmark_manager.list_bookmarks()
def get_reading_progress(self) -> float:
"""
Get reading progress as a percentage.
Returns:
Progress from 0.0 to 1.0
"""
if not self.blocks:
return 0.0
# Simple progress calculation based on block index
# A more sophisticated version would consider word positions
total_blocks = len(self.blocks)
current_block = min(self.current_position.block_index, total_blocks - 1)
return current_block / max(1, total_blocks - 1)
def has_cover(self) -> bool:
"""
Check if the document has a cover page.
Returns:
True if a cover page is available
"""
return self._has_cover
def is_on_cover(self) -> bool:
"""
Check if currently viewing the cover page.
Returns:
True if on the cover page
"""
return self._on_cover_page
def jump_to_cover(self) -> Optional[Page]:
"""
Jump to the cover page if one exists.
Returns:
Cover page or None if no cover exists
"""
if not self._has_cover:
return None
self._on_cover_page = True
self._notify_position_changed()
return self.get_current_page()
def get_position_info(self) -> Dict[str, Any]:
"""
Get detailed information about the current position.
Returns:
Dictionary with position details
"""
current_chapter = self.get_current_chapter()
font_family = self.get_font_family()
return {
'position': self.current_position.to_dict(),
'on_cover': self._on_cover_page,
'has_cover': self._has_cover,
'chapter': {
'title': current_chapter.title if current_chapter else None,
'level': current_chapter.level if current_chapter else None,
'index': current_chapter.block_index if current_chapter else None
},
'progress': self.get_reading_progress(),
'font_scale': self.font_scale,
'font_family': font_family.value if font_family else None,
'page_size': self.page_size
}
def get_cache_stats(self) -> Dict[str, Any]:
"""
Get cache statistics for debugging/monitoring.
Returns:
Dictionary with cache statistics
"""
return self.renderer.get_cache_stats()
def shutdown(self):
"""
Shutdown the ereader manager and clean up resources.
Call this when the application is closing.
"""
# Save current position
self.bookmark_manager.save_reading_position(self.current_position)
# Shutdown renderer and buffer
self.renderer.shutdown()
def __del__(self):
"""Cleanup on destruction"""
self.shutdown()
# Convenience function for quick setup
def create_ereader_manager(blocks: List[Block],
page_size: Tuple[int, int],
document_id: str = "default",
**kwargs) -> EreaderLayoutManager:
"""
Convenience function to create an ereader manager with sensible defaults.
Args:
blocks: Document blocks to render
page_size: Page size (width, height) in pixels
document_id: Unique identifier for the document
**kwargs: Additional arguments passed to EreaderLayoutManager
Returns:
Configured EreaderLayoutManager instance
"""
return EreaderLayoutManager(blocks, page_size, document_id, **kwargs)