auto flake and corrections
This commit is contained in:
@@ -5,16 +5,22 @@ import numpy as np
|
||||
|
||||
from pyWebLayout.concrete import Page, Line, Text
|
||||
from pyWebLayout.concrete.image import RenderableImage
|
||||
from pyWebLayout.concrete.functional import LinkText, ButtonText, FormFieldText
|
||||
from pyWebLayout.concrete.functional import ButtonText, FormFieldText
|
||||
from pyWebLayout.concrete.table import TableRenderer, TableStyle
|
||||
from pyWebLayout.abstract import Paragraph, Word, Link
|
||||
from pyWebLayout.abstract import Paragraph, Word
|
||||
from pyWebLayout.abstract.block import Image as AbstractImage, PageBreak, Table
|
||||
from pyWebLayout.abstract.functional import Button, Form, FormField
|
||||
from pyWebLayout.abstract.inline import LinkedWord
|
||||
from pyWebLayout.style.concrete_style import ConcreteStyleRegistry, RenderingContext, StyleResolver
|
||||
from pyWebLayout.style import Font, Alignment
|
||||
|
||||
def paragraph_layouter(paragraph: Paragraph, page: Page, start_word: int = 0, pretext: Optional[Text] = None, alignment_override: Optional['Alignment'] = None) -> Tuple[bool, Optional[int], Optional[Text]]:
|
||||
|
||||
def paragraph_layouter(paragraph: Paragraph,
|
||||
page: Page,
|
||||
start_word: int = 0,
|
||||
pretext: Optional[Text] = None,
|
||||
alignment_override: Optional['Alignment'] = None) -> Tuple[bool,
|
||||
Optional[int],
|
||||
Optional[Text]]:
|
||||
"""
|
||||
Layout a paragraph of text within a given page.
|
||||
|
||||
@@ -44,7 +50,7 @@ def paragraph_layouter(paragraph: Paragraph, page: Page, start_word: int = 0, pr
|
||||
# paragraph.style is already a Font object (concrete), not AbstractStyle
|
||||
# We need to get word spacing constraints from the Font's abstract style if available
|
||||
# For now, use reasonable defaults based on font size
|
||||
|
||||
|
||||
if isinstance(paragraph.style, Font):
|
||||
# paragraph.style is already a Font (concrete style)
|
||||
font = paragraph.style
|
||||
@@ -63,7 +69,7 @@ def paragraph_layouter(paragraph: Paragraph, page: Page, start_word: int = 0, pr
|
||||
base_font_size = 16
|
||||
else:
|
||||
base_font_size = int(paragraph.style.font_size)
|
||||
|
||||
|
||||
rendering_context = RenderingContext(base_font_size=base_font_size)
|
||||
style_resolver = StyleResolver(rendering_context)
|
||||
style_registry = ConcreteStyleRegistry(style_resolver)
|
||||
@@ -76,7 +82,11 @@ def paragraph_layouter(paragraph: Paragraph, page: Page, start_word: int = 0, pr
|
||||
text_align = concrete_style.text_align
|
||||
|
||||
# Apply page-level word spacing override if specified
|
||||
if hasattr(page.style, 'word_spacing') and isinstance(page.style.word_spacing, int) and page.style.word_spacing > 0:
|
||||
if hasattr(
|
||||
page.style,
|
||||
'word_spacing') and isinstance(
|
||||
page.style.word_spacing,
|
||||
int) and page.style.word_spacing > 0:
|
||||
# Add the page-level word spacing to both min and max constraints
|
||||
min_ws, max_ws = word_spacing_constraints
|
||||
word_spacing_constraints = (
|
||||
@@ -87,7 +97,7 @@ def paragraph_layouter(paragraph: Paragraph, page: Page, start_word: int = 0, pr
|
||||
# Apply alignment override if provided
|
||||
if alignment_override is not None:
|
||||
text_align = alignment_override
|
||||
|
||||
|
||||
# Cap font size to page maximum if needed
|
||||
if font.font_size > page.style.max_font_size:
|
||||
font = Font(
|
||||
@@ -99,7 +109,7 @@ def paragraph_layouter(paragraph: Paragraph, page: Page, start_word: int = 0, pr
|
||||
decoration=font.decoration,
|
||||
background=font.background
|
||||
)
|
||||
|
||||
|
||||
# Calculate baseline-to-baseline spacing: font size + additional line spacing
|
||||
# This is the vertical distance between baselines of consecutive lines
|
||||
# Formula: baseline_spacing = font_size + line_spacing (absolute pixels)
|
||||
@@ -108,18 +118,20 @@ def paragraph_layouter(paragraph: Paragraph, page: Page, start_word: int = 0, pr
|
||||
if not isinstance(line_spacing_value, int):
|
||||
line_spacing_value = 5
|
||||
baseline_spacing = font.font_size + line_spacing_value
|
||||
|
||||
|
||||
# Get font metrics for boundary checking
|
||||
ascent, descent = font.font.getmetrics()
|
||||
|
||||
def create_new_line(word: Optional[Union[Word, Text]] = None, is_first_line: bool = False) -> Optional[Line]:
|
||||
def create_new_line(word: Optional[Union[Word, Text]] = None,
|
||||
is_first_line: bool = False) -> Optional[Line]:
|
||||
"""Helper function to create a new line, returns None if page is full."""
|
||||
# Check if this line's baseline and descenders would fit on the page
|
||||
if not page.can_fit_line(baseline_spacing, ascent, descent):
|
||||
return None
|
||||
|
||||
# For the first line, position it so text starts at the top boundary
|
||||
# For subsequent lines, use current y_offset which tracks baseline-to-baseline spacing
|
||||
# For subsequent lines, use current y_offset which tracks
|
||||
# baseline-to-baseline spacing
|
||||
if is_first_line:
|
||||
# Position line origin so that baseline (origin + ascent) is close to top
|
||||
# We want minimal space above the text, so origin should be at boundary
|
||||
@@ -131,9 +143,9 @@ def paragraph_layouter(paragraph: Paragraph, page: Page, start_word: int = 0, pr
|
||||
# Create a temporary Text object to calculate word width
|
||||
if word:
|
||||
temp_text = Text.from_word(word, page.draw)
|
||||
word_width = temp_text.width
|
||||
temp_text.width
|
||||
else:
|
||||
word_width = 0
|
||||
pass
|
||||
|
||||
return Line(
|
||||
spacing=word_spacing_constraints,
|
||||
@@ -163,7 +175,7 @@ def paragraph_layouter(paragraph: Paragraph, page: Page, start_word: int = 0, pr
|
||||
# but we may want to create LinkText for LinkedWord instances in future
|
||||
# For now, the abstract layer (LinkedWord) carries the link info,
|
||||
# and the concrete layer (LinkText) would be created during rendering
|
||||
|
||||
|
||||
success, overflow_text = current_line.add_word(word, current_pretext)
|
||||
|
||||
if success:
|
||||
@@ -195,7 +207,19 @@ def paragraph_layouter(paragraph: Paragraph, page: Page, start_word: int = 0, pr
|
||||
# Word is too wide for the line, we need to hyphenate it
|
||||
if len(word.text) >= 6:
|
||||
# Try to hyphenate the word
|
||||
splits = [(Text(pair[0], word.style, page.draw, line=current_line, source=word), Text(pair[1], word.style, page.draw, line=current_line, source=word)) for pair in word.possible_hyphenation()]
|
||||
splits = [
|
||||
(Text(
|
||||
pair[0],
|
||||
word.style,
|
||||
page.draw,
|
||||
line=current_line,
|
||||
source=word),
|
||||
Text(
|
||||
pair[1],
|
||||
word.style,
|
||||
page.draw,
|
||||
line=current_line,
|
||||
source=word)) for pair in word.possible_hyphenation()]
|
||||
if len(splits) > 0:
|
||||
# Use the first hyphenation point
|
||||
first_part, second_part = splits[0]
|
||||
@@ -230,15 +254,15 @@ def paragraph_layouter(paragraph: Paragraph, page: Page, start_word: int = 0, pr
|
||||
def pagebreak_layouter(page_break: PageBreak, page: Page) -> bool:
|
||||
"""
|
||||
Handle a page break element.
|
||||
|
||||
|
||||
A page break signals that all subsequent content should start on a new page.
|
||||
This function always returns False to indicate that the current page is complete
|
||||
and a new page should be created for subsequent content.
|
||||
|
||||
|
||||
Args:
|
||||
page_break: The PageBreak block
|
||||
page: The current page (not used, but kept for consistency)
|
||||
|
||||
|
||||
Returns:
|
||||
bool: Always False to force creation of a new page
|
||||
"""
|
||||
@@ -246,48 +270,49 @@ def pagebreak_layouter(page_break: PageBreak, page: Page) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def image_layouter(image: AbstractImage, page: Page, max_width: Optional[int] = None,
|
||||
def image_layouter(image: AbstractImage, page: Page, max_width: Optional[int] = None,
|
||||
max_height: Optional[int] = None) -> bool:
|
||||
"""
|
||||
Layout an image within a given page.
|
||||
|
||||
|
||||
This function places an image on the page, respecting size constraints
|
||||
and available space. Images are centered horizontally by default.
|
||||
|
||||
|
||||
Args:
|
||||
image: The abstract Image object to layout
|
||||
page: The page to layout the image on
|
||||
max_width: Maximum width constraint (defaults to page available width)
|
||||
max_height: Maximum height constraint (defaults to remaining page height)
|
||||
|
||||
|
||||
Returns:
|
||||
bool: True if image was successfully laid out, False if page ran out of space
|
||||
"""
|
||||
# Use page available width if max_width not specified
|
||||
if max_width is None:
|
||||
max_width = page.available_width
|
||||
|
||||
|
||||
# Calculate available height on page
|
||||
available_height = page.size[1] - page._current_y_offset - page.border_size
|
||||
if max_height is None:
|
||||
max_height = available_height
|
||||
else:
|
||||
max_height = min(max_height, available_height)
|
||||
|
||||
|
||||
# Calculate scaled dimensions
|
||||
scaled_width, scaled_height = image.calculate_scaled_dimensions(max_width, max_height)
|
||||
|
||||
scaled_width, scaled_height = image.calculate_scaled_dimensions(
|
||||
max_width, max_height)
|
||||
|
||||
# Check if image fits on current page
|
||||
if scaled_height is None or scaled_height > available_height:
|
||||
return False
|
||||
|
||||
|
||||
# Create renderable image
|
||||
x_offset = page.border_size
|
||||
y_offset = page._current_y_offset
|
||||
|
||||
|
||||
# Access page.draw to ensure canvas is initialized
|
||||
_ = page.draw
|
||||
|
||||
|
||||
renderable_image = RenderableImage(
|
||||
image=image,
|
||||
canvas=page._canvas,
|
||||
@@ -298,14 +323,17 @@ def image_layouter(image: AbstractImage, page: Page, max_width: Optional[int] =
|
||||
halign=Alignment.CENTER,
|
||||
valign=Alignment.TOP
|
||||
)
|
||||
|
||||
|
||||
# Add to page
|
||||
page.add_child(renderable_image)
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def table_layouter(table: Table, page: Page, style: Optional[TableStyle] = None) -> bool:
|
||||
def table_layouter(
|
||||
table: Table,
|
||||
page: Page,
|
||||
style: Optional[TableStyle] = None) -> bool:
|
||||
"""
|
||||
Layout a table within a given page.
|
||||
|
||||
@@ -356,8 +384,17 @@ def table_layouter(table: Table, page: Page, style: Optional[TableStyle] = None)
|
||||
return True
|
||||
|
||||
|
||||
def button_layouter(button: Button, page: Page, font: Optional[Font] = None,
|
||||
padding: Tuple[int, int, int, int] = (4, 8, 4, 8)) -> Tuple[bool, str]:
|
||||
def button_layouter(button: Button,
|
||||
page: Page,
|
||||
font: Optional[Font] = None,
|
||||
padding: Tuple[int,
|
||||
int,
|
||||
int,
|
||||
int] = (4,
|
||||
8,
|
||||
4,
|
||||
8)) -> Tuple[bool,
|
||||
str]:
|
||||
"""
|
||||
Layout a button within a given page and register it for callback binding.
|
||||
|
||||
@@ -510,17 +547,17 @@ def form_layouter(form: Form, page: Page, font: Optional[Font] = None,
|
||||
class DocumentLayouter:
|
||||
"""
|
||||
Document layouter that orchestrates layout of various abstract elements.
|
||||
|
||||
|
||||
Delegates to specialized layouters for different content types:
|
||||
- paragraph_layouter for text paragraphs
|
||||
- image_layouter for images
|
||||
- table_layouter for tables
|
||||
|
||||
|
||||
This class acts as a coordinator, managing the overall document flow
|
||||
and page context while delegating specific layout tasks to specialized
|
||||
layouter functions.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, page: Page):
|
||||
"""
|
||||
Initialize the document layouter with a page.
|
||||
@@ -538,24 +575,28 @@ class DocumentLayouter:
|
||||
context = RenderingContext()
|
||||
style_resolver = StyleResolver(context)
|
||||
self.style_registry = ConcreteStyleRegistry(style_resolver)
|
||||
|
||||
def layout_paragraph(self, paragraph: Paragraph, start_word: int = 0,
|
||||
pretext: Optional[Text] = None) -> Tuple[bool, Optional[int], Optional[Text]]:
|
||||
|
||||
def layout_paragraph(self,
|
||||
paragraph: Paragraph,
|
||||
start_word: int = 0,
|
||||
pretext: Optional[Text] = None) -> Tuple[bool,
|
||||
Optional[int],
|
||||
Optional[Text]]:
|
||||
"""
|
||||
Layout a paragraph using the paragraph_layouter.
|
||||
|
||||
|
||||
Args:
|
||||
paragraph: The paragraph to layout
|
||||
start_word: Index of the first word to process (for continuation)
|
||||
pretext: Optional pretext from a previous hyphenated word
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (success, failed_word_index, remaining_pretext)
|
||||
"""
|
||||
return paragraph_layouter(paragraph, self.page, start_word, pretext)
|
||||
|
||||
|
||||
def layout_image(self, image: AbstractImage, max_width: Optional[int] = None,
|
||||
max_height: Optional[int] = None) -> bool:
|
||||
max_height: Optional[int] = None) -> bool:
|
||||
"""
|
||||
Layout an image using the image_layouter.
|
||||
|
||||
@@ -582,8 +623,17 @@ class DocumentLayouter:
|
||||
"""
|
||||
return table_layouter(table, self.page, style)
|
||||
|
||||
def layout_button(self, button: Button, font: Optional[Font] = None,
|
||||
padding: Tuple[int, int, int, int] = (4, 8, 4, 8)) -> Tuple[bool, str]:
|
||||
def layout_button(self,
|
||||
button: Button,
|
||||
font: Optional[Font] = None,
|
||||
padding: Tuple[int,
|
||||
int,
|
||||
int,
|
||||
int] = (4,
|
||||
8,
|
||||
4,
|
||||
8)) -> Tuple[bool,
|
||||
str]:
|
||||
"""
|
||||
Layout a button using the button_layouter.
|
||||
|
||||
@@ -612,7 +662,8 @@ class DocumentLayouter:
|
||||
"""
|
||||
return form_layouter(form, self.page, font, field_spacing)
|
||||
|
||||
def layout_document(self, elements: List[Union[Paragraph, AbstractImage, Table, Button, Form]]) -> bool:
|
||||
def layout_document(
|
||||
self, elements: List[Union[Paragraph, AbstractImage, Table, Button, Form]]) -> bool:
|
||||
"""
|
||||
Layout a list of abstract elements (paragraphs, images, tables, buttons, and forms).
|
||||
|
||||
|
||||
@@ -13,18 +13,12 @@ with features like:
|
||||
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass, asdict
|
||||
from typing import List, Dict, Tuple, Optional, Union, Generator, Any
|
||||
from enum import Enum
|
||||
import json
|
||||
import multiprocessing
|
||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||
import threading
|
||||
import time
|
||||
from typing import List, Dict, Tuple, Optional, Any
|
||||
|
||||
from pyWebLayout.abstract.block import Block, Paragraph, Heading, HeadingLevel, Table, HList
|
||||
from pyWebLayout.abstract.inline import Word
|
||||
from pyWebLayout.concrete.page import Page
|
||||
from pyWebLayout.concrete.text import Line, Text
|
||||
from pyWebLayout.concrete.text import Text
|
||||
from pyWebLayout.style.page_style import PageStyle
|
||||
from pyWebLayout.style import Font
|
||||
from pyWebLayout.layout.document_layouter import paragraph_layouter
|
||||
@@ -38,32 +32,33 @@ class RenderingPosition:
|
||||
"""
|
||||
chapter_index: int = 0 # Which chapter (based on headings)
|
||||
block_index: int = 0 # Which block within chapter
|
||||
word_index: int = 0 # Which word within block (for paragraphs)
|
||||
# Which word within block (for paragraphs)
|
||||
word_index: int = 0
|
||||
table_row: int = 0 # Which row for tables
|
||||
table_col: int = 0 # Which column for tables
|
||||
list_item_index: int = 0 # Which item for lists
|
||||
remaining_pretext: Optional[str] = None # Hyphenated word continuation
|
||||
page_y_offset: int = 0 # Vertical position on page
|
||||
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Serialize position for saving to file/database"""
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'RenderingPosition':
|
||||
"""Deserialize position from saved state"""
|
||||
return cls(**data)
|
||||
|
||||
|
||||
def copy(self) -> 'RenderingPosition':
|
||||
"""Create a copy of this position"""
|
||||
return RenderingPosition(**asdict(self))
|
||||
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
"""Check if two positions are equal"""
|
||||
if not isinstance(other, RenderingPosition):
|
||||
return False
|
||||
return asdict(self) == asdict(other)
|
||||
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""Make position hashable for use as dict key"""
|
||||
return hash(tuple(asdict(self).values()))
|
||||
@@ -71,8 +66,13 @@ class RenderingPosition:
|
||||
|
||||
class ChapterInfo:
|
||||
"""Information about a chapter/section in the document"""
|
||||
|
||||
def __init__(self, title: str, level: HeadingLevel, position: RenderingPosition, block_index: int):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
level: HeadingLevel,
|
||||
position: RenderingPosition,
|
||||
block_index: int):
|
||||
self.title = title
|
||||
self.level = level
|
||||
self.position = position
|
||||
@@ -84,16 +84,16 @@ class ChapterNavigator:
|
||||
Handles chapter/section navigation based on HTML heading structure (H1-H6).
|
||||
Builds a table of contents and provides navigation capabilities.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, blocks: List[Block]):
|
||||
self.blocks = blocks
|
||||
self.chapters: List[ChapterInfo] = []
|
||||
self._build_chapter_map()
|
||||
|
||||
|
||||
def _build_chapter_map(self):
|
||||
"""Scan blocks for headings and build chapter navigation map"""
|
||||
current_chapter_index = 0
|
||||
|
||||
|
||||
for block_index, block in enumerate(self.blocks):
|
||||
if isinstance(block, Heading):
|
||||
# Create position for this heading
|
||||
@@ -105,23 +105,23 @@ class ChapterNavigator:
|
||||
table_col=0,
|
||||
list_item_index=0
|
||||
)
|
||||
|
||||
|
||||
# Extract heading text
|
||||
heading_text = self._extract_heading_text(block)
|
||||
|
||||
|
||||
chapter_info = ChapterInfo(
|
||||
title=heading_text,
|
||||
level=block.level,
|
||||
position=position,
|
||||
block_index=block_index
|
||||
)
|
||||
|
||||
|
||||
self.chapters.append(chapter_info)
|
||||
|
||||
|
||||
# Only increment chapter index for top-level headings (H1)
|
||||
if block.level == HeadingLevel.H1:
|
||||
current_chapter_index += 1
|
||||
|
||||
|
||||
def _extract_heading_text(self, heading: Heading) -> str:
|
||||
"""Extract text content from a heading block"""
|
||||
words = []
|
||||
@@ -129,33 +129,35 @@ class ChapterNavigator:
|
||||
if isinstance(word, Word):
|
||||
words.append(word.text)
|
||||
return " ".join(words)
|
||||
|
||||
def get_table_of_contents(self) -> List[Tuple[str, HeadingLevel, RenderingPosition]]:
|
||||
|
||||
def get_table_of_contents(
|
||||
self) -> List[Tuple[str, HeadingLevel, RenderingPosition]]:
|
||||
"""Generate table of contents from heading structure"""
|
||||
return [(chapter.title, chapter.level, chapter.position) for chapter in self.chapters]
|
||||
|
||||
return [(chapter.title, chapter.level, chapter.position)
|
||||
for chapter in self.chapters]
|
||||
|
||||
def get_chapter_position(self, chapter_title: str) -> Optional[RenderingPosition]:
|
||||
"""Get rendering position for a chapter by title"""
|
||||
for chapter in self.chapters:
|
||||
if chapter.title.lower() == chapter_title.lower():
|
||||
return chapter.position
|
||||
return None
|
||||
|
||||
|
||||
def get_current_chapter(self, position: RenderingPosition) -> Optional[ChapterInfo]:
|
||||
"""Determine which chapter contains the current position"""
|
||||
if not self.chapters:
|
||||
return None
|
||||
|
||||
|
||||
# Find the chapter that contains this position
|
||||
for i, chapter in enumerate(self.chapters):
|
||||
# Check if this is the last chapter or if position is before next chapter
|
||||
if i == len(self.chapters) - 1:
|
||||
return chapter
|
||||
|
||||
|
||||
next_chapter = self.chapters[i + 1]
|
||||
if position.chapter_index < next_chapter.position.chapter_index:
|
||||
return chapter
|
||||
|
||||
|
||||
return self.chapters[0] if self.chapters else None
|
||||
|
||||
|
||||
@@ -164,24 +166,24 @@ class FontScaler:
|
||||
Handles font scaling operations for ereader font size adjustments.
|
||||
Applies scaling at layout/render time while preserving original font objects.
|
||||
"""
|
||||
|
||||
|
||||
@staticmethod
|
||||
def scale_font(font: Font, scale_factor: float) -> Font:
|
||||
"""
|
||||
Create a scaled version of a font for layout calculations.
|
||||
|
||||
|
||||
Args:
|
||||
font: Original font object
|
||||
scale_factor: Scaling factor (1.0 = no change, 2.0 = double size, etc.)
|
||||
|
||||
|
||||
Returns:
|
||||
New Font object with scaled size
|
||||
"""
|
||||
if scale_factor == 1.0:
|
||||
return font
|
||||
|
||||
|
||||
scaled_size = max(1, int(font.font_size * scale_factor))
|
||||
|
||||
|
||||
return Font(
|
||||
font_path=font._font_path,
|
||||
font_size=scaled_size,
|
||||
@@ -193,13 +195,14 @@ class FontScaler:
|
||||
language=font.language,
|
||||
min_hyphenation_width=font.min_hyphenation_width
|
||||
)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def scale_word_spacing(spacing: Tuple[int, int], scale_factor: float) -> Tuple[int, int]:
|
||||
def scale_word_spacing(spacing: Tuple[int, int],
|
||||
scale_factor: float) -> Tuple[int, int]:
|
||||
"""Scale word spacing constraints proportionally"""
|
||||
if scale_factor == 1.0:
|
||||
return spacing
|
||||
|
||||
|
||||
min_spacing, max_spacing = spacing
|
||||
return (
|
||||
max(1, int(min_spacing * scale_factor)),
|
||||
@@ -212,41 +215,49 @@ class BidirectionalLayouter:
|
||||
Core layout engine supporting both forward and backward page rendering.
|
||||
Handles font scaling and maintains position state.
|
||||
"""
|
||||
|
||||
def __init__(self, blocks: List[Block], page_style: PageStyle, page_size: Tuple[int, int] = (800, 600), alignment_override=None):
|
||||
|
||||
def __init__(self,
|
||||
blocks: List[Block],
|
||||
page_style: PageStyle,
|
||||
page_size: Tuple[int,
|
||||
int] = (800,
|
||||
600),
|
||||
alignment_override=None):
|
||||
self.blocks = blocks
|
||||
self.page_style = page_style
|
||||
self.page_size = page_size
|
||||
self.chapter_navigator = ChapterNavigator(blocks)
|
||||
self.alignment_override = alignment_override
|
||||
|
||||
def render_page_forward(self, position: RenderingPosition, font_scale: float = 1.0) -> Tuple[Page, RenderingPosition]:
|
||||
|
||||
def render_page_forward(self, position: RenderingPosition,
|
||||
font_scale: float = 1.0) -> Tuple[Page, RenderingPosition]:
|
||||
"""
|
||||
Render a page starting from the given position, moving forward through the document.
|
||||
|
||||
|
||||
Args:
|
||||
position: Starting position in document
|
||||
font_scale: Font scaling factor
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (rendered_page, next_position)
|
||||
"""
|
||||
page = Page(size=self.page_size, style=self.page_style)
|
||||
current_pos = position.copy()
|
||||
|
||||
|
||||
# Start laying out blocks from the current position
|
||||
while current_pos.block_index < len(self.blocks) and page.free_space()[1] > 0:
|
||||
# Additional bounds check to prevent IndexError
|
||||
if current_pos.block_index >= len(self.blocks):
|
||||
break
|
||||
|
||||
|
||||
block = self.blocks[current_pos.block_index]
|
||||
|
||||
|
||||
# Apply font scaling to the block
|
||||
scaled_block = self._scale_block_fonts(block, font_scale)
|
||||
|
||||
|
||||
# Try to fit the block on the current page
|
||||
success, new_pos = self._layout_block_on_page(scaled_block, page, current_pos, font_scale)
|
||||
success, new_pos = self._layout_block_on_page(
|
||||
scaled_block, page, current_pos, font_scale)
|
||||
|
||||
if not success:
|
||||
# Block doesn't fit, we're done with this page
|
||||
@@ -262,45 +273,50 @@ class BidirectionalLayouter:
|
||||
# We've reached the end of the document
|
||||
current_pos = new_pos
|
||||
break
|
||||
|
||||
|
||||
current_pos = new_pos
|
||||
|
||||
|
||||
return page, current_pos
|
||||
|
||||
def render_page_backward(self, end_position: RenderingPosition, font_scale: float = 1.0) -> Tuple[Page, RenderingPosition]:
|
||||
|
||||
def render_page_backward(self,
|
||||
end_position: RenderingPosition,
|
||||
font_scale: float = 1.0) -> Tuple[Page,
|
||||
RenderingPosition]:
|
||||
"""
|
||||
Render a page that ends at the given position, filling backward.
|
||||
Critical for "previous page" navigation.
|
||||
|
||||
|
||||
Args:
|
||||
end_position: Position where page should end
|
||||
font_scale: Font scaling factor
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (rendered_page, start_position)
|
||||
"""
|
||||
# This is a complex operation that requires iterative refinement
|
||||
# We'll start with an estimated start position and refine it
|
||||
|
||||
|
||||
estimated_start = self._estimate_page_start(end_position, font_scale)
|
||||
|
||||
|
||||
# Render forward from estimated start and see if we reach the target
|
||||
page, actual_end = self.render_page_forward(estimated_start, font_scale)
|
||||
|
||||
|
||||
# If we overshot or undershot, adjust and try again
|
||||
# This is a simplified implementation - a full version would be more sophisticated
|
||||
# This is a simplified implementation - a full version would be more
|
||||
# sophisticated
|
||||
if self._position_compare(actual_end, end_position) != 0:
|
||||
# Adjust estimate and try again (simplified)
|
||||
estimated_start = self._adjust_start_estimate(estimated_start, end_position, actual_end)
|
||||
estimated_start = self._adjust_start_estimate(
|
||||
estimated_start, end_position, actual_end)
|
||||
page, actual_end = self.render_page_forward(estimated_start, font_scale)
|
||||
|
||||
|
||||
return page, estimated_start
|
||||
|
||||
|
||||
def _scale_block_fonts(self, block: Block, font_scale: float) -> Block:
|
||||
"""Apply font scaling to all fonts in a block"""
|
||||
if font_scale == 1.0:
|
||||
return block
|
||||
|
||||
|
||||
# This is a simplified implementation
|
||||
# In practice, we'd need to handle each block type appropriately
|
||||
if isinstance(block, (Paragraph, Heading)):
|
||||
@@ -309,20 +325,27 @@ class BidirectionalLayouter:
|
||||
scaled_block = Heading(block.level, scaled_block_style)
|
||||
else:
|
||||
scaled_block = Paragraph(scaled_block_style)
|
||||
|
||||
|
||||
# words_iter() returns tuples of (position, word)
|
||||
for position, word in block.words_iter():
|
||||
if isinstance(word, Word):
|
||||
scaled_word = Word(word.text, FontScaler.scale_font(word.style, font_scale))
|
||||
scaled_word = Word(
|
||||
word.text, FontScaler.scale_font(
|
||||
word.style, font_scale))
|
||||
scaled_block.add_word(scaled_word)
|
||||
return scaled_block
|
||||
|
||||
|
||||
return block
|
||||
|
||||
def _layout_block_on_page(self, block: Block, page: Page, position: RenderingPosition, font_scale: float) -> Tuple[bool, RenderingPosition]:
|
||||
|
||||
def _layout_block_on_page(self,
|
||||
block: Block,
|
||||
page: Page,
|
||||
position: RenderingPosition,
|
||||
font_scale: float) -> Tuple[bool,
|
||||
RenderingPosition]:
|
||||
"""
|
||||
Try to layout a block on the page starting from the given position.
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (success, new_position)
|
||||
"""
|
||||
@@ -339,18 +362,23 @@ class BidirectionalLayouter:
|
||||
new_pos = position.copy()
|
||||
new_pos.block_index += 1
|
||||
return True, new_pos
|
||||
|
||||
def _layout_paragraph_on_page(self, paragraph: Paragraph, page: Page, position: RenderingPosition, font_scale: float) -> Tuple[bool, RenderingPosition]:
|
||||
|
||||
def _layout_paragraph_on_page(self,
|
||||
paragraph: Paragraph,
|
||||
page: Page,
|
||||
position: RenderingPosition,
|
||||
font_scale: float) -> Tuple[bool,
|
||||
RenderingPosition]:
|
||||
"""
|
||||
Layout a paragraph on the page using the core paragraph_layouter.
|
||||
Integrates font scaling and position tracking with the proven layout logic.
|
||||
|
||||
|
||||
Args:
|
||||
paragraph: The paragraph to layout (already scaled if font_scale != 1.0)
|
||||
page: The page to layout on
|
||||
position: Current rendering position
|
||||
font_scale: Font scaling factor (used for context, paragraph should already be scaled)
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (success, new_position)
|
||||
"""
|
||||
@@ -365,7 +393,7 @@ class BidirectionalLayouter:
|
||||
line=None,
|
||||
source=None
|
||||
)
|
||||
|
||||
|
||||
# Call the core paragraph layouter with alignment override if set
|
||||
success, failed_word_index, remaining_pretext = paragraph_layouter(
|
||||
paragraph,
|
||||
@@ -374,10 +402,10 @@ class BidirectionalLayouter:
|
||||
pretext=pretext_obj,
|
||||
alignment_override=self.alignment_override
|
||||
)
|
||||
|
||||
|
||||
# Create new position based on the result
|
||||
new_pos = position.copy()
|
||||
|
||||
|
||||
if success:
|
||||
# Paragraph was fully laid out, move to next block
|
||||
new_pos.block_index += 1
|
||||
@@ -389,25 +417,35 @@ class BidirectionalLayouter:
|
||||
if failed_word_index is not None:
|
||||
# Update position to the word that didn't fit
|
||||
new_pos.word_index = failed_word_index
|
||||
|
||||
|
||||
# Convert Text object back to string if there's remaining pretext
|
||||
if remaining_pretext is not None and hasattr(remaining_pretext, 'text'):
|
||||
new_pos.remaining_pretext = remaining_pretext.text
|
||||
else:
|
||||
new_pos.remaining_pretext = None
|
||||
|
||||
|
||||
return False, new_pos
|
||||
else:
|
||||
# No specific word failed, but layout wasn't successful
|
||||
# This shouldn't normally happen, but handle it gracefully
|
||||
return False, position
|
||||
|
||||
def _layout_heading_on_page(self, heading: Heading, page: Page, position: RenderingPosition, font_scale: float) -> Tuple[bool, RenderingPosition]:
|
||||
|
||||
def _layout_heading_on_page(self,
|
||||
heading: Heading,
|
||||
page: Page,
|
||||
position: RenderingPosition,
|
||||
font_scale: float) -> Tuple[bool,
|
||||
RenderingPosition]:
|
||||
"""Layout a heading on the page"""
|
||||
# Similar to paragraph but with heading-specific styling
|
||||
return self._layout_paragraph_on_page(heading, page, position, font_scale)
|
||||
|
||||
def _layout_table_on_page(self, table: Table, page: Page, position: RenderingPosition, font_scale: float) -> Tuple[bool, RenderingPosition]:
|
||||
|
||||
def _layout_table_on_page(self,
|
||||
table: Table,
|
||||
page: Page,
|
||||
position: RenderingPosition,
|
||||
font_scale: float) -> Tuple[bool,
|
||||
RenderingPosition]:
|
||||
"""Layout a table on the page with column fitting and row continuation"""
|
||||
# This is a complex operation that would need full table layout logic
|
||||
# For now, skip tables
|
||||
@@ -416,8 +454,13 @@ class BidirectionalLayouter:
|
||||
new_pos.table_row = 0
|
||||
new_pos.table_col = 0
|
||||
return True, new_pos
|
||||
|
||||
def _layout_list_on_page(self, hlist: HList, page: Page, position: RenderingPosition, font_scale: float) -> Tuple[bool, RenderingPosition]:
|
||||
|
||||
def _layout_list_on_page(self,
|
||||
hlist: HList,
|
||||
page: Page,
|
||||
position: RenderingPosition,
|
||||
font_scale: float) -> Tuple[bool,
|
||||
RenderingPosition]:
|
||||
"""Layout a list on the page"""
|
||||
# This would need list-specific layout logic
|
||||
# For now, skip lists
|
||||
@@ -425,33 +468,43 @@ class BidirectionalLayouter:
|
||||
new_pos.block_index += 1
|
||||
new_pos.list_item_index = 0
|
||||
return True, new_pos
|
||||
|
||||
def _estimate_page_start(self, end_position: RenderingPosition, font_scale: float) -> RenderingPosition:
|
||||
|
||||
def _estimate_page_start(
|
||||
self,
|
||||
end_position: RenderingPosition,
|
||||
font_scale: float) -> RenderingPosition:
|
||||
"""Estimate where a page should start to end at the given position"""
|
||||
# This is a simplified heuristic - a full implementation would be more sophisticated
|
||||
# This is a simplified heuristic - a full implementation would be more
|
||||
# sophisticated
|
||||
estimated_start = end_position.copy()
|
||||
|
||||
|
||||
# Move back by an estimated number of blocks that would fit on a page
|
||||
estimated_blocks_per_page = max(1, int(10 / font_scale)) # Rough estimate
|
||||
estimated_start.block_index = max(0, end_position.block_index - estimated_blocks_per_page)
|
||||
estimated_start.block_index = max(
|
||||
0, end_position.block_index - estimated_blocks_per_page)
|
||||
estimated_start.word_index = 0
|
||||
|
||||
|
||||
return estimated_start
|
||||
|
||||
def _adjust_start_estimate(self, current_start: RenderingPosition, target_end: RenderingPosition, actual_end: RenderingPosition) -> RenderingPosition:
|
||||
|
||||
def _adjust_start_estimate(
|
||||
self,
|
||||
current_start: RenderingPosition,
|
||||
target_end: RenderingPosition,
|
||||
actual_end: RenderingPosition) -> RenderingPosition:
|
||||
"""Adjust start position estimate based on overshoot/undershoot"""
|
||||
# Simplified adjustment logic
|
||||
adjusted = current_start.copy()
|
||||
|
||||
|
||||
comparison = self._position_compare(actual_end, target_end)
|
||||
if comparison > 0: # Overshot
|
||||
adjusted.block_index = max(0, adjusted.block_index + 1)
|
||||
elif comparison < 0: # Undershot
|
||||
adjusted.block_index = max(0, adjusted.block_index - 1)
|
||||
|
||||
|
||||
return adjusted
|
||||
|
||||
def _position_compare(self, pos1: RenderingPosition, pos2: RenderingPosition) -> int:
|
||||
|
||||
def _position_compare(self, pos1: RenderingPosition,
|
||||
pos2: RenderingPosition) -> int:
|
||||
"""Compare two positions (-1: pos1 < pos2, 0: equal, 1: pos1 > pos2)"""
|
||||
if pos1.chapter_index != pos2.chapter_index:
|
||||
return 1 if pos1.chapter_index > pos2.chapter_index else -1
|
||||
@@ -470,16 +523,17 @@ def _add_page_methods():
|
||||
"""Check if a line of given height can fit on the page"""
|
||||
available_height = self.content_size[1] - self._current_y_offset
|
||||
return available_height >= line_height
|
||||
|
||||
|
||||
Page.can_fit_line = can_fit_line
|
||||
|
||||
|
||||
if not hasattr(Page, 'available_width'):
|
||||
@property
|
||||
def available_width(self) -> int:
|
||||
"""Get available width for content"""
|
||||
return self.content_size[0]
|
||||
|
||||
|
||||
Page.available_width = available_width
|
||||
|
||||
|
||||
# Apply the page methods
|
||||
_add_page_methods()
|
||||
|
||||
@@ -9,7 +9,6 @@ into a unified, easy-to-use API.
|
||||
from __future__ import annotations
|
||||
from typing import List, Dict, Optional, Tuple, Any, Callable
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from .ereader_layout import RenderingPosition, ChapterNavigator, ChapterInfo
|
||||
@@ -23,11 +22,11 @@ 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
|
||||
@@ -35,13 +34,13 @@ class BookmarkManager:
|
||||
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():
|
||||
@@ -55,7 +54,7 @@ class BookmarkManager:
|
||||
except Exception as e:
|
||||
print(f"Failed to load bookmarks: {e}")
|
||||
self._bookmarks = {}
|
||||
|
||||
|
||||
def _save_bookmarks(self):
|
||||
"""Save bookmarks to file"""
|
||||
try:
|
||||
@@ -67,25 +66,25 @@ class BookmarkManager:
|
||||
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
|
||||
"""
|
||||
@@ -94,32 +93,32 @@ class BookmarkManager:
|
||||
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
|
||||
"""
|
||||
@@ -128,11 +127,11 @@ class BookmarkManager:
|
||||
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
|
||||
"""
|
||||
@@ -149,7 +148,7 @@ class BookmarkManager:
|
||||
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
|
||||
@@ -158,17 +157,17 @@ class EreaderLayoutManager:
|
||||
- Position persistence
|
||||
- Progress tracking
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
blocks: List[Block],
|
||||
page_size: Tuple[int, int],
|
||||
|
||||
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
|
||||
@@ -180,125 +179,132 @@ class EreaderLayoutManager:
|
||||
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
|
||||
|
||||
|
||||
# Load last reading position if available
|
||||
saved_position = self.bookmark_manager.load_reading_position()
|
||||
if saved_position:
|
||||
self.current_position = saved_position
|
||||
|
||||
|
||||
# Callbacks for UI updates
|
||||
self.position_changed_callback: Optional[Callable[[RenderingPosition], None]] = None
|
||||
self.chapter_changed_callback: Optional[Callable[[Optional[ChapterInfo]], None]] = None
|
||||
|
||||
def set_position_changed_callback(self, callback: Callable[[RenderingPosition], None]):
|
||||
self.position_changed_callback: Optional[Callable[[
|
||||
RenderingPosition], None]] = None
|
||||
self.chapter_changed_callback: Optional[Callable[[
|
||||
Optional[ChapterInfo]], None]] = None
|
||||
|
||||
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]):
|
||||
|
||||
def set_chapter_changed_callback(
|
||||
self, callback: Callable[[Optional[ChapterInfo]], None]):
|
||||
"""Set callback for chapter changes"""
|
||||
self.chapter_changed_callback = callback
|
||||
|
||||
|
||||
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)
|
||||
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.
|
||||
|
||||
|
||||
Returns:
|
||||
Rendered 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.
|
||||
|
||||
|
||||
Returns:
|
||||
Next page or None if at end of document
|
||||
"""
|
||||
page, next_position = self.renderer.render_page(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()
|
||||
|
||||
|
||||
return None # At end of document
|
||||
|
||||
|
||||
def previous_page(self) -> Optional[Page]:
|
||||
"""
|
||||
Go to the previous page.
|
||||
|
||||
|
||||
Returns:
|
||||
Previous page or None if at beginning of document
|
||||
"""
|
||||
if self._is_at_beginning():
|
||||
return None
|
||||
|
||||
|
||||
# Use backward rendering to find the previous page
|
||||
page, start_position = self.renderer.render_page_backward(self.current_position, self.font_scale)
|
||||
|
||||
page, start_position = self.renderer.render_page_backward(
|
||||
self.current_position, self.font_scale)
|
||||
|
||||
if start_position != self.current_position:
|
||||
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"""
|
||||
return (self.current_position.chapter_index == 0 and
|
||||
self.current_position.block_index == 0 and
|
||||
return (self.current_position.chapter_index == 0 and
|
||||
self.current_position.block_index == 0 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._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
|
||||
"""
|
||||
@@ -306,14 +312,14 @@ class EreaderLayoutManager:
|
||||
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
|
||||
"""
|
||||
@@ -321,23 +327,23 @@ class EreaderLayoutManager:
|
||||
if 0 <= chapter_index < len(chapters):
|
||||
return self.jump_to_position(chapters[chapter_index].position)
|
||||
return None
|
||||
|
||||
|
||||
def set_font_scale(self, scale: float) -> Page:
|
||||
"""
|
||||
Change the font scale and re-render current page.
|
||||
|
||||
|
||||
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
|
||||
# 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
|
||||
@@ -397,7 +403,8 @@ class EreaderLayoutManager:
|
||||
Returns:
|
||||
Re-rendered page with decreased block spacing
|
||||
"""
|
||||
self.page_style.inter_block_spacing = max(0, self.page_style.inter_block_spacing - amount)
|
||||
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
|
||||
return self.get_current_page()
|
||||
@@ -432,31 +439,32 @@ class EreaderLayoutManager:
|
||||
self.renderer.buffer.invalidate_all() # Clear cache to force re-render
|
||||
return self.get_current_page()
|
||||
|
||||
def get_table_of_contents(self) -> List[Tuple[str, HeadingLevel, RenderingPosition]]:
|
||||
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
|
||||
"""
|
||||
@@ -465,26 +473,26 @@ class EreaderLayoutManager:
|
||||
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
|
||||
"""
|
||||
@@ -492,42 +500,42 @@ class EreaderLayoutManager:
|
||||
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 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()
|
||||
|
||||
|
||||
return {
|
||||
'position': self.current_position.to_dict(),
|
||||
'chapter': {
|
||||
@@ -539,16 +547,16 @@ class EreaderLayoutManager:
|
||||
'font_scale': self.font_scale,
|
||||
'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.
|
||||
@@ -556,29 +564,29 @@ class EreaderLayoutManager:
|
||||
"""
|
||||
# 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:
|
||||
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
|
||||
"""
|
||||
|
||||
@@ -8,12 +8,9 @@ multiprocessing to achieve sub-second page navigation performance.
|
||||
from __future__ import annotations
|
||||
from typing import Dict, Optional, List, Tuple, Any
|
||||
from collections import OrderedDict
|
||||
import multiprocessing
|
||||
from concurrent.futures import ProcessPoolExecutor, as_completed, Future
|
||||
from concurrent.futures import ProcessPoolExecutor, Future
|
||||
import threading
|
||||
import time
|
||||
import pickle
|
||||
from dataclasses import asdict
|
||||
|
||||
from .ereader_layout import RenderingPosition, BidirectionalLayouter
|
||||
from pyWebLayout.concrete.page import Page
|
||||
@@ -21,28 +18,34 @@ from pyWebLayout.abstract.block import Block
|
||||
from pyWebLayout.style.page_style import PageStyle
|
||||
|
||||
|
||||
def _render_page_worker(args: Tuple[List[Block], PageStyle, RenderingPosition, float, bool]) -> Tuple[RenderingPosition, bytes, RenderingPosition]:
|
||||
def _render_page_worker(args: Tuple[List[Block],
|
||||
PageStyle,
|
||||
RenderingPosition,
|
||||
float,
|
||||
bool]) -> Tuple[RenderingPosition,
|
||||
bytes,
|
||||
RenderingPosition]:
|
||||
"""
|
||||
Worker function for multiprocess page rendering.
|
||||
|
||||
|
||||
Args:
|
||||
args: Tuple of (blocks, page_style, position, font_scale, is_backward)
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (original_position, pickled_page, next_position)
|
||||
"""
|
||||
blocks, page_style, position, font_scale, is_backward = args
|
||||
|
||||
|
||||
layouter = BidirectionalLayouter(blocks, page_style)
|
||||
|
||||
|
||||
if is_backward:
|
||||
page, next_pos = layouter.render_page_backward(position, font_scale)
|
||||
else:
|
||||
page, next_pos = layouter.render_page_forward(position, font_scale)
|
||||
|
||||
|
||||
# Serialize the page for inter-process communication
|
||||
pickled_page = pickle.dumps(page)
|
||||
|
||||
|
||||
return position, pickled_page, next_pos
|
||||
|
||||
|
||||
@@ -51,40 +54,46 @@ class PageBuffer:
|
||||
Intelligent page caching system with LRU eviction and background rendering.
|
||||
Maintains separate forward and backward buffers for optimal navigation performance.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, buffer_size: int = 5, max_workers: int = 4):
|
||||
"""
|
||||
Initialize the page buffer.
|
||||
|
||||
|
||||
Args:
|
||||
buffer_size: Number of pages to cache in each direction
|
||||
max_workers: Maximum number of worker processes for background rendering
|
||||
"""
|
||||
self.buffer_size = buffer_size
|
||||
self.max_workers = max_workers
|
||||
|
||||
|
||||
# LRU caches for forward and backward pages
|
||||
self.forward_buffer: OrderedDict[RenderingPosition, Page] = OrderedDict()
|
||||
self.backward_buffer: OrderedDict[RenderingPosition, Page] = OrderedDict()
|
||||
|
||||
|
||||
# Position tracking for next/previous positions
|
||||
self.position_map: Dict[RenderingPosition, RenderingPosition] = {} # current -> next
|
||||
self.reverse_position_map: Dict[RenderingPosition, RenderingPosition] = {} # current -> previous
|
||||
|
||||
self.position_map: Dict[RenderingPosition,
|
||||
RenderingPosition] = {} # current -> next
|
||||
self.reverse_position_map: Dict[RenderingPosition,
|
||||
RenderingPosition] = {} # current -> previous
|
||||
|
||||
# Background rendering
|
||||
self.executor: Optional[ProcessPoolExecutor] = None
|
||||
self.pending_renders: Dict[RenderingPosition, Future] = {}
|
||||
self.render_lock = threading.Lock()
|
||||
|
||||
|
||||
# Document state
|
||||
self.blocks: Optional[List[Block]] = None
|
||||
self.page_style: Optional[PageStyle] = None
|
||||
self.current_font_scale: float = 1.0
|
||||
|
||||
def initialize(self, blocks: List[Block], page_style: PageStyle, font_scale: float = 1.0):
|
||||
|
||||
def initialize(
|
||||
self,
|
||||
blocks: List[Block],
|
||||
page_style: PageStyle,
|
||||
font_scale: float = 1.0):
|
||||
"""
|
||||
Initialize the buffer with document blocks and page style.
|
||||
|
||||
|
||||
Args:
|
||||
blocks: Document blocks to render
|
||||
page_style: Page styling configuration
|
||||
@@ -93,18 +102,18 @@ class PageBuffer:
|
||||
self.blocks = blocks
|
||||
self.page_style = page_style
|
||||
self.current_font_scale = font_scale
|
||||
|
||||
|
||||
# Start the process pool
|
||||
if self.executor is None:
|
||||
self.executor = ProcessPoolExecutor(max_workers=self.max_workers)
|
||||
|
||||
|
||||
def get_page(self, position: RenderingPosition) -> Optional[Page]:
|
||||
"""
|
||||
Get a cached page if available.
|
||||
|
||||
|
||||
Args:
|
||||
position: Position to get page for
|
||||
|
||||
|
||||
Returns:
|
||||
Cached page or None if not available
|
||||
"""
|
||||
@@ -114,20 +123,25 @@ class PageBuffer:
|
||||
page = self.forward_buffer.pop(position)
|
||||
self.forward_buffer[position] = page
|
||||
return page
|
||||
|
||||
|
||||
# Check backward buffer
|
||||
if position in self.backward_buffer:
|
||||
# Move to end (most recently used)
|
||||
page = self.backward_buffer.pop(position)
|
||||
self.backward_buffer[position] = page
|
||||
return page
|
||||
|
||||
|
||||
return None
|
||||
|
||||
def cache_page(self, position: RenderingPosition, page: Page, next_position: Optional[RenderingPosition] = None, is_backward: bool = False):
|
||||
|
||||
def cache_page(
|
||||
self,
|
||||
position: RenderingPosition,
|
||||
page: Page,
|
||||
next_position: Optional[RenderingPosition] = None,
|
||||
is_backward: bool = False):
|
||||
"""
|
||||
Cache a rendered page with LRU eviction.
|
||||
|
||||
|
||||
Args:
|
||||
position: Position of the page
|
||||
page: Rendered page to cache
|
||||
@@ -135,46 +149,49 @@ class PageBuffer:
|
||||
is_backward: Whether this is a backward-rendered page
|
||||
"""
|
||||
target_buffer = self.backward_buffer if is_backward else self.forward_buffer
|
||||
|
||||
|
||||
# Add to cache
|
||||
target_buffer[position] = page
|
||||
|
||||
|
||||
# Track position relationships
|
||||
if next_position:
|
||||
if is_backward:
|
||||
self.reverse_position_map[next_position] = position
|
||||
else:
|
||||
self.position_map[position] = next_position
|
||||
|
||||
|
||||
# Evict oldest if buffer is full
|
||||
if len(target_buffer) > self.buffer_size:
|
||||
oldest_pos, _ = target_buffer.popitem(last=False)
|
||||
# Clean up position maps
|
||||
self.position_map.pop(oldest_pos, None)
|
||||
self.reverse_position_map.pop(oldest_pos, None)
|
||||
|
||||
def start_background_rendering(self, current_position: RenderingPosition, direction: str = 'forward'):
|
||||
|
||||
def start_background_rendering(
|
||||
self,
|
||||
current_position: RenderingPosition,
|
||||
direction: str = 'forward'):
|
||||
"""
|
||||
Start background rendering of upcoming pages.
|
||||
|
||||
|
||||
Args:
|
||||
current_position: Current reading position
|
||||
direction: 'forward', 'backward', or 'both'
|
||||
"""
|
||||
if not self.blocks or not self.page_style or not self.executor:
|
||||
return
|
||||
|
||||
|
||||
with self.render_lock:
|
||||
if direction in ['forward', 'both']:
|
||||
self._queue_forward_renders(current_position)
|
||||
|
||||
|
||||
if direction in ['backward', 'both']:
|
||||
self._queue_backward_renders(current_position)
|
||||
|
||||
|
||||
def _queue_forward_renders(self, start_position: RenderingPosition):
|
||||
"""Queue forward page renders starting from the given position"""
|
||||
current_pos = start_position
|
||||
|
||||
|
||||
for i in range(self.buffer_size):
|
||||
# Skip if already cached or being rendered
|
||||
if current_pos in self.forward_buffer or current_pos in self.pending_renders:
|
||||
@@ -183,19 +200,25 @@ class PageBuffer:
|
||||
if not current_pos:
|
||||
break
|
||||
continue
|
||||
|
||||
|
||||
# Queue render job
|
||||
args = (self.blocks, self.page_style, current_pos, self.current_font_scale, False)
|
||||
args = (
|
||||
self.blocks,
|
||||
self.page_style,
|
||||
current_pos,
|
||||
self.current_font_scale,
|
||||
False)
|
||||
future = self.executor.submit(_render_page_worker, args)
|
||||
self.pending_renders[current_pos] = future
|
||||
|
||||
# We don't know the next position yet, so we'll update it when the render completes
|
||||
|
||||
# We don't know the next position yet, so we'll update it when the render
|
||||
# completes
|
||||
break
|
||||
|
||||
|
||||
def _queue_backward_renders(self, start_position: RenderingPosition):
|
||||
"""Queue backward page renders ending at the given position"""
|
||||
current_pos = start_position
|
||||
|
||||
|
||||
for i in range(self.buffer_size):
|
||||
# Skip if already cached or being rendered
|
||||
if current_pos in self.backward_buffer or current_pos in self.pending_renders:
|
||||
@@ -204,44 +227,50 @@ class PageBuffer:
|
||||
if not current_pos:
|
||||
break
|
||||
continue
|
||||
|
||||
|
||||
# Queue render job
|
||||
args = (self.blocks, self.page_style, current_pos, self.current_font_scale, True)
|
||||
args = (
|
||||
self.blocks,
|
||||
self.page_style,
|
||||
current_pos,
|
||||
self.current_font_scale,
|
||||
True)
|
||||
future = self.executor.submit(_render_page_worker, args)
|
||||
self.pending_renders[current_pos] = future
|
||||
|
||||
# We don't know the previous position yet, so we'll update it when the render completes
|
||||
|
||||
# We don't know the previous position yet, so we'll update it when the
|
||||
# render completes
|
||||
break
|
||||
|
||||
|
||||
def check_completed_renders(self):
|
||||
"""Check for completed background renders and cache the results"""
|
||||
if not self.pending_renders:
|
||||
return
|
||||
|
||||
|
||||
completed = []
|
||||
|
||||
|
||||
with self.render_lock:
|
||||
for position, future in self.pending_renders.items():
|
||||
if future.done():
|
||||
try:
|
||||
original_pos, pickled_page, next_pos = future.result()
|
||||
|
||||
|
||||
# Deserialize the page
|
||||
page = pickle.loads(pickled_page)
|
||||
|
||||
|
||||
# Cache the page
|
||||
self.cache_page(original_pos, page, next_pos, is_backward=False)
|
||||
|
||||
|
||||
completed.append(position)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"Background render failed for position {position}: {e}")
|
||||
completed.append(position)
|
||||
|
||||
|
||||
# Remove completed renders
|
||||
for pos in completed:
|
||||
self.pending_renders.pop(pos, None)
|
||||
|
||||
|
||||
def invalidate_all(self):
|
||||
"""Clear all cached pages and cancel pending renders"""
|
||||
with self.render_lock:
|
||||
@@ -249,24 +278,24 @@ class PageBuffer:
|
||||
for future in self.pending_renders.values():
|
||||
future.cancel()
|
||||
self.pending_renders.clear()
|
||||
|
||||
|
||||
# Clear caches
|
||||
self.forward_buffer.clear()
|
||||
self.backward_buffer.clear()
|
||||
self.position_map.clear()
|
||||
self.reverse_position_map.clear()
|
||||
|
||||
|
||||
def set_font_scale(self, font_scale: float):
|
||||
"""
|
||||
Update font scale and invalidate cache.
|
||||
|
||||
|
||||
Args:
|
||||
font_scale: New font scaling factor
|
||||
"""
|
||||
if font_scale != self.current_font_scale:
|
||||
self.current_font_scale = font_scale
|
||||
self.invalidate_all()
|
||||
|
||||
|
||||
def get_cache_stats(self) -> Dict[str, Any]:
|
||||
"""Get cache statistics for debugging/monitoring"""
|
||||
return {
|
||||
@@ -277,7 +306,7 @@ class PageBuffer:
|
||||
'reverse_position_mappings': len(self.reverse_position_map),
|
||||
'current_font_scale': self.current_font_scale
|
||||
}
|
||||
|
||||
|
||||
def shutdown(self):
|
||||
"""Shutdown the page buffer and clean up resources"""
|
||||
if self.executor:
|
||||
@@ -285,14 +314,14 @@ class PageBuffer:
|
||||
with self.render_lock:
|
||||
for future in self.pending_renders.values():
|
||||
future.cancel()
|
||||
|
||||
|
||||
# Shutdown executor
|
||||
self.executor.shutdown(wait=True)
|
||||
self.executor = None
|
||||
|
||||
|
||||
# Clear all caches
|
||||
self.invalidate_all()
|
||||
|
||||
|
||||
def __del__(self):
|
||||
"""Cleanup on destruction"""
|
||||
self.shutdown()
|
||||
@@ -302,11 +331,17 @@ class BufferedPageRenderer:
|
||||
"""
|
||||
High-level interface for buffered page rendering with automatic background caching.
|
||||
"""
|
||||
|
||||
def __init__(self, blocks: List[Block], page_style: PageStyle, buffer_size: int = 5, page_size: Tuple[int, int] = (800, 600)):
|
||||
|
||||
def __init__(self,
|
||||
blocks: List[Block],
|
||||
page_style: PageStyle,
|
||||
buffer_size: int = 5,
|
||||
page_size: Tuple[int,
|
||||
int] = (800,
|
||||
600)):
|
||||
"""
|
||||
Initialize the buffered renderer.
|
||||
|
||||
|
||||
Args:
|
||||
blocks: Document blocks to render
|
||||
page_style: Page styling configuration
|
||||
@@ -316,18 +351,19 @@ class BufferedPageRenderer:
|
||||
self.layouter = BidirectionalLayouter(blocks, page_style, page_size)
|
||||
self.buffer = PageBuffer(buffer_size)
|
||||
self.buffer.initialize(blocks, page_style)
|
||||
|
||||
|
||||
self.current_position = RenderingPosition()
|
||||
self.font_scale = 1.0
|
||||
|
||||
def render_page(self, position: RenderingPosition, font_scale: float = 1.0) -> Tuple[Page, RenderingPosition]:
|
||||
|
||||
def render_page(self, position: RenderingPosition,
|
||||
font_scale: float = 1.0) -> Tuple[Page, RenderingPosition]:
|
||||
"""
|
||||
Render a page with intelligent caching.
|
||||
|
||||
|
||||
Args:
|
||||
position: Position to render from
|
||||
font_scale: Font scaling factor
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (rendered_page, next_position)
|
||||
"""
|
||||
@@ -335,40 +371,43 @@ class BufferedPageRenderer:
|
||||
if font_scale != self.font_scale:
|
||||
self.font_scale = font_scale
|
||||
self.buffer.set_font_scale(font_scale)
|
||||
|
||||
|
||||
# Check cache first
|
||||
cached_page = self.buffer.get_page(position)
|
||||
if cached_page:
|
||||
# Get next position from position map
|
||||
next_pos = self.buffer.position_map.get(position, position)
|
||||
|
||||
|
||||
# Start background rendering for upcoming pages
|
||||
self.buffer.start_background_rendering(position, 'forward')
|
||||
|
||||
|
||||
return cached_page, next_pos
|
||||
|
||||
|
||||
# Render the page directly
|
||||
page, next_pos = self.layouter.render_page_forward(position, font_scale)
|
||||
|
||||
|
||||
# Cache the result
|
||||
self.buffer.cache_page(position, page, next_pos)
|
||||
|
||||
|
||||
# Start background rendering
|
||||
self.buffer.start_background_rendering(position, 'both')
|
||||
|
||||
|
||||
# Check for completed background renders
|
||||
self.buffer.check_completed_renders()
|
||||
|
||||
|
||||
return page, next_pos
|
||||
|
||||
def render_page_backward(self, end_position: RenderingPosition, font_scale: float = 1.0) -> Tuple[Page, RenderingPosition]:
|
||||
|
||||
def render_page_backward(self,
|
||||
end_position: RenderingPosition,
|
||||
font_scale: float = 1.0) -> Tuple[Page,
|
||||
RenderingPosition]:
|
||||
"""
|
||||
Render a page ending at the given position with intelligent caching.
|
||||
|
||||
|
||||
Args:
|
||||
end_position: Position where page should end
|
||||
font_scale: Font scaling factor
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (rendered_page, start_position)
|
||||
"""
|
||||
@@ -376,36 +415,36 @@ class BufferedPageRenderer:
|
||||
if font_scale != self.font_scale:
|
||||
self.font_scale = font_scale
|
||||
self.buffer.set_font_scale(font_scale)
|
||||
|
||||
|
||||
# Check cache first
|
||||
cached_page = self.buffer.get_page(end_position)
|
||||
if cached_page:
|
||||
# Get previous position from reverse position map
|
||||
prev_pos = self.buffer.reverse_position_map.get(end_position, end_position)
|
||||
|
||||
|
||||
# Start background rendering for previous pages
|
||||
self.buffer.start_background_rendering(end_position, 'backward')
|
||||
|
||||
|
||||
return cached_page, prev_pos
|
||||
|
||||
|
||||
# Render the page directly
|
||||
page, start_pos = self.layouter.render_page_backward(end_position, font_scale)
|
||||
|
||||
|
||||
# Cache the result
|
||||
self.buffer.cache_page(start_pos, page, end_position, is_backward=True)
|
||||
|
||||
|
||||
# Start background rendering
|
||||
self.buffer.start_background_rendering(end_position, 'both')
|
||||
|
||||
|
||||
# Check for completed background renders
|
||||
self.buffer.check_completed_renders()
|
||||
|
||||
|
||||
return page, start_pos
|
||||
|
||||
|
||||
def get_cache_stats(self) -> Dict[str, Any]:
|
||||
"""Get cache statistics"""
|
||||
return self.buffer.get_cache_stats()
|
||||
|
||||
|
||||
def shutdown(self):
|
||||
"""Shutdown the renderer and clean up resources"""
|
||||
self.buffer.shutdown()
|
||||
|
||||
Reference in New Issue
Block a user