Coverage for pyWebLayout/layout/document_layouter.py: 77%

219 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-08 20:34 +0000

1from __future__ import annotations 

2 

3from typing import List, Tuple, Optional, Union 

4import numpy as np 

5 

6from pyWebLayout.concrete import Page, Line, Text 

7from pyWebLayout.concrete.image import RenderableImage 

8from pyWebLayout.concrete.functional import ButtonText, FormFieldText 

9from pyWebLayout.concrete.table import TableRenderer, TableStyle 

10from pyWebLayout.abstract import Paragraph, Word 

11from pyWebLayout.abstract.block import Image as AbstractImage, Heading, PageBreak, Table 

12from pyWebLayout.abstract.functional import Button, Form, FormField 

13from pyWebLayout.style.concrete_style import ConcreteStyleRegistry, RenderingContext, StyleResolver 

14from pyWebLayout.style import Font, Alignment 

15 

16 

17def paragraph_layouter(paragraph: Paragraph, 

18 page: Page, 

19 start_word: int = 0, 

20 pretext: Optional[Text] = None, 

21 alignment_override: Optional['Alignment'] = None) -> Tuple[bool, 

22 Optional[int], 

23 Optional[Text]]: 

24 """ 

25 Layout a paragraph of text within a given page. 

26 

27 This function extracts word spacing constraints from the style system 

28 and uses them to create properly spaced lines of text. 

29 

30 Args: 

31 paragraph: The paragraph to layout 

32 page: The page to layout the paragraph on 

33 start_word: Index of the first word to process (for continuation) 

34 pretext: Optional pretext from a previous hyphenated word 

35 alignment_override: Optional alignment to override the paragraph's default alignment 

36 

37 Returns: 

38 Tuple of: 

39 - bool: True if paragraph was completely laid out, False if page ran out of space 

40 - Optional[int]: Index of first word that didn't fit (if any) 

41 - Optional[Text]: Remaining pretext if word was hyphenated (if any) 

42 """ 

43 if not paragraph.words: 

44 return True, None, None 

45 

46 # Validate inputs 

47 if start_word >= len(paragraph.words): 

48 return True, None, None 

49 

50 # paragraph.style is already a Font object (concrete), not AbstractStyle 

51 # We need to get word spacing constraints from the Font's abstract style if available 

52 # For now, use reasonable defaults based on font size 

53 

54 # Alignment for text that does not specify its own. Headings are never 

55 # justified - stretching a two-word title across the measure is always wrong - 

56 # so they fall back to flush left. 

57 default_alignment = getattr(page.style, 'default_alignment', None) 

58 if not isinstance(default_alignment, Alignment): 

59 default_alignment = Alignment.JUSTIFY 

60 if isinstance(paragraph, Heading): 

61 default_alignment = Alignment.LEFT 

62 

63 if isinstance(paragraph.style, Font): 

64 # paragraph.style is already a Font (concrete style) 

65 font = paragraph.style 

66 # Use default word spacing constraints based on font size 

67 # Minimum spacing should be proportional to font size for better readability 

68 min_spacing = float(font.font_size) * 0.25 # 25% of font size 

69 max_spacing = float(font.font_size) * 0.5 # 50% of font size 

70 word_spacing_constraints = (int(min_spacing), int(max_spacing)) 

71 text_align = default_alignment 

72 else: 

73 # paragraph.style is an AbstractStyle, resolve it 

74 # Ensure font_size is an int (it could be a FontSize enum) 

75 from pyWebLayout.style.abstract_style import FontSize 

76 if isinstance(paragraph.style.font_size, FontSize): 76 ↛ 80line 76 didn't jump to line 80 because the condition on line 76 was always true

77 # Use a default base font size, the resolver will handle the semantic size 

78 base_font_size = 16 

79 else: 

80 base_font_size = int(paragraph.style.font_size) 

81 

82 rendering_context = RenderingContext(base_font_size=base_font_size) 

