197 lines
7.5 KiB
Python
197 lines
7.5 KiB
Python
from __future__ import annotations
|
|
|
|
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
|
|
|
|
def paragraph_layouter(paragraph: Paragraph, page: Page, start_word: int = 0, pretext: Optional[Text] = None) -> Tuple[bool, Optional[int], Optional[Text]]:
|
|
"""
|
|
Layout a paragraph of text within a given page.
|
|
|
|
This function extracts word spacing constraints from the style system
|
|
and uses them to create properly spaced lines of text.
|
|
|
|
Args:
|
|
paragraph: The paragraph to layout
|
|
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
|
|
|
|
Returns:
|
|
Tuple of:
|
|
- bool: True if paragraph was completely laid out, False if page ran out of space
|
|
- Optional[int]: Index of first word that didn't fit (if any)
|
|
- Optional[Text]: Remaining pretext if word was hyphenated (if any)
|
|
"""
|
|
if not paragraph.words:
|
|
return True, None, None
|
|
|
|
# Validate inputs
|
|
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)
|
|
|
|
# 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]:
|
|
"""Helper function to create a new line, returns None if page is full."""
|
|
if not page.can_fit_line(paragraph.line_height):
|
|
return None
|
|
|
|
y_cursor = page._current_y_offset
|
|
x_cursor = page.border_size
|
|
|
|
# Create a temporary Text object to calculate word width
|
|
if word:
|
|
temp_text = Text.from_word(word, page.draw)
|
|
word_width = temp_text.width
|
|
else:
|
|
word_width = 0
|
|
|
|
return Line(
|
|
spacing=word_spacing_constraints,
|
|
origin=(x_cursor, y_cursor),
|
|
size=(page.available_width, paragraph.line_height),
|
|
draw=page.draw,
|
|
font=concrete_style.create_font(),
|
|
halign=concrete_style.text_align
|
|
)
|
|
|
|
# Create initial line
|
|
current_line = create_new_line()
|
|
if not current_line:
|
|
return False, start_word, pretext
|
|
|
|
page.add_child(current_line)
|
|
page._current_y_offset += paragraph.line_height
|
|
|
|
# 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:
|
|
# Word fit successfully
|
|
if overflow_text is not None:
|
|
# If there's overflow text, we need to start a new line with it
|
|
current_pretext = overflow_text
|
|
current_line = create_new_line(overflow_text)
|
|
if not current_line:
|
|
# 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
|
|
# Continue to the next word
|
|
continue
|
|
else:
|
|
# No overflow, clear pretext
|
|
current_pretext = None
|
|
else:
|
|
# Word didn't fit, need a new line
|
|
current_line = create_new_line(word)
|
|
if not current_line:
|
|
# Page is full, return current position
|
|
return False, i, overflow_text
|
|
|
|
# Check if the word will fit on the new line before adding it
|
|
temp_text = Text.from_word(word, page.draw)
|
|
if temp_text.width > current_line.size[0]:
|
|
# 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()]
|
|
if len(splits) > 0:
|
|
# Use the first hyphenation point
|
|
first_part, second_part = splits[0]
|
|
current_line.add_word(word, first_part)
|
|
current_pretext = second_part
|
|
continue
|
|
|
|
page.add_child(current_line)
|
|
page._current_y_offset += paragraph.line_height
|
|
|
|
# Try to add the word to the new line
|
|
success, overflow_text = current_line.add_word(word, current_pretext)
|
|
|
|
if not success:
|
|
# Word still doesn't fit even on a new line
|
|
# This might happen with very long words or narrow pages
|
|
if overflow_text:
|
|
# Word was hyphenated, continue with the overflow
|
|
current_pretext = overflow_text
|
|
continue
|
|
else:
|
|
# Word cannot be broken, skip it or handle as error
|
|
# For now, we'll return indicating we couldn't process this word
|
|
return False, i, None
|
|
else:
|
|
current_pretext = overflow_text # May be None or hyphenated remainder
|
|
|
|
# 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
|