added fotn change API and examples
Python CI / test (3.10) (push) Successful in 2m18s
Python CI / test (3.12) (push) Successful in 2m8s
Python CI / test (3.13) (push) Successful in 2m6s

This commit is contained in:
2025-11-11 12:44:18 +01:00
parent 9de67d958e
commit 889f27e1a3
8 changed files with 732 additions and 29 deletions
+82 -17
View File
@@ -21,6 +21,7 @@ from pyWebLayout.concrete.page import Page
from pyWebLayout.concrete.text import Text
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.style import Font
from pyWebLayout.style.fonts import BundledFont, get_bundled_font_path, FontWeight, FontStyle
from pyWebLayout.layout.document_layouter import paragraph_layouter, image_layouter
@@ -181,32 +182,50 @@ class ChapterNavigator:
return self.chapters[0] if self.chapters else None
class FontScaler:
class FontFamilyOverride:
"""
Handles font scaling operations for ereader font size adjustments.
Applies scaling at layout/render time while preserving original font objects.
Manages font family preferences for ereader rendering.
Allows dynamic font family switching without modifying source blocks.
"""
@staticmethod
def scale_font(font: Font, scale_factor: float) -> Font:
def __init__(self, preferred_family: Optional[BundledFont] = None):
"""
Create a scaled version of a font for layout calculations.
Initialize font family override.
Args:
preferred_family: Preferred bundled font family (None = use original fonts)
"""
self.preferred_family = preferred_family
def override_font(self, font: Font) -> Font:
"""
Create a new font with the preferred family while preserving other attributes.
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
Font with overridden family, or original if no override is set
"""
if scale_factor == 1.0:
if self.preferred_family is None:
return font
scaled_size = max(1, int(font.font_size * scale_factor))
# Get the appropriate font path for the preferred family
# preserving the original font's weight and style
new_font_path = get_bundled_font_path(
family=self.preferred_family,
weight=font.weight,
style=font.style
)
# If we couldn't find a matching font, fall back to original
if new_font_path is None:
return font
# Create a new font with the overridden path
return Font(
font_path=font._font_path,
font_size=scaled_size,
font_path=new_font_path,
font_size=font.font_size,
colour=font.colour,
weight=font.weight,
style=font.style,
@@ -216,6 +235,49 @@ class FontScaler:
min_hyphenation_width=font.min_hyphenation_width
)
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, family_override: Optional[FontFamilyOverride] = None) -> 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.)
family_override: Optional font family override
Returns:
New Font object with scaled size and optional family override
"""
# Apply family override first if specified
working_font = font
if family_override is not None:
working_font = family_override.override_font(font)
# Then apply scaling
if scale_factor == 1.0:
return working_font
scaled_size = max(1, int(working_font.font_size * scale_factor))
return Font(
font_path=working_font._font_path,
font_size=scaled_size,
colour=working_font.colour,
weight=working_font.weight,
style=working_font.style,
decoration=working_font.decoration,
background=working_font.background,
language=working_font.language,
min_hyphenation_width=working_font.min_hyphenation_width
)
@staticmethod
def scale_word_spacing(spacing: Tuple[int, int],
scale_factor: float) -> Tuple[int, int]:
@@ -242,12 +304,14 @@ class BidirectionalLayouter:
page_size: Tuple[int,
int] = (800,
600),
alignment_override=None):
alignment_override=None,
font_family_override: Optional[FontFamilyOverride] = None):
self.blocks = blocks
self.page_style = page_style
self.page_size = page_size
self.chapter_navigator = ChapterNavigator(blocks)
self.alignment_override = alignment_override
self.font_family_override = font_family_override
def render_page_forward(self, position: RenderingPosition,
font_scale: float = 1.0) -> Tuple[Page, RenderingPosition]:
@@ -401,14 +465,15 @@ class BidirectionalLayouter:
return final_page, final_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:
"""Apply font scaling and font family override to all fonts in a block"""
# Check if we need to do any transformation
if font_scale == 1.0 and self.font_family_override is None:
return block
# This is a simplified implementation
# In practice, we'd need to handle each block type appropriately
if isinstance(block, (Paragraph, Heading)):
scaled_block_style = FontScaler.scale_font(block.style, font_scale)
scaled_block_style = FontScaler.scale_font(block.style, font_scale, self.font_family_override)
if isinstance(block, Heading):
scaled_block = Heading(block.level, scaled_block_style)
else:
@@ -419,7 +484,7 @@ class BidirectionalLayouter:
if isinstance(word, Word):
scaled_word = Word(
word.text, FontScaler.scale_font(
word.style, font_scale))
word.style, font_scale, self.font_family_override))
scaled_block.add_word(scaled_word)
return scaled_block
+41
View File
@@ -17,6 +17,7 @@ from pyWebLayout.abstract.block import Block, HeadingLevel, Image, BlockType
from pyWebLayout.concrete.page import Page
from pyWebLayout.concrete.image import RenderableImage
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.style.fonts import BundledFont
from pyWebLayout.layout.document_layouter import image_layouter
@@ -154,6 +155,7 @@ class EreaderLayoutManager:
Features:
- Sub-second page rendering with intelligent buffering
- Font scaling support
- Dynamic font family switching (Sans, Serif, Monospace)
- Chapter navigation
- Bookmark management
- Position persistence
@@ -550,6 +552,43 @@ class EreaderLayoutManager:
"""Get the current font scale"""
return self.font_scale
def set_font_family(self, family: Optional[BundledFont]) -> Page:
"""
Change the font family and re-render current page.
Switches all text in the document to use the specified bundled font family
while preserving font weights, styles, sizes, and other attributes.
Clears page history and cache since font changes invalidate all cached positions.
Args:
family: Bundled font family to use (SANS, SERIF, MONOSPACE, or None for original fonts)
Returns:
Re-rendered page with new font family
Example:
>>> from pyWebLayout.style.fonts import BundledFont
>>> manager.set_font_family(BundledFont.SERIF) # Switch to serif
>>> manager.set_font_family(BundledFont.SANS) # Switch to sans
>>> manager.set_font_family(None) # Restore original fonts
"""
# Update the renderer's font family
self.renderer.set_font_family(family)
# Clear history since font changes invalidate all cached positions
self._clear_history()
return self.get_current_page()
def get_font_family(self) -> Optional[BundledFont]:
"""
Get the current font family override.
Returns:
Current font family (SANS, SERIF, MONOSPACE) or None if using original fonts
"""
return self.renderer.get_font_family()
def increase_line_spacing(self, amount: int = 2) -> Page:
"""
Increase line spacing and re-render current page.
@@ -787,6 +826,7 @@ class EreaderLayoutManager:
Dictionary with position details
"""
current_chapter = self.get_current_chapter()
font_family = self.get_font_family()
return {
'position': self.current_position.to_dict(),
@@ -799,6 +839,7 @@ class EreaderLayoutManager:
},
'progress': self.get_reading_progress(),
'font_scale': self.font_scale,
'font_family': font_family.value if font_family else None,
'page_size': self.page_size
}
+70 -12
View File
@@ -12,31 +12,36 @@ from concurrent.futures import ProcessPoolExecutor, Future
import threading
import pickle
from .ereader_layout import RenderingPosition, BidirectionalLayouter
from .ereader_layout import RenderingPosition, BidirectionalLayouter, FontFamilyOverride
from pyWebLayout.concrete.page import Page
from pyWebLayout.abstract.block import Block
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.style.fonts import BundledFont
def _render_page_worker(args: Tuple[List[Block],
PageStyle,
RenderingPosition,
float,
bool]) -> Tuple[RenderingPosition,
bool,
Optional[BundledFont]]) -> Tuple[RenderingPosition,
bytes,
RenderingPosition]:
"""
Worker function for multiprocess page rendering.
Args:
args: Tuple of (blocks, page_style, position, font_scale, is_backward)
args: Tuple of (blocks, page_style, position, font_scale, is_backward, font_family)
Returns:
Tuple of (original_position, pickled_page, next_position)
"""
blocks, page_style, position, font_scale, is_backward = args
blocks, page_style, position, font_scale, is_backward, font_family = args
layouter = BidirectionalLayouter(blocks, page_style)
# Create font family override if specified
font_family_override = FontFamilyOverride(font_family) if font_family else None
layouter = BidirectionalLayouter(blocks, page_style, font_family_override=font_family_override)
if is_backward:
page, next_pos = layouter.render_page_backward(position, font_scale)
@@ -85,12 +90,14 @@ class PageBuffer:
self.blocks: Optional[List[Block]] = None
self.page_style: Optional[PageStyle] = None
self.current_font_scale: float = 1.0
self.current_font_family: Optional[BundledFont] = None
def initialize(
self,
blocks: List[Block],
page_style: PageStyle,
font_scale: float = 1.0):
font_scale: float = 1.0,
font_family: Optional[BundledFont] = None):
"""
Initialize the buffer with document blocks and page style.
@@ -98,10 +105,12 @@ class PageBuffer:
blocks: Document blocks to render
page_style: Page styling configuration
font_scale: Current font scaling factor
font_family: Optional font family override
"""
self.blocks = blocks
self.page_style = page_style
self.current_font_scale = font_scale
self.current_font_family = font_family
# Start the process pool
if self.executor is None:
@@ -207,7 +216,8 @@ class PageBuffer:
self.page_style,
current_pos,
self.current_font_scale,
False)
False,
self.current_font_family)
future = self.executor.submit(_render_page_worker, args)
self.pending_renders[current_pos] = future
@@ -234,7 +244,8 @@ class PageBuffer:
self.page_style,
current_pos,
self.current_font_scale,
True)
True,
self.current_font_family)
future = self.executor.submit(_render_page_worker, args)
self.pending_renders[current_pos] = future
@@ -296,6 +307,17 @@ class PageBuffer:
self.current_font_scale = font_scale
self.invalidate_all()
def set_font_family(self, font_family: Optional[BundledFont]):
"""
Update font family and invalidate cache.
Args:
font_family: New font family (None = use original fonts)
"""
if font_family != self.current_font_family:
self.current_font_family = font_family
self.invalidate_all()
def get_cache_stats(self) -> Dict[str, Any]:
"""Get cache statistics for debugging/monitoring"""
return {
@@ -304,7 +326,8 @@ class PageBuffer:
'pending_renders': len(self.pending_renders),
'position_mappings': len(self.position_map),
'reverse_position_mappings': len(self.reverse_position_map),
'current_font_scale': self.current_font_scale
'current_font_scale': self.current_font_scale,
'current_font_family': self.current_font_family.value if self.current_font_family else None
}
def shutdown(self):
@@ -338,7 +361,8 @@ class BufferedPageRenderer:
buffer_size: int = 5,
page_size: Tuple[int,
int] = (800,
600)):
600),
font_family: Optional[BundledFont] = None):
"""
Initialize the buffered renderer.
@@ -347,13 +371,21 @@ class BufferedPageRenderer:
page_style: Page styling configuration
buffer_size: Number of pages to cache in each direction
page_size: Page size (width, height) in pixels
font_family: Optional font family override
"""
self.layouter = BidirectionalLayouter(blocks, page_style, page_size)
# Create font family override if specified
font_family_override = FontFamilyOverride(font_family) if font_family else None
self.layouter = BidirectionalLayouter(blocks, page_style, page_size, font_family_override=font_family_override)
self.buffer = PageBuffer(buffer_size)
self.buffer.initialize(blocks, page_style)
self.buffer.initialize(blocks, page_style, font_family=font_family)
self.page_size = page_size
self.blocks = blocks
self.page_style = page_style
self.current_position = RenderingPosition()
self.font_scale = 1.0
self.font_family = font_family
def render_page(self, position: RenderingPosition,
font_scale: float = 1.0) -> Tuple[Page, RenderingPosition]:
@@ -453,6 +485,32 @@ class BufferedPageRenderer:
return page, start_pos
def set_font_family(self, font_family: Optional[BundledFont]):
"""
Change the font family and invalidate cache.
Args:
font_family: New font family (None = use original fonts)
"""
if font_family != self.font_family:
self.font_family = font_family
# Update buffer
self.buffer.set_font_family(font_family)
# Recreate layouter with new font family override
font_family_override = FontFamilyOverride(font_family) if font_family else None
self.layouter = BidirectionalLayouter(
self.blocks,
self.page_style,
self.page_size,
font_family_override=font_family_override
)
def get_font_family(self) -> Optional[BundledFont]:
"""Get the current font family override"""
return self.font_family
def get_cache_stats(self) -> Dict[str, Any]:
"""Get cache statistics"""
return self.buffer.get_cache_stats()