83 style_resolver = StyleResolver(rendering_context) 

84 style_registry = ConcreteStyleRegistry(style_resolver) 

85 concrete_style = style_registry.get_concrete_style(paragraph.style) 

86 font = concrete_style.create_font() 

87 word_spacing_constraints = ( 

88 int(concrete_style.word_spacing_min), 

89 int(concrete_style.word_spacing_max) 

90 ) 

91 # text_align is None when the source did not specify one. 

92 text_align = concrete_style.text_align or default_alignment 

93 

94 # Apply page-level word spacing override if specified 

95 if hasattr( 95 ↛ 101line 95 didn't jump to line 101 because the condition on line 95 was never true

96 page.style, 

97 'word_spacing') and isinstance( 

98 page.style.word_spacing, 

99 int) and page.style.word_spacing > 0: 

100 # Add the page-level word spacing to both min and max constraints 

101 min_ws, max_ws = word_spacing_constraints 

102 word_spacing_constraints = ( 

103 min_ws + page.style.word_spacing, 

104 max_ws + page.style.word_spacing 

105 ) 

106 

107 # Apply alignment override if provided 

108 if alignment_override is not None: 

109 text_align = alignment_override 

110 

111 # Cap font size to page maximum if needed 

112 if font.font_size > page.style.max_font_size: 112 ↛ 114line 112 didn't jump to line 114 because the condition on line 112 was never true

113 # Use paragraph's font registry to create the capped font 

114 if hasattr(paragraph, 'get_or_create_font'): 

115 font = paragraph.get_or_create_font( 

116 font_path=font._font_path, 

117 font_size=page.style.max_font_size, 

118 colour=font.colour, 

119 weight=font.weight, 

120 style=font.style, 

121 decoration=font.decoration, 

122 background=font.background 

123 ) 

124 else: 

125 # Fallback to direct creation (will still use global cache) 

126 font = Font( 

127 font_path=font._font_path, 

128 font_size=page.style.max_font_size, 

129 colour=font.colour, 

130 weight=font.weight, 

131 style=font.style, 

132 decoration=font.decoration, 

133 background=font.background 

134 ) 

135 

136 # Calculate baseline-to-baseline spacing: font size + additional line spacing 

137 # This is the vertical distance between baselines of consecutive lines 

138 # Formula: baseline_spacing = font_size + line_spacing (absolute pixels) 

139 line_spacing_value = getattr(page.style, 'line_spacing', 5) 

140 # Ensure line_spacing is an int (could be Mock in tests) 

141 if not isinstance(line_spacing_value, int): 

142 line_spacing_value = 5 

143 baseline_spacing = font.font_size + line_spacing_value 

144 

145 # Get font metrics for boundary checking 

146 ascent, descent = font.font.getmetrics() 

147 

148 def create_new_line(word: Optional[Union[Word, Text]] = None, 

149 is_first_line: bool = False) -> Optional[Line]: 

150 """Helper function to create a new line, returns None if page is full.""" 

151 # Check if this line's baseline and descenders would fit on the page 

152 if not page.can_fit_line(baseline_spacing, ascent, descent): 

153 return None 

154 

155 # For the first line, position it so text starts at the top boundary 

156 # For subsequent lines, use current y_offset which tracks 

157 # baseline-to-baseline spacing 

158 if is_first_line: 158 ↛ 161line 158 didn't jump to line 161 because the condition on line 158 was never true

159 # Position line origin so that baseline (origin + ascent) is close to top 

160 # We want minimal space above the text, so origin should be at boundary 

161 y_cursor = page._current_y_offset 

162 else: 

163 y_cursor = page._current_y_offset 

164 x_cursor = page.content_origin[0] 

165 

166 # `word` is accepted for call-site readability only: the line that is about 

167 # to be created measures it when it is added, so measuring it here as well 

168 # only paid for a Text object that was immediately discarded. 

169 

