Working version for ebook rendering!!
This commit is contained in:
@@ -4,9 +4,9 @@ from typing import List, Tuple, Optional, Union
|
||||
|
||||
from pyWebLayout.concrete import Page, Line, Text
|
||||
from pyWebLayout.abstract import Paragraph, Word, Link
|
||||
from pyWebLayout.style.concrete_style import ConcreteStyleRegistry
|
||||
from pyWebLayout.style.concrete_style import ConcreteStyleRegistry, RenderingContext, StyleResolver
|
||||
|
||||
def paragraph_layouter(paragraph: Paragraph, page: Page, start_word: int = 0, pretext: Optional[Text] = 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.
|
||||
|
||||
@@ -18,6 +18,7 @@ def paragraph_layouter(paragraph: Paragraph, page: Page, start_word: int = 0, pr
|
||||
page: The page to layout the paragraph on
|
||||
start_word: Index of the first word to process (for continuation)
|
||||
pretext: Optional pretext from a previous hyphenated word
|
||||
alignment_override: Optional alignment to override the paragraph's default alignment
|
||||
|
||||
Returns:
|
||||
Tuple of:
|
||||
@@ -32,22 +33,71 @@ def paragraph_layouter(paragraph: Paragraph, page: Page, start_word: int = 0, pr
|
||||
if start_word >= len(paragraph.words):
|
||||
return True, None, None
|
||||
|
||||
# Get the concrete style with resolved word spacing constraints
|
||||
style_registry = ConcreteStyleRegistry(page.style_resolver)
|
||||
concrete_style = style_registry.get_concrete_style(paragraph.style)
|
||||
# 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
|
||||
from pyWebLayout.style import Font, Alignment
|
||||
|
||||
if isinstance(paragraph.style, Font):
|
||||
# paragraph.style is already a Font (concrete style)
|
||||
font = paragraph.style
|
||||
# Use default word spacing constraints based on font size
|
||||
# Minimum spacing should be proportional to font size for better readability
|
||||
min_spacing = float(font.font_size) * 0.25 # 25% of font size
|
||||
max_spacing = float(font.font_size) * 0.5 # 50% of font size
|
||||
word_spacing_constraints = (int(min_spacing), int(max_spacing))
|
||||
text_align = Alignment.LEFT # Default alignment
|
||||
else:
|
||||
# paragraph.style is an AbstractStyle, resolve it
|
||||
rendering_context = RenderingContext(base_font_size=paragraph.style.font_size)
|
||||
style_resolver = StyleResolver(rendering_context)
|
||||
style_registry = ConcreteStyleRegistry(style_resolver)
|
||||
concrete_style = style_registry.get_concrete_style(paragraph.style)
|
||||
font = concrete_style.create_font()
|
||||
word_spacing_constraints = (
|
||||
int(concrete_style.word_spacing_min),
|
||||
int(concrete_style.word_spacing_max)
|
||||
)
|
||||
text_align = concrete_style.text_align
|
||||
|
||||
# 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:
|
||||
from pyWebLayout.style import Font
|
||||
font = Font(
|
||||
font_path=font._font_path,
|
||||
font_size=page.style.max_font_size,
|
||||
colour=font.colour,
|
||||
weight=font.weight,
|
||||
style=font.style,
|
||||
decoration=font.decoration,
|
||||
background=font.background
|
||||
)
|
||||
|
||||
# Calculate baseline-to-baseline spacing using line spacing multiplier
|
||||
# This is the vertical distance between baselines of consecutive lines
|
||||
baseline_spacing = int(font.font_size * page.style.line_spacing_multiplier)
|
||||
|
||||
# Get font metrics for boundary checking
|
||||
ascent, descent = font.font.getmetrics()
|
||||
|
||||
# Extract word spacing constraints (min, max) for Line constructor
|
||||
word_spacing_constraints = (
|
||||
int(concrete_style.word_spacing_min),
|
||||
int(concrete_style.word_spacing_max)
|
||||
)
|
||||
|
||||
def create_new_line(word: Optional[Union[Word, Text]] = None) -> 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."""
|
||||
if not page.can_fit_line(paragraph.line_height):
|
||||
# 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
|
||||
|
||||
y_cursor = page._current_y_offset
|
||||
# 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
|
||||
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
|
||||
y_cursor = page._current_y_offset
|
||||
else:
|
||||
y_cursor = page._current_y_offset
|
||||
x_cursor = page.border_size
|
||||
|
||||
# Create a temporary Text object to calculate word width
|
||||
@@ -60,10 +110,10 @@ def paragraph_layouter(paragraph: Paragraph, page: Page, start_word: int = 0, pr
|
||||
return Line(
|
||||
spacing=word_spacing_constraints,
|
||||
origin=(x_cursor, y_cursor),
|
||||
size=(page.available_width, paragraph.line_height),
|
||||
size=(page.available_width, baseline_spacing),
|
||||
draw=page.draw,
|
||||
font=concrete_style.create_font(),
|
||||
halign=concrete_style.text_align
|
||||
font=font,
|
||||
halign=text_align
|
||||
)
|
||||
|
||||
# Create initial line
|
||||
@@ -72,15 +122,14 @@ def paragraph_layouter(paragraph: Paragraph, page: Page, start_word: int = 0, pr
|
||||
return False, start_word, pretext
|
||||
|
||||
page.add_child(current_line)
|
||||
page._current_y_offset += paragraph.line_height
|
||||
# Note: add_child already updates _current_y_offset based on child's origin and size
|
||||
# No need to manually increment it here
|
||||
|
||||
# Track current position in paragraph
|
||||
current_pretext = pretext
|
||||
|
||||
# Process words starting from start_word
|
||||
for i, word in enumerate(paragraph.words[start_word:], start=start_word):
|
||||
if current_pretext:
|
||||
print(current_pretext.text)
|
||||
success, overflow_text = current_line.add_word(word, current_pretext)
|
||||
|
||||
if success:
|
||||
@@ -93,7 +142,7 @@ def paragraph_layouter(paragraph: Paragraph, page: Page, start_word: int = 0, pr
|
||||
# If we can't create a new line, return with the current state
|
||||
return False, i, overflow_text
|
||||
page.add_child(current_line)
|
||||
page._current_y_offset += paragraph.line_height
|
||||
# Note: add_child already updates _current_y_offset
|
||||
# Continue to the next word
|
||||
continue
|
||||
else:
|
||||
@@ -121,7 +170,7 @@ def paragraph_layouter(paragraph: Paragraph, page: Page, start_word: int = 0, pr
|
||||
continue
|
||||
|
||||
page.add_child(current_line)
|
||||
page._current_y_offset += paragraph.line_height
|
||||
# Note: add_child already updates _current_y_offset
|
||||
|
||||
# Try to add the word to the new line
|
||||
success, overflow_text = current_line.add_word(word, current_pretext)
|
||||
@@ -142,55 +191,3 @@ def paragraph_layouter(paragraph: Paragraph, page: Page, start_word: int = 0, pr
|
||||
|
||||
# All words processed successfully
|
||||
return True, None, None
|
||||
|
||||
class DocumentLayouter:
|
||||
"""
|
||||
Class-based document layouter for more complex layout operations.
|
||||
"""
|
||||
|
||||
def __init__(self, page: Page):
|
||||
"""Initialize the layouter with a page."""
|
||||
self.page = page
|
||||
self.style_registry = ConcreteStyleRegistry(page.style_resolver)
|
||||
|
||||
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 class-based approach.
|
||||
|
||||
This method provides the same functionality as the standalone function
|
||||
but with better state management and reusability.
|
||||
"""
|
||||
return paragraph_layouter(paragraph, self.page, start_word, pretext)
|
||||
|
||||
def layout_document(self, paragraphs: List[Paragraph]) -> bool:
|
||||
"""
|
||||
Layout multiple paragraphs in sequence.
|
||||
|
||||
Args:
|
||||
paragraphs: List of paragraphs to layout
|
||||
|
||||
Returns:
|
||||
True if all paragraphs were laid out successfully, False otherwise
|
||||
"""
|
||||
for paragraph in paragraphs:
|
||||
start_word = 0
|
||||
pretext = None
|
||||
|
||||
while True:
|
||||
complete, next_word, remaining_pretext = self.layout_paragraph(
|
||||
paragraph, start_word, pretext
|
||||
)
|
||||
|
||||
if complete:
|
||||
# Paragraph finished
|
||||
break
|
||||
|
||||
if next_word is None:
|
||||
# Error condition
|
||||
return False
|
||||
|
||||
# Continue on next page or handle page break
|
||||
# For now, we'll just return False indicating we need more space
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -27,6 +27,7 @@ from pyWebLayout.concrete.page import Page
|
||||
from pyWebLayout.concrete.text import Line, Text
|
||||
from pyWebLayout.style.page_style import PageStyle
|
||||
from pyWebLayout.style import Font
|
||||
from pyWebLayout.layout.document_layouter import paragraph_layouter
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -212,11 +213,12 @@ class BidirectionalLayouter:
|
||||
Handles font scaling and maintains position state.
|
||||
"""
|
||||
|
||||
def __init__(self, blocks: List[Block], page_style: PageStyle, page_size: Tuple[int, int] = (800, 600)):
|
||||
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]:
|
||||
"""
|
||||
@@ -328,54 +330,66 @@ class BidirectionalLayouter:
|
||||
return True, new_pos
|
||||
|
||||
def _layout_paragraph_on_page(self, paragraph: Paragraph, page: Page, position: RenderingPosition, font_scale: float) -> Tuple[bool, RenderingPosition]:
|
||||
"""Layout a paragraph on the page with font scaling support"""
|
||||
# This would integrate with the existing paragraph_layouter but with font scaling
|
||||
# For now, this is a placeholder implementation
|
||||
"""
|
||||
Layout a paragraph on the page using the core paragraph_layouter.
|
||||
Integrates font scaling and position tracking with the proven layout logic.
|
||||
|
||||
# Calculate scaled line height
|
||||
line_height = int(paragraph.style.font_size * font_scale * 1.2) # 1.2 is line spacing factor
|
||||
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)
|
||||
"""
|
||||
# Convert remaining_pretext from string to Text object if needed
|
||||
pretext_obj = None
|
||||
if position.remaining_pretext:
|
||||
# Create a Text object from the pretext string
|
||||
pretext_obj = Text(
|
||||
position.remaining_pretext,
|
||||
paragraph.style,
|
||||
page.draw,
|
||||
line=None,
|
||||
source=None
|
||||
)
|
||||
|
||||
if not page.can_fit_line(line_height):
|
||||
return False, position
|
||||
|
||||
# Create a line and try to fit words
|
||||
y_cursor = page._current_y_offset
|
||||
x_cursor = page.border_size
|
||||
|
||||
# Scale word spacing constraints
|
||||
word_spacing = FontScaler.scale_word_spacing((5, 15), font_scale) # Default spacing
|
||||
|
||||
line = Line(
|
||||
spacing=word_spacing,
|
||||
origin=(x_cursor, y_cursor),
|
||||
size=(page.available_width, line_height),
|
||||
draw=page.draw,
|
||||
font=FontScaler.scale_font(paragraph.style, font_scale)
|
||||
# Call the core paragraph layouter with alignment override if set
|
||||
success, failed_word_index, remaining_pretext = paragraph_layouter(
|
||||
paragraph,
|
||||
page,
|
||||
start_word=position.word_index,
|
||||
pretext=pretext_obj,
|
||||
alignment_override=self.alignment_override
|
||||
)
|
||||
|
||||
# Add words starting from position.word_index
|
||||
words_added = 0
|
||||
for i, word in enumerate(paragraph.words[position.word_index:], start=position.word_index):
|
||||
success, overflow = line.add_word(word)
|
||||
if not success:
|
||||
break
|
||||
words_added += 1
|
||||
# Create new position based on the result
|
||||
new_pos = position.copy()
|
||||
|
||||
if words_added > 0:
|
||||
page.add_child(line)
|
||||
page._current_y_offset += line_height
|
||||
|
||||
new_pos = position.copy()
|
||||
new_pos.word_index += words_added
|
||||
|
||||
# If we finished the paragraph, move to next block
|
||||
if new_pos.word_index >= len(paragraph.words):
|
||||
new_pos.block_index += 1
|
||||
new_pos.word_index = 0
|
||||
|
||||
if success:
|
||||
# Paragraph was fully laid out, move to next block
|
||||
new_pos.block_index += 1
|
||||
new_pos.word_index = 0
|
||||
new_pos.remaining_pretext = None
|
||||
return True, new_pos
|
||||
|
||||
return False, position
|
||||
else:
|
||||
# Paragraph was not fully laid out
|
||||
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]:
|
||||
"""Layout a heading on the page"""
|
||||
|
||||
Reference in New Issue
Block a user