Coverage for pyWebLayout/layout/ereader_manager.py: 84%
292 statements
« prev ^ index » next coverage.py v7.11.2, created at 2025-11-12 12:02 +0000
« prev ^ index » next coverage.py v7.11.2, created at 2025-11-12 12:02 +0000
1"""
2High-performance ereader layout manager with sub-second page rendering.
4This module provides the main interface for ereader applications, combining
5position tracking, font scaling, chapter navigation, and intelligent page buffering
6into a unified, easy-to-use API.
7"""
9from __future__ import annotations
10from typing import List, Dict, Optional, Tuple, Any, Callable
11import json
12from pathlib import Path
14from .ereader_layout import RenderingPosition, ChapterNavigator, ChapterInfo
15from .page_buffer import BufferedPageRenderer
16from pyWebLayout.abstract.block import Block, HeadingLevel, Image, BlockType
17from pyWebLayout.concrete.page import Page
18from pyWebLayout.concrete.image import RenderableImage
19from pyWebLayout.style.page_style import PageStyle
20from pyWebLayout.style.fonts import BundledFont
21from pyWebLayout.layout.document_layouter import image_layouter
24class BookmarkManager:
25 """
26 Manages bookmarks and reading position persistence for ereader applications.
27 """
29 def __init__(self, document_id: str, bookmarks_dir: str = "bookmarks"):
30 """
31 Initialize bookmark manager.
33 Args:
34 document_id: Unique identifier for the document
35 bookmarks_dir: Directory to store bookmark files
36 """
37 self.document_id = document_id
38 self.bookmarks_dir = Path(bookmarks_dir)
39 self.bookmarks_dir.mkdir(exist_ok=True)
41 self.bookmarks_file = self.bookmarks_dir / f"{document_id}_bookmarks.json"
42 self.position_file = self.bookmarks_dir / f"{document_id}_position.json"
44 self._bookmarks: Dict[str, RenderingPosition] = {}
45 self._load_bookmarks()
47 def _load_bookmarks(self):
48 """Load bookmarks from file"""
49 if self.bookmarks_file.exists():
50 try:
51 with open(self.bookmarks_file, 'r') as f:
52 data = json.load(f)
53 self._bookmarks = {
54 name: RenderingPosition.from_dict(pos_data)
55 for name, pos_data in data.items()
56 }
57 except Exception as e:
58 print(f"Failed to load bookmarks: {e}")
59 self._bookmarks = {}
61 def _save_bookmarks(self):
62 """Save bookmarks to file"""
63 try:
64 data = {
65 name: position.to_dict()
66 for name, position in self._bookmarks.items()
67 }
68 with open(self.bookmarks_file, 'w') as f:
69 json.dump(data, f, indent=2)
70 except Exception as e:
71 print(f"Failed to save bookmarks: {e}")
73 def add_bookmark(self, name: str, position: RenderingPosition):
74 """
75 Add a bookmark at the given position.
77 Args:
78 name: Bookmark name
79 position: Position to bookmark
80 """
81 self._bookmarks[name] = position
82 self._save_bookmarks()
84 def remove_bookmark(self, name: str) -> bool:
85 """
86 Remove a bookmark.
88 Args:
89 name: Bookmark name to remove
91 Returns:
92 True if bookmark was removed, False if not found
93 """
94 if name in self._bookmarks:
95 del self._bookmarks[name]
96 self._save_bookmarks()
97 return True
98 return False
100 def get_bookmark(self, name: str) -> Optional[RenderingPosition]:
101 """
102 Get a bookmark position.
104 Args:
105 name: Bookmark name
107 Returns:
108 Bookmark position or None if not found
109 """
110 return self._bookmarks.get(name)
112 def list_bookmarks(self) -> List[Tuple[str, RenderingPosition]]:
113 """
114 Get all bookmarks.
116 Returns:
117 List of (name, position) tuples
118 """
119 return list(self._bookmarks.items())
121 def save_reading_position(self, position: RenderingPosition):
122 """
123 Save the current reading position.
125 Args:
126 position: Current reading position
127 """
128 try:
129 with open(self.position_file, 'w') as f:
130 json.dump(position.to_dict(), f, indent=2)
131 except Exception as e:
132 print(f"Failed to save reading position: {e}")
134 def load_reading_position(self) -> Optional[RenderingPosition]:
135 """
136 Load the last reading position.
138 Returns:
139 Last reading position or None if not found
140 """
141 if self.position_file.exists():
142 try:
143 with open(self.position_file, 'r') as f:
144 data = json.load(f)
145 return RenderingPosition.from_dict(data)
146 except Exception as e:
147 print(f"Failed to load reading position: {e}")
148 return None
151class EreaderLayoutManager:
152 """
153 High-level ereader layout manager providing a complete interface for ereader applications.
155 Features:
156 - Sub-second page rendering with intelligent buffering
157 - Font scaling support
158 - Dynamic font family switching (Sans, Serif, Monospace)
159 - Chapter navigation
160 - Bookmark management
161 - Position persistence
162 - Progress tracking
163 """
165 def __init__(self,
166 blocks: List[Block],
167 page_size: Tuple[int, int],
168 document_id: str = "default",
169 buffer_size: int = 5,
170 page_style: Optional[PageStyle] = None,
171 bookmarks_dir: str = "bookmarks"):
172 """
173 Initialize the ereader layout manager.
175 Args:
176 blocks: Document blocks to render
177 page_size: Page size (width, height) in pixels
178 document_id: Unique identifier for the document (for bookmarks/position)
179 buffer_size: Number of pages to cache in each direction
180 page_style: Custom page styling (uses default if None)
181 bookmarks_dir: Directory to store bookmark files
182 """
183 self.blocks = blocks
184 self.page_size = page_size
185 self.document_id = document_id
187 # Initialize page style
188 if page_style is None:
189 page_style = PageStyle()
190 self.page_style = page_style
192 # Initialize core components
193 self.renderer = BufferedPageRenderer(blocks, page_style, buffer_size, page_size)
194 self.chapter_navigator = ChapterNavigator(blocks)
195 self.bookmark_manager = BookmarkManager(document_id, bookmarks_dir)
197 # Current state
198 self.current_position = RenderingPosition()
199 self.font_scale = 1.0
201 # Cover page handling
202 self._has_cover = self._detect_cover()
203 self._on_cover_page = self._has_cover # Start on cover if one exists
205 # Page position history for fast backward navigation
206 # List of (position, font_scale) tuples representing the start of each page visited
207 self._page_history: List[Tuple[RenderingPosition, float]] = []
208 self._max_history_size = 50 # Keep last 50 page positions
210 # Load last reading position if available
211 saved_position = self.bookmark_manager.load_reading_position()
212 if saved_position:
213 self.current_position = saved_position
214 self._on_cover_page = False # If we have a saved position, we're past the cover
216 # Callbacks for UI updates
217 self.position_changed_callback: Optional[Callable[[
218 RenderingPosition], None]] = None
219 self.chapter_changed_callback: Optional[Callable[[
220 Optional[ChapterInfo]], None]] = None
222 def set_position_changed_callback(
223 self, callback: Callable[[RenderingPosition], None]):
224 """Set callback for position changes"""
225 self.position_changed_callback = callback
227 def set_chapter_changed_callback(
228 self, callback: Callable[[Optional[ChapterInfo]], None]):
229 """Set callback for chapter changes"""
230 self.chapter_changed_callback = callback
232 def _detect_cover(self) -> bool:
233 """
234 Detect if the document has a cover page.
236 A cover is detected if:
237 1. The first block is an Image block, OR
238 2. The document has cover metadata (future enhancement)
240 Returns:
241 True if a cover page should be rendered
242 """
243 if not self.blocks:
244 return False
246 # Check if first block is an image - treat it as a cover
247 first_block = self.blocks[0]
248 if isinstance(first_block, Image):
249 return True
251 return False
253 def _render_cover_page(self) -> Page:
254 """
255 Render a dedicated cover page.
257 The cover page displays the first image block (if it exists)
258 using the standard image layouter with maximum dimensions to fill the page.
260 Returns:
261 Rendered cover page
262 """
263 # Create a new page for the cover
264 page = Page(self.page_size, self.page_style)
266 if not self.blocks or not isinstance(self.blocks[0], Image): 266 ↛ 268line 266 didn't jump to line 268 because the condition on line 266 was never true
267 # No cover image, return blank page
268 return page
270 cover_image_block = self.blocks[0]
272 # Use the image layouter to render the cover image
273 # Use full page dimensions (minus borders/padding) for cover
274 try:
275 max_width = self.page_size[0] - 2 * self.page_style.border_width
276 max_height = self.page_size[1] - 2 * self.page_style.border_width
278 # Layout the image on the page
279 success = image_layouter(
280 image=cover_image_block,
281 page=page,
282 max_width=max_width,
283 max_height=max_height
284 )
286 if not success: 286 ↛ 293line 286 didn't jump to line 293 because the condition on line 286 was always true
287 print("Warning: Failed to layout cover image")
289 except Exception as e:
290 # If image loading fails, just return the blank page
291 print(f"Warning: Failed to load cover image: {e}")
293 return page
295 def _notify_position_changed(self):
296 """Notify UI of position change"""
297 if self.position_changed_callback:
298 self.position_changed_callback(self.current_position)
300 # Check if chapter changed
301 current_chapter = self.chapter_navigator.get_current_chapter(
302 self.current_position)
303 if self.chapter_changed_callback:
304 self.chapter_changed_callback(current_chapter)
306 # Auto-save reading position
307 self.bookmark_manager.save_reading_position(self.current_position)
309 def get_current_page(self) -> Page:
310 """
311 Get the page at the current reading position.
313 If on the cover page, returns the rendered cover.
314 Otherwise, returns the regular content page.
316 Returns:
317 Rendered page
318 """
319 # Check if we're on the cover page
320 if self._on_cover_page and self._has_cover:
321 return self._render_cover_page()
323 page, _ = self.renderer.render_page(self.current_position, self.font_scale)
324 return page
326 def next_page(self) -> Optional[Page]:
327 """
328 Advance to the next page.
330 If currently on the cover page, advances to the first content page.
331 Otherwise, advances to the next content page.
333 Returns:
334 Next page or None if at end of document
335 """
336 # Special case: transitioning from cover to first content page
337 if self._on_cover_page and self._has_cover:
338 self._on_cover_page = False
339 # If first block is an image (the cover), skip it and start from block 1
340 if self.blocks and isinstance(self.blocks[0], Image): 340 ↛ 343line 340 didn't jump to line 343 because the condition on line 340 was always true
341 self.current_position = RenderingPosition(chapter_index=0, block_index=1)
342 else:
343 self.current_position = RenderingPosition()
344 self._notify_position_changed()
345 return self.get_current_page()
347 # Save current position to history before moving forward
348 self._add_to_history(self.current_position, self.font_scale)
350 page, next_position = self.renderer.render_page(
351 self.current_position, self.font_scale)
353 # Check if we made progress
354 if next_position != self.current_position:
355 self.current_position = next_position
356 self._notify_position_changed()
357 return self.get_current_page()
359 return None # At end of document
361 def previous_page(self) -> Optional[Page]:
362 """
363 Go to the previous page.
365 Uses cached page history for instant navigation when available,
366 falls back to iterative refinement algorithm when needed.
367 Can navigate back to the cover page if it exists.
369 Returns:
370 Previous page or None if at beginning of document (or on cover)
371 """
372 # Special case: if at the beginning of content and there's a cover, go back to it
373 if self._has_cover and self._is_at_beginning() and not self._on_cover_page:
374 self._on_cover_page = True
375 self._notify_position_changed()
376 return self.get_current_page()
378 # Can't go before the cover
379 if self._on_cover_page: 379 ↛ 380line 379 didn't jump to line 380 because the condition on line 379 was never true
380 return None
382 if self._is_at_beginning():
383 return None
385 # Fast path: Check if we have this position in history
386 previous_position = self._get_from_history(self.current_position, self.font_scale)
388 if previous_position is not None:
389 # Cache hit! Use the cached position for instant navigation
390 self.current_position = previous_position
391 self._notify_position_changed()
392 return self.get_current_page()
394 # Slow path: Use backward rendering to find the previous page
395 # This uses the iterative refinement algorithm we just fixed
396 page, start_position = self.renderer.render_page_backward(
397 self.current_position, self.font_scale)
399 if start_position != self.current_position: 399 ↛ 407line 399 didn't jump to line 407 because the condition on line 399 was always true
400 # Save this calculated position to history for future use
401 self._add_to_history(start_position, self.font_scale)
403 self.current_position = start_position
404 self._notify_position_changed()
405 return page
407 return None # At beginning of document
409 def _is_at_beginning(self) -> bool:
410 """
411 Check if we're at the beginning of the document content.
413 If a cover exists (first block is an Image), the beginning of content
414 is at block_index=1. Otherwise, it's at block_index=0.
415 """
416 # Determine the first content block index
417 first_content_block = 1 if (self._has_cover and self.blocks and isinstance(self.blocks[0], Image)) else 0
419 return (self.current_position.chapter_index == 0 and
420 self.current_position.block_index == first_content_block and
421 self.current_position.word_index == 0)
423 def jump_to_position(self, position: RenderingPosition) -> Page:
424 """
425 Jump to a specific position in the document.
427 Args:
428 position: Position to jump to
430 Returns:
431 Page at the new position
432 """
433 self.current_position = position
434 self._on_cover_page = False # Jumping to a position means we're past the cover
435 self._notify_position_changed()
436 return self.get_current_page()
438 def jump_to_chapter(self, chapter_title: str) -> Optional[Page]:
439 """
440 Jump to a specific chapter by title.
442 Args:
443 chapter_title: Title of the chapter to jump to
445 Returns:
446 Page at chapter start or None if chapter not found
447 """
448 position = self.chapter_navigator.get_chapter_position(chapter_title)
449 if position:
450 return self.jump_to_position(position)
451 return None
453 def jump_to_chapter_index(self, chapter_index: int) -> Optional[Page]:
454 """
455 Jump to a chapter by index.
457 Args:
458 chapter_index: Index of the chapter (0-based)
460 Returns:
461 Page at chapter start or None if index invalid
462 """
463 chapters = self.chapter_navigator.chapters
464 if 0 <= chapter_index < len(chapters):
465 return self.jump_to_position(chapters[chapter_index].position)
466 return None
468 def _add_to_history(self, position: RenderingPosition, font_scale: float):
469 """
470 Add a page position to the navigation history.
472 Args:
473 position: The page start position to remember
474 font_scale: The font scale at this position
475 """
476 # Only add if it's different from the last entry
477 if not self._page_history or \
478 self._page_history[-1][0] != position or \
479 self._page_history[-1][1] != font_scale:
481 self._page_history.append((position.copy(), font_scale))
483 # Trim history if it exceeds max size
484 if len(self._page_history) > self._max_history_size: 484 ↛ 485line 484 didn't jump to line 485 because the condition on line 484 was never true
485 self._page_history.pop(0)
487 def _get_from_history(
488 self,
489 current_position: RenderingPosition,
490 current_font_scale: float) -> Optional[RenderingPosition]:
491 """
492 Get the previous page position from history.
494 Searches backward through history to find the last position that
495 comes before the current position at the same font scale.
497 Args:
498 current_position: Current page position
499 current_font_scale: Current font scale
501 Returns:
502 Previous page position or None if not found in history
503 """
504 # Search backward through history
505 for i in range(len(self._page_history) - 1, -1, -1):
506 hist_position, hist_font_scale = self._page_history[i]
508 # Must match font scale
509 if hist_font_scale != current_font_scale: 509 ↛ 510line 509 didn't jump to line 510 because the condition on line 509 was never true
510 continue
512 # Must be before current position
513 if (hist_position.chapter_index < current_position.chapter_index or
514 (hist_position.chapter_index == current_position.chapter_index and
515 hist_position.block_index < current_position.block_index) or
516 (hist_position.chapter_index == current_position.chapter_index and
517 hist_position.block_index == current_position.block_index and
518 hist_position.word_index < current_position.word_index)):
520 # Found a previous position - remove it and everything after from history
521 # since we're navigating backward
522 self._page_history = self._page_history[:i]
523 return hist_position.copy()
525 return None
527 def _clear_history(self):
528 """Clear the page navigation history."""
529 self._page_history.clear()
531 def set_font_scale(self, scale: float) -> Page:
532 """
533 Change the font scale and re-render current page.
535 Clears page history since font changes invalidate all cached positions.
537 Args:
538 scale: Font scaling factor (1.0 = normal, 2.0 = double size, etc.)
540 Returns:
541 Re-rendered page with new font scale
542 """
543 if scale != self.font_scale:
544 self.font_scale = scale
545 # Clear history since font scale changes invalidate all cached positions
546 self._clear_history()
547 # The renderer will handle cache invalidation
549 return self.get_current_page()
551 def get_font_scale(self) -> float:
552 """Get the current font scale"""
553 return self.font_scale
555 def set_font_family(self, family: Optional[BundledFont]) -> Page:
556 """
557 Change the font family and re-render current page.
559 Switches all text in the document to use the specified bundled font family
560 while preserving font weights, styles, sizes, and other attributes.
561 Clears page history and cache since font changes invalidate all cached positions.
563 Args:
564 family: Bundled font family to use (SANS, SERIF, MONOSPACE, or None for original fonts)
566 Returns:
567 Re-rendered page with new font family
569 Example:
570 >>> from pyWebLayout.style.fonts import BundledFont
571 >>> manager.set_font_family(BundledFont.SERIF) # Switch to serif
572 >>> manager.set_font_family(BundledFont.SANS) # Switch to sans
573 >>> manager.set_font_family(None) # Restore original fonts
574 """
575 # Update the renderer's font family
576 self.renderer.set_font_family(family)
578 # Clear history since font changes invalidate all cached positions
579 self._clear_history()
581 return self.get_current_page()
583 def get_font_family(self) -> Optional[BundledFont]:
584 """
585 Get the current font family override.
587 Returns:
588 Current font family (SANS, SERIF, MONOSPACE) or None if using original fonts
589 """
590 return self.renderer.get_font_family()
592 def increase_line_spacing(self, amount: int = 2) -> Page:
593 """
594 Increase line spacing and re-render current page.
596 Clears page history since spacing changes invalidate all cached positions.
598 Args:
599 amount: Pixels to add to line spacing (default: 2)
601 Returns:
602 Re-rendered page with increased line spacing
603 """
604 self.page_style.line_spacing += amount
605 self.renderer.page_style = self.page_style # Update renderer's reference
606 self.renderer.buffer.invalidate_all() # Clear cache to force re-render
607 self._clear_history() # Clear position history
608 return self.get_current_page()
610 def decrease_line_spacing(self, amount: int = 2) -> Page:
611 """
612 Decrease line spacing and re-render current page.
614 Clears page history since spacing changes invalidate all cached positions.
616 Args:
617 amount: Pixels to remove from line spacing (default: 2)
619 Returns:
620 Re-rendered page with decreased line spacing
621 """
622 self.page_style.line_spacing = max(0, self.page_style.line_spacing - amount)
623 self.renderer.page_style = self.page_style # Update renderer's reference
624 self.renderer.buffer.invalidate_all() # Clear cache to force re-render
625 self._clear_history() # Clear position history
626 return self.get_current_page()
628 def increase_inter_block_spacing(self, amount: int = 5) -> Page:
629 """
630 Increase spacing between blocks and re-render current page.
632 Clears page history since spacing changes invalidate all cached positions.
634 Args:
635 amount: Pixels to add to inter-block spacing (default: 5)
637 Returns:
638 Re-rendered page with increased block spacing
639 """
640 self.page_style.inter_block_spacing += amount
641 self.renderer.page_style = self.page_style # Update renderer's reference
642 self.renderer.buffer.invalidate_all() # Clear cache to force re-render
643 self._clear_history() # Clear position history
644 return self.get_current_page()
646 def decrease_inter_block_spacing(self, amount: int = 5) -> Page:
647 """
648 Decrease spacing between blocks and re-render current page.
650 Clears page history since spacing changes invalidate all cached positions.
652 Args:
653 amount: Pixels to remove from inter-block spacing (default: 5)
655 Returns:
656 Re-rendered page with decreased block spacing
657 """
658 self.page_style.inter_block_spacing = max(
659 0, self.page_style.inter_block_spacing - amount)
660 self.renderer.page_style = self.page_style # Update renderer's reference
661 self.renderer.buffer.invalidate_all() # Clear cache to force re-render
662 self._clear_history() # Clear position history
663 return self.get_current_page()
665 def increase_word_spacing(self, amount: int = 2) -> Page:
666 """
667 Increase spacing between words and re-render current page.
669 Clears page history since spacing changes invalidate all cached positions.
671 Args:
672 amount: Pixels to add to word spacing (default: 2)
674 Returns:
675 Re-rendered page with increased word spacing
676 """
677 self.page_style.word_spacing += amount
678 self.renderer.page_style = self.page_style # Update renderer's reference
679 self.renderer.buffer.invalidate_all() # Clear cache to force re-render
680 self._clear_history() # Clear position history
681 return self.get_current_page()
683 def decrease_word_spacing(self, amount: int = 2) -> Page:
684 """
685 Decrease spacing between words and re-render current page.
687 Clears page history since spacing changes invalidate all cached positions.
689 Args:
690 amount: Pixels to remove from word spacing (default: 2)
692 Returns:
693 Re-rendered page with decreased word spacing
694 """
695 self.page_style.word_spacing = max(0, self.page_style.word_spacing - amount)
696 self.renderer.page_style = self.page_style # Update renderer's reference
697 self.renderer.buffer.invalidate_all() # Clear cache to force re-render
698 self._clear_history() # Clear position history
699 return self.get_current_page()
701 def get_table_of_contents(
702 self) -> List[Tuple[str, HeadingLevel, RenderingPosition]]:
703 """
704 Get the table of contents.
706 Returns:
707 List of (title, level, position) tuples
708 """
709 return self.chapter_navigator.get_table_of_contents()
711 def get_current_chapter(self) -> Optional[ChapterInfo]:
712 """
713 Get information about the current chapter.
715 Returns:
716 Current chapter info or None if no chapters
717 """
718 return self.chapter_navigator.get_current_chapter(self.current_position)
720 def add_bookmark(self, name: str) -> bool:
721 """
722 Add a bookmark at the current position.
724 Args:
725 name: Bookmark name
727 Returns:
728 True if bookmark was added successfully
729 """
730 try:
731 self.bookmark_manager.add_bookmark(name, self.current_position)
732 return True
733 except Exception:
734 return False
736 def remove_bookmark(self, name: str) -> bool:
737 """
738 Remove a bookmark.
740 Args:
741 name: Bookmark name
743 Returns:
744 True if bookmark was removed
745 """
746 return self.bookmark_manager.remove_bookmark(name)
748 def jump_to_bookmark(self, name: str) -> Optional[Page]:
749 """
750 Jump to a bookmark.
752 Args:
753 name: Bookmark name
755 Returns:
756 Page at bookmark position or None if bookmark not found
757 """
758 position = self.bookmark_manager.get_bookmark(name)
759 if position:
760 return self.jump_to_position(position)
761 return None
763 def list_bookmarks(self) -> List[Tuple[str, RenderingPosition]]:
764 """
765 Get all bookmarks.
767 Returns:
768 List of (name, position) tuples
769 """
770 return self.bookmark_manager.list_bookmarks()
772 def get_reading_progress(self) -> float:
773 """
774 Get reading progress as a percentage.
776 Returns:
777 Progress from 0.0 to 1.0
778 """
779 if not self.blocks:
780 return 0.0
782 # Simple progress calculation based on block index
783 # A more sophisticated version would consider word positions
784 total_blocks = len(self.blocks)
785 current_block = min(self.current_position.block_index, total_blocks - 1)
787 return current_block / max(1, total_blocks - 1)
789 def has_cover(self) -> bool:
790 """
791 Check if the document has a cover page.
793 Returns:
794 True if a cover page is available
795 """
796 return self._has_cover
798 def is_on_cover(self) -> bool:
799 """
800 Check if currently viewing the cover page.
802 Returns:
803 True if on the cover page
804 """
805 return self._on_cover_page
807 def jump_to_cover(self) -> Optional[Page]:
808 """
809 Jump to the cover page if one exists.
811 Returns:
812 Cover page or None if no cover exists
813 """
814 if not self._has_cover: 814 ↛ 815line 814 didn't jump to line 815 because the condition on line 814 was never true
815 return None
817 self._on_cover_page = True
818 self._notify_position_changed()
819 return self.get_current_page()
821 def get_position_info(self) -> Dict[str, Any]:
822 """
823 Get detailed information about the current position.
825 Returns:
826 Dictionary with position details
827 """
828 current_chapter = self.get_current_chapter()
829 font_family = self.get_font_family()
831 return {
832 'position': self.current_position.to_dict(),
833 'on_cover': self._on_cover_page,
834 'has_cover': self._has_cover,
835 'chapter': {
836 'title': current_chapter.title if current_chapter else None,
837 'level': current_chapter.level if current_chapter else None,
838 'index': current_chapter.block_index if current_chapter else None
839 },
840 'progress': self.get_reading_progress(),
841 'font_scale': self.font_scale,
842 'font_family': font_family.value if font_family else None,
843 'page_size': self.page_size
844 }
846 def get_cache_stats(self) -> Dict[str, Any]:
847 """
848 Get cache statistics for debugging/monitoring.
850 Returns:
851 Dictionary with cache statistics
852 """
853 return self.renderer.get_cache_stats()
855 def shutdown(self):
856 """
857 Shutdown the ereader manager and clean up resources.
858 Call this when the application is closing.
859 """
860 # Save current position
861 self.bookmark_manager.save_reading_position(self.current_position)
863 # Shutdown renderer and buffer
864 self.renderer.shutdown()
866 def __del__(self):
867 """Cleanup on destruction"""
868 self.shutdown()
871# Convenience function for quick setup
872def create_ereader_manager(blocks: List[Block],
873 page_size: Tuple[int, int],
874 document_id: str = "default",
875 **kwargs) -> EreaderLayoutManager:
876 """
877 Convenience function to create an ereader manager with sensible defaults.
879 Args:
880 blocks: Document blocks to render
881 page_size: Page size (width, height) in pixels
882 document_id: Unique identifier for the document
883 **kwargs: Additional arguments passed to EreaderLayoutManager
885 Returns:
886 Configured EreaderLayoutManager instance
887 """
888 return EreaderLayoutManager(blocks, page_size, document_id, **kwargs)