170 return Line( 

171 spacing=word_spacing_constraints, 

172 origin=(x_cursor, y_cursor), 

173 size=(page.available_width, baseline_spacing), 

174 draw=page.measurement_draw, 

175 font=font, 

176 halign=text_align 

177 ) 

178 

179 # Create initial line 

180 current_line = create_new_line() 

181 if not current_line: 

182 return False, start_word, pretext 

183 

184 page.add_child(current_line) 

185 # Note: add_child already updates _current_y_offset based on child's origin and size 

186 # No need to manually increment it here 

187 

188 # Track current position in paragraph 

189 current_pretext = pretext 

190 

191 # Process words starting from start_word 

192 for i, word in enumerate(paragraph.words[start_word:], start=start_word): 

193 # Check if this is a LinkedWord and needs special handling in concrete layer 

194 # Note: The Line.add_word method will create Text objects internally, 

195 # but we may want to create LinkText for LinkedWord instances in future 

196 # For now, the abstract layer (LinkedWord) carries the link info, 

197 # and the concrete layer (LinkText) would be created during rendering 

198 

199 success, overflow_text = current_line.add_word(word, current_pretext) 

200 

201 if success: 

202 # Word fit successfully 

203 if overflow_text is not None: 

204 # If there's overflow text, we need to start a new line with it 

205 current_pretext = overflow_text 

206 current_line = create_new_line(overflow_text) 

207 if not current_line: 

208 # If we can't create a new line, return with the current state 

209 return False, i, overflow_text 

210 page.add_child(current_line) 

211 # Note: add_child already updates _current_y_offset 

212 # Continue to the next word 

213 continue 

214 else: 

215 # No overflow, clear pretext 

216 current_pretext = None 

217 else: 

218 # Word didn't fit, need a new line 

219 current_line = create_new_line(word) 

220 if not current_line: 

221 # Page is full, return current position 

222 return False, i, overflow_text 

223 

224 # Check if the word will fit on the new line before adding it 

225 temp_text = Text.from_word(word, page.measurement_draw) 

226 if temp_text.width > current_line.size[0]: 

227 # Word is too wide for the line, we need to hyphenate it 

228 if len(word.text) >= 6: 228 ↛ 250line 228 didn't jump to line 250 because the condition on line 228 was always true

229 # Try to hyphenate the word 

230 splits = [ 

231 (Text( 

232 pair[0], 

233 word.style, 

234 page.measurement_draw, 

235 line=current_line, 

236 source=word), 

237 Text( 

238 pair[1], 

239 word.style, 

240 page.measurement_draw, 

241 line=current_line, 

242 source=word)) for pair in word.possible_hyphenation()] 

243 if len(splits) > 0: 243 ↛ 250line 243 didn't jump to line 250 because the condition on line 243 was always true

244 # Use the first hyphenation point 

245 first_part, second_part = splits[0] 

246 current_line.add_word(word, first_part) 

247 current_pretext = second_part 

248 continue 

249 

250 page.add_child(current_line) 

251 # Note: add_child already updates _current_y_offset 

252 

253 # Try to add the word to the new line 

254 success, overflow_text = current_line.add_word(word, current_pretext) 

255 

256 if not success: 256 ↛ 259line 256 didn't jump to line 259 because the condition on line 256 was never true

257 # Word still doesn't fit even on a new line 

258 # This might happen with very long words or narrow pages 

259 if overflow_text: 

260 # Word was hyphenated, continue with the overflow 

261 current_pretext = overflow_text 

262 continue 

263 else: 

264 # Word cannot be broken, skip it or handle as error 

265 # For now, we'll return indicating we couldn't process this word 

266 return False, i, None 

267 else: 

268 current_pretext = overflow_text # May be None or hyphenated remainder 

269 

270 # All words processed successfully. The line holding the final word is the 

271 # end of the paragraph, so it is rendered at its natural width rather than 

