auto flake and corrections

This commit is contained in:
2025-11-08 23:46:15 +01:00
parent 1ea870eef5
commit 781a9b6c08
81 changed files with 4646 additions and 3718 deletions
+99 -48
View File
@@ -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).
+155 -101
View File
@@ -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()
+114 -106
View File
@@ -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
"""
+137 -98
View File
@@ -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()