706 lines
26 KiB
Python
706 lines
26 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import List, Tuple, Optional, Union
|
|
import numpy as np
|
|
|
|
from pyWebLayout.concrete import Page, Line, Text
|
|
from pyWebLayout.concrete.image import RenderableImage
|
|
from pyWebLayout.concrete.functional import ButtonText, FormFieldText
|
|
from pyWebLayout.concrete.table import TableRenderer, TableStyle
|
|
from pyWebLayout.abstract import Paragraph, Word
|
|
from pyWebLayout.abstract.block import Image as AbstractImage, PageBreak, Table
|
|
from pyWebLayout.abstract.functional import Button, Form, FormField
|
|
from pyWebLayout.style.concrete_style import ConcreteStyleRegistry, RenderingContext, StyleResolver
|
|
from pyWebLayout.style import Font, Alignment
|
|
|
|
|
|
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.
|
|
|
|
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
|
|
alignment_override: Optional alignment to override the paragraph's default alignment
|
|
|
|
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
|
|
|
|
# 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
|
|
|
|
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
|
|
# Ensure font_size is an int (it could be a FontSize enum)
|
|
from pyWebLayout.style.abstract_style import FontSize
|
|
if isinstance(paragraph.style.font_size, FontSize):
|
|
# Use a default base font size, the resolver will handle the semantic size
|
|
base_font_size = 16
|
|
else:
|
|
base_font_size = int(paragraph.style.font_size)
|
|
|
|
rendering_context = RenderingContext(base_font_size=base_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 page-level word spacing override if specified
|
|
if hasattr(
|
|
page.style,
|
|
'word_spacing') and isinstance(
|
|
page.style.word_spacing,
|
|
int) and page.style.word_spacing > 0:
|
|
# Add the page-level word spacing to both min and max constraints
|
|
min_ws, max_ws = word_spacing_constraints
|
|
word_spacing_constraints = (
|
|
min_ws + page.style.word_spacing,
|
|
max_ws + page.style.word_spacing
|
|
)
|
|
|
|
# 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:
|
|
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: font size + additional line spacing
|
|
# This is the vertical distance between baselines of consecutive lines
|
|
# Formula: baseline_spacing = font_size + line_spacing (absolute pixels)
|
|
line_spacing_value = getattr(page.style, 'line_spacing', 5)
|
|
# Ensure line_spacing is an int (could be Mock in tests)
|
|
if not isinstance(line_spacing_value, int):
|
|
line_spacing_value = 5
|
|
baseline_spacing = font.font_size + line_spacing_value
|
|
|
|
# Get font metrics for boundary checking
|
|
ascent, descent = font.font.getmetrics()
|
|
|
|
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."""
|
|
# 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
|
|
|
|
# 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
|
|
if word:
|
|
temp_text = Text.from_word(word, page.draw)
|
|
temp_text.width
|
|
else:
|
|
pass
|
|
|
|
return Line(
|
|
spacing=word_spacing_constraints,
|
|
origin=(x_cursor, y_cursor),
|
|
size=(page.available_width, baseline_spacing),
|
|
draw=page.draw,
|
|
font=font,
|
|
halign=text_align
|
|
)
|
|
|
|
# Create initial line
|
|
current_line = create_new_line()
|
|
if not current_line:
|
|
return False, start_word, pretext
|
|
|
|
page.add_child(current_line)
|
|
# 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):
|
|
# Check if this is a LinkedWord and needs special handling in concrete layer
|
|
# Note: The Line.add_word method will create Text objects internally,
|
|
# but we may want to create LinkText for LinkedWord instances in future
|
|
# For now, the abstract layer (LinkedWord) carries the link info,
|
|
# and the concrete layer (LinkText) would be created during rendering
|
|
|
|
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)
|
|
# Note: add_child already updates _current_y_offset
|
|
# 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)
|
|
# 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)
|
|
|
|
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
|
|
|
|
|
|
def pagebreak_layouter(page_break: PageBreak, page: Page) -> bool:
|
|
"""
|
|
Handle a page break element.
|
|
|
|
A page break signals that all subsequent content should start on a new page.
|
|
This function always returns False to indicate that the current page is complete
|
|
and a new page should be created for subsequent content.
|
|
|
|
Args:
|
|
page_break: The PageBreak block
|
|
page: The current page (not used, but kept for consistency)
|
|
|
|
Returns:
|
|
bool: Always False to force creation of a new page
|
|
"""
|
|
# Page break always forces a new page
|
|
return False
|
|
|
|
|
|
def image_layouter(image: AbstractImage, page: Page, max_width: Optional[int] = None,
|
|
max_height: Optional[int] = None) -> bool:
|
|
"""
|
|
Layout an image within a given page.
|
|
|
|
This function places an image on the page, respecting size constraints
|
|
and available space. Images are centered horizontally by default.
|
|
|
|
Args:
|
|
image: The abstract Image object to layout
|
|
page: The page to layout the image on
|
|
max_width: Maximum width constraint (defaults to page available width)
|
|
max_height: Maximum height constraint (defaults to remaining page height)
|
|
|
|
Returns:
|
|
bool: True if image was successfully laid out, False if page ran out of space
|
|
"""
|
|
# Use page available width if max_width not specified
|
|
if max_width is None:
|
|
max_width = page.available_width
|
|
|
|
# Calculate available height on page
|
|
available_height = page.size[1] - page._current_y_offset - page.border_size
|
|
if max_height is None:
|
|
max_height = available_height
|
|
else:
|
|
max_height = min(max_height, available_height)
|
|
|
|
# Calculate scaled dimensions
|
|
scaled_width, scaled_height = image.calculate_scaled_dimensions(
|
|
max_width, max_height)
|
|
|
|
# Check if image fits on current page
|
|
if scaled_height is None or scaled_height > available_height:
|
|
return False
|
|
|
|
# Create renderable image
|
|
x_offset = page.border_size
|
|
y_offset = page._current_y_offset
|
|
|
|
# Access page.draw to ensure canvas is initialized
|
|
_ = page.draw
|
|
|
|
renderable_image = RenderableImage(
|
|
image=image,
|
|
canvas=page._canvas,
|
|
max_width=max_width,
|
|
max_height=max_height,
|
|
origin=(x_offset, y_offset),
|
|
size=(scaled_width or max_width, scaled_height or max_height),
|
|
halign=Alignment.CENTER,
|
|
valign=Alignment.TOP
|
|
)
|
|
|
|
# Add to page
|
|
page.add_child(renderable_image)
|
|
|
|
return True
|
|
|
|
|
|
def table_layouter(
|
|
table: Table,
|
|
page: Page,
|
|
style: Optional[TableStyle] = None) -> bool:
|
|
"""
|
|
Layout a table within a given page.
|
|
|
|
This function uses the TableRenderer to render the table at the current
|
|
page position, advancing the page's y-offset after successful rendering.
|
|
|
|
Args:
|
|
table: The abstract Table object to layout
|
|
page: The page to layout the table on
|
|
style: Optional table styling configuration
|
|
|
|
Returns:
|
|
bool: True if table was successfully laid out, False if page ran out of space
|
|
"""
|
|
# Calculate available space
|
|
available_width = page.available_width
|
|
x_offset = page.border_size
|
|
y_offset = page._current_y_offset
|
|
|
|
# Access page.draw to ensure canvas is initialized
|
|
draw = page.draw
|
|
canvas = page._canvas
|
|
|
|
# Create table renderer
|
|
origin = (x_offset, y_offset)
|
|
renderer = TableRenderer(
|
|
table=table,
|
|
origin=origin,
|
|
available_width=available_width,
|
|
draw=draw,
|
|
style=style,
|
|
canvas=canvas
|
|
)
|
|
|
|
# Check if table fits on current page
|
|
table_height = renderer.size[1]
|
|
available_height = page.size[1] - y_offset - page.border_size
|
|
|
|
if table_height > available_height:
|
|
return False
|
|
|
|
# Render the table
|
|
renderer.render()
|
|
|
|
# Update page y-offset
|
|
page._current_y_offset = y_offset + table_height
|
|
|
|
return True
|
|
|
|
|
|
def button_layouter(button: Button,
|
|
page: Page,
|
|
font: Optional[Font] = None,
|
|
padding: Tuple[int,
|
|
int,
|
|
int,
|
|
int] = (4,
|
|
8,
|
|
4,
|
|
8)) -> Tuple[bool,
|
|
str]:
|
|
"""
|
|
Layout a button within a given page and register it for callback binding.
|
|
|
|
This function creates a ButtonText renderable, positions it on the page,
|
|
and registers it in the page's callback registry using the button's html_id
|
|
(if available) or an auto-generated id.
|
|
|
|
Args:
|
|
button: The abstract Button object to layout
|
|
page: The page to layout the button on
|
|
font: Optional font for button text (defaults to page default)
|
|
padding: Padding around button text (top, right, bottom, left)
|
|
|
|
Returns:
|
|
Tuple of:
|
|
- bool: True if button was successfully laid out, False if page ran out of space
|
|
- str: The id used to register the button in the callback registry
|
|
"""
|
|
# Use provided font or create a default one
|
|
if font is None:
|
|
font = Font(font_size=14, colour=(255, 255, 255))
|
|
|
|
# Calculate available space
|
|
available_height = page.size[1] - page._current_y_offset - page.border_size
|
|
|
|
# Create ButtonText renderable
|
|
button_text = ButtonText(button, font, page.draw, padding=padding)
|
|
|
|
# Check if button fits on current page
|
|
button_height = button_text.size[1]
|
|
if button_height > available_height:
|
|
return False, ""
|
|
|
|
# Position the button
|
|
x_offset = page.border_size
|
|
y_offset = page._current_y_offset
|
|
|
|
button_text.set_origin(np.array([x_offset, y_offset]))
|
|
|
|
# Register in callback registry
|
|
html_id = button.html_id
|
|
registered_id = page.callbacks.register(button_text, html_id=html_id)
|
|
|
|
# Add to page
|
|
page.add_child(button_text)
|
|
|
|
return True, registered_id
|
|
|
|
|
|
def form_field_layouter(field: FormField, page: Page, font: Optional[Font] = None,
|
|
field_height: int = 24) -> Tuple[bool, str]:
|
|
"""
|
|
Layout a form field within a given page and register it for callback binding.
|
|
|
|
This function creates a FormFieldText renderable, positions it on the page,
|
|
and registers it in the page's callback registry.
|
|
|
|
Args:
|
|
field: The abstract FormField object to layout
|
|
page: The page to layout the field on
|
|
font: Optional font for field label (defaults to page default)
|
|
field_height: Height of the input field area
|
|
|
|
Returns:
|
|
Tuple of:
|
|
- bool: True if field was successfully laid out, False if page ran out of space
|
|
- str: The id used to register the field in the callback registry
|
|
"""
|
|
# Use provided font or create a default one
|
|
if font is None:
|
|
font = Font(font_size=12, colour=(0, 0, 0))
|
|
|
|
# Calculate available space
|
|
available_height = page.size[1] - page._current_y_offset - page.border_size
|
|
|
|
# Create FormFieldText renderable
|
|
field_text = FormFieldText(field, font, page.draw, field_height=field_height)
|
|
|
|
# Check if field fits on current page
|
|
total_field_height = field_text.size[1]
|
|
if total_field_height > available_height:
|
|
return False, ""
|
|
|
|
# Position the field
|
|
x_offset = page.border_size
|
|
y_offset = page._current_y_offset
|
|
|
|
field_text.set_origin(np.array([x_offset, y_offset]))
|
|
|
|
# Register in callback registry (use field name as html_id fallback)
|
|
html_id = getattr(field, '_html_id', None) or field.name
|
|
registered_id = page.callbacks.register(field_text, html_id=html_id)
|
|
|
|
# Add to page
|
|
page.add_child(field_text)
|
|
|
|
return True, registered_id
|
|
|
|
|
|
def form_layouter(form: Form, page: Page, font: Optional[Font] = None,
|
|
field_spacing: int = 10) -> Tuple[bool, List[str]]:
|
|
"""
|
|
Layout a complete form with all its fields within a given page.
|
|
|
|
This function creates FormFieldText renderables for all fields in the form,
|
|
positions them vertically, and registers both the form and its fields in
|
|
the page's callback registry.
|
|
|
|
Args:
|
|
form: The abstract Form object to layout
|
|
page: The page to layout the form on
|
|
font: Optional font for field labels (defaults to page default)
|
|
field_spacing: Vertical spacing between fields in pixels
|
|
|
|
Returns:
|
|
Tuple of:
|
|
- bool: True if form was successfully laid out, False if page ran out of space
|
|
- List[str]: List of registered ids for all fields (empty if layout failed)
|
|
"""
|
|
# Use provided font or create a default one
|
|
if font is None:
|
|
font = Font(font_size=12, colour=(0, 0, 0))
|
|
|
|
# Track registered field ids
|
|
field_ids = []
|
|
|
|
# Layout each field in the form
|
|
for field_name, field in form._fields.items():
|
|
# Add spacing before each field (except the first)
|
|
if field_ids:
|
|
page._current_y_offset += field_spacing
|
|
|
|
# Layout the field
|
|
success, field_id = form_field_layouter(field, page, font)
|
|
|
|
if not success:
|
|
# Couldn't fit this field, return failure
|
|
return False, []
|
|
|
|
field_ids.append(field_id)
|
|
|
|
# Register the form itself (optional, for form submission)
|
|
# Note: The form doesn't have a visual representation, but we can track it
|
|
# for submission callbacks
|
|
# form_id = page.callbacks.register(form, html_id=form.html_id)
|
|
|
|
return True, field_ids
|
|
|
|
|
|
class DocumentLayouter:
|
|
"""
|
|
Document layouter that orchestrates layout of various abstract elements.
|
|
|
|
Delegates to specialized layouters for different content types:
|
|
- paragraph_layouter for text paragraphs
|
|
- image_layouter for images
|
|
- table_layouter for tables
|
|
|
|
This class acts as a coordinator, managing the overall document flow
|
|
and page context while delegating specific layout tasks to specialized
|
|
layouter functions.
|
|
"""
|
|
|
|
def __init__(self, page: Page):
|
|
"""
|
|
Initialize the document layouter with a page.
|
|
|
|
Args:
|
|
page: The page to layout content on
|
|
"""
|
|
self.page = page
|
|
# Create a style resolver if page doesn't have one
|
|
if hasattr(page, 'style_resolver'):
|
|
style_resolver = page.style_resolver
|
|
else:
|
|
# Create a default rendering context and style resolver
|
|
from pyWebLayout.style.concrete_style import RenderingContext
|
|
context = RenderingContext()
|
|
style_resolver = StyleResolver(context)
|
|
self.style_registry = ConcreteStyleRegistry(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 paragraph_layouter.
|
|
|
|
Args:
|
|
paragraph: The paragraph to layout
|
|
start_word: Index of the first word to process (for continuation)
|
|
pretext: Optional pretext from a previous hyphenated word
|
|
|
|
Returns:
|
|
Tuple of (success, failed_word_index, remaining_pretext)
|
|
"""
|
|
return paragraph_layouter(paragraph, self.page, start_word, pretext)
|
|
|
|
def layout_image(self, image: AbstractImage, max_width: Optional[int] = None,
|
|
max_height: Optional[int] = None) -> bool:
|
|
"""
|
|
Layout an image using the image_layouter.
|
|
|
|
Args:
|
|
image: The abstract Image object to layout
|
|
max_width: Maximum width constraint (defaults to page available width)
|
|
max_height: Maximum height constraint (defaults to remaining page height)
|
|
|
|
Returns:
|
|
bool: True if image was successfully laid out, False if page ran out of space
|
|
"""
|
|
return image_layouter(image, self.page, max_width, max_height)
|
|
|
|
def layout_table(self, table: Table, style: Optional[TableStyle] = None) -> bool:
|
|
"""
|
|
Layout a table using the table_layouter.
|
|
|
|
Args:
|
|
table: The abstract Table object to layout
|
|
style: Optional table styling configuration
|
|
|
|
Returns:
|
|
bool: True if table was successfully laid out, False if page ran out of space
|
|
"""
|
|
return table_layouter(table, self.page, style)
|
|
|
|
def layout_button(self,
|
|
button: Button,
|
|
font: Optional[Font] = None,
|
|
padding: Tuple[int,
|
|
int,
|
|
int,
|
|
int] = (4,
|
|
8,
|
|
4,
|
|
8)) -> Tuple[bool,
|
|
str]:
|
|
"""
|
|
Layout a button using the button_layouter.
|
|
|
|
Args:
|
|
button: The abstract Button object to layout
|
|
font: Optional font for button text
|
|
padding: Padding around button text
|
|
|
|
Returns:
|
|
Tuple of (success, registered_id)
|
|
"""
|
|
return button_layouter(button, self.page, font, padding)
|
|
|
|
def layout_form(self, form: Form, font: Optional[Font] = None,
|
|
field_spacing: int = 10) -> Tuple[bool, List[str]]:
|
|
"""
|
|
Layout a form using the form_layouter.
|
|
|
|
Args:
|
|
form: The abstract Form object to layout
|
|
font: Optional font for field labels
|
|
field_spacing: Vertical spacing between fields
|
|
|
|
Returns:
|
|
Tuple of (success, list_of_field_ids)
|
|
"""
|
|
return form_layouter(form, self.page, font, field_spacing)
|
|
|
|
def layout_document(
|
|
self, elements: List[Union[Paragraph, AbstractImage, Table, Button, Form]]) -> bool:
|
|
"""
|
|
Layout a list of abstract elements (paragraphs, images, tables, buttons, and forms).
|
|
|
|
This method delegates to specialized layouters based on element type:
|
|
- Paragraphs are handled by layout_paragraph
|
|
- Images are handled by layout_image
|
|
- Tables are handled by layout_table
|
|
- Buttons are handled by layout_button
|
|
- Forms are handled by layout_form
|
|
|
|
Args:
|
|
elements: List of abstract elements to layout
|
|
|
|
Returns:
|
|
True if all elements were successfully laid out, False otherwise
|
|
"""
|
|
for element in elements:
|
|
if isinstance(element, Paragraph):
|
|
success, _, _ = self.layout_paragraph(element)
|
|
if not success:
|
|
return False
|
|
elif isinstance(element, AbstractImage):
|
|
success = self.layout_image(element)
|
|
if not success:
|
|
return False
|
|
elif isinstance(element, Table):
|
|
success = self.layout_table(element)
|
|
if not success:
|
|
return False
|
|
elif isinstance(element, Button):
|
|
success, _ = self.layout_button(element)
|
|
if not success:
|
|
return False
|
|
elif isinstance(element, Form):
|
|
success, _ = self.layout_form(element)
|
|
if not success:
|
|
return False
|
|
# Future: elif isinstance(element, CodeBlock): use code_layouter
|
|
return True
|