272 # justified to the full column. A paragraph continued on the next page does 

273 # not reach here, so its lines stay justified - which is correct. 

274 if current_line is not None: 274 ↛ 277line 274 didn't jump to line 277 because the condition on line 274 was always true

275 current_line.is_paragraph_end = True 

276 

277 return True, None, None 

278 

279 

280def pagebreak_layouter(page_break: PageBreak, page: Page) -> bool: 

281 """ 

282 Handle a page break element. 

283 

284 A page break signals that all subsequent content should start on a new page. 

285 This function always returns False to indicate that the current page is complete 

286 and a new page should be created for subsequent content. 

287 

288 Args: 

289 page_break: The PageBreak block 

290 page: The current page (not used, but kept for consistency) 

291 

292 Returns: 

293 bool: Always False to force creation of a new page 

294 """ 

295 # Page break always forces a new page 

296 return False 

297 

298 

299def image_layouter(image: AbstractImage, page: Page, max_width: Optional[int] = None, 

300 max_height: Optional[int] = None) -> bool: 

301 """ 

302 Layout an image within a given page. 

303 

304 This function places an image on the page, respecting size constraints 

305 and available space. Images are centered horizontally by default. 

306 

307 Args: 

308 image: The abstract Image object to layout 

309 page: The page to layout the image on 

310 max_width: Maximum width constraint (defaults to page available width) 

311 max_height: Maximum height constraint (defaults to remaining page height) 

312 

313 Returns: 

314 bool: True if image was successfully laid out, False if page ran out of space 

315 """ 

316 # Use page available width if max_width not specified 

317 if max_width is None: 

318 max_width = page.available_width 

319 

320 # Calculate available height on page 

321 available_height = page.remaining_height 

322 

323 # If no space available, image doesn't fit 

324 if available_height <= 0: 324 ↛ 325line 324 didn't jump to line 325 because the condition on line 324 was never true

325 return False 

326 

327 if max_height is None: 

328 max_height = available_height 

329 else: 

330 max_height = min(max_height, available_height) 

331 

332 # Calculate scaled dimensions 

333 scaled_width, scaled_height = image.calculate_scaled_dimensions( 

334 max_width, max_height) 

335 

336 # Check if image fits on current page 

337 if scaled_height is None or scaled_height > available_height: 

338 return False 

339 

340 # Create renderable image 

341 x_offset = page.content_origin[0] 

342 y_offset = page._current_y_offset 

343 

344 # Access page.draw to ensure canvas is initialized 

345 _ = page.draw 

346 

347 renderable_image = RenderableImage( 

348 image=image, 

349 canvas=page._canvas, 

350 max_width=max_width, 

351 max_height=max_height, 

352 origin=(x_offset, y_offset), 

353 size=(scaled_width or max_width, scaled_height or max_height), 

354 halign=Alignment.CENTER, 

355 valign=Alignment.TOP 

356 ) 

357 

358 # Add to page 

359 page.add_child(renderable_image) 

360 

361 return True 

362 

363 

364def table_layouter( 

365 table: Table, 

366 page: Page, 

367 style: Optional[TableStyle] = None) -> bool: 

368 """ 

369 Layout a table within a given page. 

370 

371 This function uses the TableRenderer to render the table at the current 

372 page position, advancing the page's y-offset after successful rendering. 

373 

374 Args: 

375 table: The abstract Table object to layout 

376 page: The page to layout the table on 

377 style: Optional table styling configuration 

378 

379 Returns: 

380 bool: True if table was successfully laid out, False if page ran out of space 

381 """ 

382 # Calculate available space 

383 available_width = page.available_width 

384 x_offset = page.content_origin[0] 

385 y_offset = page._current_y_offset 

386 

387 # Access page.draw to ensure canvas is initialized 

388 draw = page.draw 

389 canvas = page._canvas 

390 

391 # Create table renderer 

392 origin = (x_offset, y_offset) 

