added document layouter
Python CI / test (push) Failing after 4m51s

This commit is contained in:
2025-06-28 22:02:39 +02:00
parent 56a6ec19e8
commit b1c4a1c125
14 changed files with 1279 additions and 9 deletions
+11
View File
@@ -0,0 +1,11 @@
"""
Typesetting module for the pyWebLayout library.
This package handles the organization and arrangement of elements for rendering, including:
- Flow layout algorithms
- Container management
- Element positioning and sizing
- Content wrapping and overflow
- Coordinate systems and transformations
- Pagination for book-like content
"""
+162
View File
@@ -0,0 +1,162 @@
from __future__ import annotations
from typing import List, Tuple, Optional
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() -> 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
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):
success, overflow_text = current_line.add_word(word, current_pretext)
if success:
# Word fit successfully
current_pretext = None # Clear pretext after successful placement
else:
# Word didn't fit, need a new line
current_line = create_new_line()
if not current_line:
# Page is full, return current position
return False, i, overflow_text
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