added fotn change API and examples
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user