393 renderer = TableRenderer( 

394 table=table, 

395 origin=origin, 

396 available_width=available_width, 

397 draw=draw, 

398 style=style, 

399 canvas=canvas 

400 ) 

401 

402 # Check if table fits on current page 

403 table_height = renderer.size[1] 

404 available_height = page.remaining_height 

405 

406 if table_height > available_height: 

407 return False 

408 

409 # Render the table 

410 renderer.render() 

411 

412 # Update page y-offset 

413 page._current_y_offset = y_offset + table_height 

414 

415 return True 

416 

417 

418def button_layouter(button: Button, 

419 page: Page, 

420 font: Optional[Font] = None, 

421 padding: Tuple[int, 

422 int, 

423 int, 

424 int] = (4, 

425 8, 

426 4, 

427 8)) -> Tuple[bool, 

428 str]: 

429 """ 

430 Layout a button within a given page and register it for callback binding. 

431 

432 This function creates a ButtonText renderable, positions it on the page, 

433 and registers it in the page's callback registry using the button's html_id 

434 (if available) or an auto-generated id. 

435 

436 Args: 

437 button: The abstract Button object to layout 

438 page: The page to layout the button on 

439 font: Optional font for button text (defaults to page default) 

440 padding: Padding around button text (top, right, bottom, left) 

441 

442 Returns: 

443 Tuple of: 

444 - bool: True if button was successfully laid out, False if page ran out of space 

445 - str: The id used to register the button in the callback registry 

446 """ 

447 # Use provided font or create a default one 

448 if font is None: 

449 font = Font(font_size=14, colour=(255, 255, 255)) 

450 

451 # Calculate available space 

452 available_height = page.remaining_height 

453 

454 # Create ButtonText renderable 

455 button_text = ButtonText(button, font, page.measurement_draw, padding=padding) 

456 

457 # Check if button fits on current page 

458 button_height = button_text.size[1] 

459 if button_height > available_height: 

460 return False, "" 

461 

462 # Position the button 

463 x_offset = page.content_origin[0] 

464 y_offset = page._current_y_offset 

465 

466 button_text.set_origin(np.array([x_offset, y_offset])) 

467 

468 # Register in callback registry 

469 html_id = button.html_id 

470 registered_id = page.callbacks.register(button_text, html_id=html_id) 

471 

472 # Add to page 

473 page.add_child(button_text) 

474 

475 return True, registered_id 

476 

477 

478def form_field_layouter(field: FormField, page: Page, font: Optional[Font] = None, 

479 field_height: int = 24) -> Tuple[bool, str]: 

480 """ 

481 Layout a form field within a given page and register it for callback binding. 

482 

483 This function creates a FormFieldText renderable, positions it on the page, 

484 and registers it in the page's callback registry. 

485 

486 Args: 

487 field: The abstract FormField object to layout 

488 page: The page to layout the field on 

489 font: Optional font for field label (defaults to page default) 

490 field_height: Height of the input field area 

491 

492 Returns: 

493 Tuple of: 

494 - bool: True if field was successfully laid out, False if page ran out of space 

495 - str: The id used to register the field in the callback registry 

496 """ 

497 # Use provided font or create a default one 

498 if font is None: 498 ↛ 499line 498 didn't jump to line 499 because the condition on line 498 was never true

499 font = Font(font_size=12, colour=(0, 0, 0)) 

500 

501 # Calculate available space 

502 available_height = page.remaining_height 

503 

504 # Create FormFieldText renderable 

505 field_text = FormFieldText(field, font, page.measurement_draw, 

506 field_height=field_height) 

507 

508 # Check if field fits on current page 

509 total_field_height = field_text.size[1] 

510 if total_field_height > available_height: 510 ↛ 511line 510 didn't jump to line 511 because the condition on line 510 was never true

511 return False, "" 

512 

513 # Position the field 

514 x_offset = page.content_origin[0] 

515 y_offset = page._current_y_offset 

516 

517 field_text.set_origin(np.array([x_offset, y_offset])) 

518 

519 # Register in callback registry (use field name as html_id fallback) 

520 html_id = getattr(field, '_html_id', None) or field.name 

521 registered_id = page.callbacks.register(field_text, html_id=html_id) 

522 

523 # Add to page 

524 page.add_child(field_text) 

525 

526 return True, registered_id 

527 

528 

529def form_layouter(form: Form, page: Page, font: Optional[Font] = None, 

530 field_spacing: int = 10) -> Tuple[bool, List[str]]: 

531 """ 

532 Layout a complete form with all its fields within a given page. 

533 

534 This function creates FormFieldText renderables for all fields in the form, 

535 positions them vertically, and registers both the form and its fields in 

536 the page's callback registry. 

537 

538 Args: 

539 form: The abstract Form object to layout 

540 page: The page to layout the form on 

541 font: Optional font for field labels (defaults to page default) 

542 field_spacing: Vertical spacing between fields in pixels 

543 

544 Returns: 

545 Tuple of: 

546 - bool: True if form was successfully laid out, False if page ran out of space 

547 - List[str]: List of registered ids for all fields (empty if layout failed) 

548 """ 

549 # Use provided font or create a default one 

550 if font is None: 550 ↛ 551line 550 didn't jump to line 551 because the condition on line 550 was never true

551 font = Font(font_size=12, colour=(0, 0, 0)) 

552 

553 # Track registered field ids 

554 field_ids = [] 

555 

556 # Layout each field in the form 

557 for field_name, field in form._fields.items(): 

558 # Add spacing before each field (except the first) 

559 if field_ids: 

560 page._current_y_offset += field_spacing 

561 

562 # Layout the field 

563 success, field_id = form_field_layouter(field, page, font) 

564 

565 if not success: 565 ↛ 567line 565 didn't jump to line 567 because the condition on line 565 was never true

566 # Couldn't fit this field, return failure 

567 return False, [] 

568 

569 field_ids.append(field_id) 

570 

571 # Register the form itself (optional, for form submission) 

572 # Note: The form doesn't have a visual representation, but we can track it 

573 # for submission callbacks 

574 # form_id = page.callbacks.register(form, html_id=form.html_id) 

575 

576 return True, field_ids 

577 

578 

579class DocumentLayouter: 

580 """ 

581 Document layouter that orchestrates layout of various abstract elements. 

582 

583 Delegates to specialized layouters for different content types: 

584 - paragraph_layouter for text paragraphs 

585 - image_layouter for images 

586 - table_layouter for tables 

587 

588 This class acts as a coordinator, managing the overall document flow 

589 and page context while delegating specific layout tasks to specialized 

590 layouter functions. 

591 """ 

592 

593 def __init__(self, page: Page): 

594 """ 

595 Initialize the document layouter with a page. 

596 

597 Args: 

598 page: The page to layout content on 

599 """ 

600 self.page = page 

601 # Create a style resolver if page doesn't have one 

602 if hasattr(page, 'style_resolver'): 

603 style_resolver = page.style_resolver 

604 else: 

605 # Create a default rendering context and style resolver 

606 from pyWebLayout.style.concrete_style import RenderingContext 

607 context = RenderingContext() 

608 style_resolver = StyleResolver(context) 

609 self.style_registry = ConcreteStyleRegistry(style_resolver) 

610 

611 def layout_paragraph(self, 

612 paragraph: Paragraph, 

613 start_word: int = 0, 

614 pretext: Optional[Text] = None) -> Tuple[bool, 

615 Optional[int], 

616 Optional[Text]]: 

617 """ 

618 Layout a paragraph using the paragraph_layouter. 

619 

620 Args: 

621 paragraph: The paragraph to layout 

622 start_word: Index of the first word to process (for continuation) 

623 pretext: Optional pretext from a previous hyphenated word 

624 

625 Returns: 

626 Tuple of (success, failed_word_index, remaining_pretext) 

627 """ 

628 return paragraph_layouter(paragraph, self.page, start_word, pretext) 

629 

630 def layout_image(self, image: AbstractImage, max_width: Optional[int] = None, 

631 max_height: Optional[int] = None) -> bool: 

632 """ 

633 Layout an image using the image_layouter. 

634 

635 Args: 

636 image: The abstract Image object to layout 

637 max_width: Maximum width constraint (defaults to page available width) 

638 max_height: Maximum height constraint (defaults to remaining page height) 

639 

640 Returns: 

641 bool: True if image was successfully laid out, False if page ran out of space 

642 """ 

643 return image_layouter(image, self.page, max_width, max_height) 

644 

645 def layout_table(self, table: Table, style: Optional[TableStyle] = None) -> bool: 

646 """ 

647 Layout a table using the table_layouter. 

648 

649 Args: 

650 table: The abstract Table object to layout 

651 style: Optional table styling configuration 

652 

653 Returns: 

654 bool: True if table was successfully laid out, False if page ran out of space 

655 """ 

656 return table_layouter(table, self.page, style) 

657 

658 def layout_button(self, 

659 button: Button, 

660 font: Optional[Font] = None, 

661 padding: Tuple[int, 

662 int, 

663 int, 

664 int] = (4, 

665 8, 

666 4, 

667 8)) -> Tuple[bool, 

668 str]: 

669 """ 

670 Layout a button using the button_layouter. 

671 

672 Args: 

673 button: The abstract Button object to layout 

674 font: Optional font for button text 

675 padding: Padding around button text 

676 

677 Returns: 

678 Tuple of (success, registered_id) 

679 """ 

680 return button_layouter(button, self.page, font, padding) 

681 

682 def layout_form(self, form: Form, font: Optional[Font] = None, 

683 field_spacing: int = 10) -> Tuple[bool, List[str]]: 

684 """ 

685 Layout a form using the form_layouter. 

686 

687 Args: 

688 form: The abstract Form object to layout 

689 font: Optional font for field labels 

690 field_spacing: Vertical spacing between fields 

691 

692 Returns: 

693 Tuple of (success, list_of_field_ids) 

694 """ 

695 return form_layouter(form, self.page, font, field_spacing) 

696 

697 def layout_document( 

698 self, elements: List[Union[Paragraph, AbstractImage, Table, Button, Form]]) -> bool: 

699 """ 

700 Layout a list of abstract elements (paragraphs, images, tables, buttons, and forms). 

701 

702 This method delegates to specialized layouters based on element type: 

703 - Paragraphs are handled by layout_paragraph 

704 - Images are handled by layout_image 

705 - Tables are handled by layout_table 

706 - Buttons are handled by layout_button 

707 - Forms are handled by layout_form 

708 

709 Args: 

710 elements: List of abstract elements to layout 

711 

712 Returns: 

713 True if all elements were successfully laid out, False otherwise 

714 """ 

715 for element in elements: 

716 if isinstance(element, Paragraph): 

717 success, _, _ = self.layout_paragraph(element) 

718 if not success: 

719 return False 

720 elif isinstance(element, AbstractImage): 

721 success = self.layout_image(element) 

722 if not success: 722 ↛ 723line 722 didn't jump to line 723 because the condition on line 722 was never true

723 return False 

724 elif isinstance(element, Table): 724 ↛ 728line 724 didn't jump to line 728 because the condition on line 724 was always true

725 success = self.layout_table(element) 

726 if not success: 

727 return False 

728 elif isinstance(element, Button): 

729 success, _ = self.layout_button(element) 

730 if not success: 

731 return False 

732 elif isinstance(element, Form): 

733 success, _ = self.layout_form(element) 

734 if not success: 

735 return False 

736 # Future: elif isinstance(element, CodeBlock): use code_layouter 

737 return True