Coverage for fpdf2_textindex/_fpdf/_fpdf.py: 55.58%

633 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-01 14:22 +0000

1"""Fixes bugs in :py:class:`fpdf.FPDF`.""" 

2 

3# ruff: noqa: E501, E713, RUF069, SIM102, SIM108, UP007, UP045 

4 

5from collections import defaultdict 

6from collections.abc import Callable, Iterator, Sequence 

7from contextlib import contextmanager 

8import re 

9import types 

10from typing import Literal, Optional, Union 

11import warnings 

12 

13from fpdf.deprecation import get_stack_level 

14from fpdf.deprecation import support_deprecated_txt_arg 

15from fpdf.drawing_primitives import DeviceCMYK 

16from fpdf.drawing_primitives import DeviceGray 

17from fpdf.drawing_primitives import DeviceRGB 

18from fpdf.drawing_primitives import convert_to_device_color 

19from fpdf.enums import Align 

20from fpdf.enums import CharVPos 

21from fpdf.enums import DocumentCompliance 

22from fpdf.enums import MethodReturnValue 

23from fpdf.enums import PDFResourceType 

24from fpdf.enums import PageOrientation 

25from fpdf.enums import TextEmphasis 

26from fpdf.enums import TextMode 

27from fpdf.enums import WrapMode 

28from fpdf.enums import XPos 

29from fpdf.enums import YPos 

30from fpdf.errors import FPDFException 

31from fpdf.fonts import CoreFont 

32from fpdf.fonts import TTFFont 

33import fpdf.fpdf 

34from fpdf.fpdf import ToCPlaceholder 

35from fpdf.fpdf import check_page 

36from fpdf.graphics_state import StateStackType 

37from fpdf.line_break import Fragment 

38from fpdf.line_break import MultiLineBreak 

39from fpdf.line_break import TextLine 

40from fpdf.line_break import TotalPagesSubstitutionFragment 

41from fpdf.outline import OutlineSection 

42from fpdf.output import ResourceTypes 

43from fpdf.syntax import PDFArray 

44from fpdf.table import draw_box_borders 

45from fpdf.unicode_script import UnicodeScript 

46from fpdf.unicode_script import get_unicode_script 

47from fpdf.util import FloatTolerance 

48from fpdf.util import Padding 

49 

50 

51class FPDF(fpdf.fpdf.FPDF): 

52 __PATCHED__: bool = True 

53 # BUGFIX: Support of empty md links and escaped square brackets in link 

54 MARKDOWN_ESCAPE_CHARACTER = "\\" 

55 MARKDOWN_LINK_REGEX = re.compile( 

56 rf"^(?<!{MARKDOWN_ESCAPE_CHARACTER * 2:s})" 

57 rf"\[((?:{MARKDOWN_ESCAPE_CHARACTER * 2:s}[\[\]]|[^\[\]])*)\]" 

58 r"\(([^()]+)\)(.*)$", 

59 re.DOTALL, 

60 ) 

61 _MARKDOWN_LINK_TEXT_UNESCAPE = re.compile(r"\\([\[\]])") 

62 

63 def __init__( 

64 self, 

65 orientation: str | PageOrientation = PageOrientation.PORTRAIT, 

66 unit: str | float = "mm", 

67 format: str | tuple[float, float] = "A4", 

68 font_cache_dir: Literal["DEPRECATED"] = "DEPRECATED", 

69 *, 

70 enforce_compliance: str | DocumentCompliance | None = None, 

71 ) -> None: 

72 super().__init__( 

73 orientation=orientation, 

74 unit=unit, 

75 format=format, 

76 font_cache_dir=font_cache_dir, 

77 enforce_compliance=enforce_compliance, 

78 ) 

79 # BUGFIX: https://github.com/py-pdf/fpdf2/issues/1837, bug-toc-rendering 

80 self._toc_gstate: Optional[StateStackType] = None 

81 

82 # BUGFIX: https://github.com/py-pdf/fpdf2/issues/1807, dry-run-in-toc 

83 @contextmanager 

84 def _disable_writing(self) -> Iterator[None]: 

85 if not isinstance(self._out, types.MethodType): 85 ↛ 88line 85 didn't jump to line 88 because the condition on line 85 was never true

86 # This mean that self._out has already been redefined. 

87 # This is the case of a nested call to this method: we do nothing 

88 yield 

89 return 

90 self._out = lambda *args, **kwargs: None # type: ignore[method-assign] 

91 prev_page, prev_pages_count, prev_x, prev_y, prev_toc_inserted_pages = ( 

92 self.page, 

93 self.pages_count, 

94 self.x, 

95 self.y, 

96 self._toc_inserted_pages, 

97 ) 

98 annots = PDFArray(self.pages[self.page].annots or []) 

99 self._push_local_stack() 

100 try: 

101 yield 

102 finally: 

103 self._pop_local_stack() 

104 # restore location: 

105 for p in range(prev_pages_count + 1, self.pages_count + 1): 

106 del self.pages[p] 

107 self.page = prev_page 

108 self.pages[self.page].annots = annots 

109 self.set_xy(prev_x, prev_y) 

110 # restore inserted pages in toc 

111 self._toc_inserted_pages = prev_toc_inserted_pages 

112 # restore writing function: 

113 del self._out 

114 

115 def _insert_table_of_contents(self) -> None: 

116 # Doc has been closed but we want to write to self.pages[self.page] instead of self.buffer: 

117 tocp = self.toc_placeholder 

118 assert tocp is not None 

119 prev_page, prev_y = self.page, self.y 

120 self.page, self.y = tocp.start_page, tocp.y 

121 # BUGFIX: https://github.com/py-pdf/fpdf2/issues/1807, bug-toc-rendering 

122 # Set gstate to toc page 

123 assert self._toc_gstate is not None 

124 assert not self._is_current_graphics_state_nested() 

125 cur_gstate = self._pop_local_stack() 

126 self._push_local_stack(new=self._toc_gstate) 

127 # flag rendering ToC for page breaking function 

128 self.in_toc_rendering = True 

129 self._set_orientation(tocp.page_orientation, self.dw_pt, self.dh_pt) 

130 tocp.render_function(self, self._outline) 

131 self.in_toc_rendering = False # set ToC rendering flag off 

132 expected_final_page = tocp.start_page + tocp.pages - 1 

133 if ( 133 ↛ 137line 133 didn't jump to line 137 because the condition on line 133 was never true

134 self.page != expected_final_page 

135 and not self._toc_allow_page_insertion 

136 ): 

137 too = "many" if self.page > expected_final_page else "few" 

138 error_msg = f"The rendering function passed to FPDF.insert_toc_placeholder triggered too {too} page breaks: " 

139 error_msg += f"ToC ended on page {self.page} while it was expected to span exactly {tocp.pages} pages" 

140 raise FPDFException(error_msg) 

141 if self._toc_inserted_pages: 

142 # Generating final page footer after more pages were inserted: 

143 self._render_footer() 

144 # We need to reorder the pages, because some new pages have been inserted in the ToC, 

145 # but they have been inserted at the end of self.pages: 

146 new_pages = [ 

147 self.pages.pop(len(self.pages)) 

148 for _ in range(self._toc_inserted_pages) 

149 ] 

150 new_pages = list(reversed(new_pages)) 

151 indices_remap: dict[int, int] = {} 

152 for page_index in range( 

153 tocp.start_page + 1, self.pages_count + len(new_pages) + 1 

154 ): 

155 if page_index in self.pages: 

156 new_pages.append(self.pages.pop(page_index)) 

157 page = self.pages[page_index] = new_pages.pop(0) 

158 # Fix page indices: 

159 indices_remap[page.index()] = page_index 

160 page.set_index(page_index) 

161 # Fix page labels: 

162 if tocp.reset_page_indices is False: 162 ↛ 163line 162 didn't jump to line 163 because the condition on line 162 was never true

163 page.get_page_label().st = page_index # type: ignore[union-attr] 

164 assert len(new_pages) == 0, f"#new_pages: {len(new_pages)}" 

165 # Fix links: 

166 for dest in self.links.values(): 

167 assert dest.page_number is not None 

168 new_index = indices_remap.get(dest.page_number) 

169 if new_index is not None: 

170 dest.page_number = new_index 

171 # Fix outline: 

172 for section in self._outline: 172 ↛ 173line 172 didn't jump to line 173 because the loop on line 172 never started

173 new_index = indices_remap.get(section.page_number) 

174 if new_index is not None: 

175 section.dest = section.dest.replace(page=new_index) 

176 section.page_number = new_index 

177 if section.struct_elem: 

178 # pylint: disable=protected-access 

179 section.struct_elem._page_number = ( # pyright: ignore[reportPrivateUsage] 

180 new_index 

181 ) 

182 # Fix resource catalog: 

183 resources_per_page = self._resource_catalog.resources_per_page 

184 new_resources_per_page: dict[ 

185 tuple[int, PDFResourceType], set[ResourceTypes] 

186 ] = defaultdict(set) 

187 for ( 

188 page_number, 

189 resource_type, 

190 ), resource in resources_per_page.items(): 

191 key = ( 

192 indices_remap.get(page_number, page_number), 

193 resource_type, 

194 ) 

195 new_resources_per_page[key] = resource 

196 self._resource_catalog.resources_per_page = new_resources_per_page 

197 # BUGFIX: https://github.com/py-pdf/fpdf2/issues/1807, bug-toc-rendering 

198 # Reset gstate (after rendering of footer) 

199 while self._is_current_graphics_state_nested(): 199 ↛ 200line 199 didn't jump to line 200 because the condition on line 199 was never true

200 self._pop_local_stack() 

201 self._pop_local_stack() 

202 self._push_local_stack(cur_gstate) 

203 # Reset page and y 

204 self.page, self.y = prev_page, prev_y 

205 

206 # Bugfix: https://github.com/py-pdf/fpdf2/issues/1840, multi-cell-md-result 

207 def _join_text_lines( 

208 self, 

209 text_lines: list[TextLine], 

210 markdown: bool = False, 

211 ) -> list[str]: 

212 output_lines: list[str] = [] 

213 if not markdown: 213 ↛ 214line 213 didn't jump to line 214 because the condition on line 213 was never true

214 for text_line in text_lines: 

215 characters: list[str] = [] 

216 for frag in text_line.fragments: 

217 characters.extend(frag.characters) 

218 output_lines.append("".join(characters)) 

219 else: 

220 emphasis_markers: dict[TextEmphasis, str] = { 

221 TextEmphasis.NONE: "", 

222 TextEmphasis.B: self.MARKDOWN_BOLD_MARKER, 

223 TextEmphasis.I: self.MARKDOWN_ITALICS_MARKER, 

224 TextEmphasis.U: self.MARKDOWN_UNDERLINE_MARKER, 

225 TextEmphasis.S: self.MARKDOWN_STRIKETHROUGH_MARKER, 

226 } 

227 marker_pattern: str = "|".join( 

228 re.escape(m) 

229 for te, m in emphasis_markers.items() 

230 if te != TextEmphasis.NONE 

231 ) 

232 escape_pattern: re.Pattern[str] = re.compile( 

233 rf"({marker_pattern:s})" 

234 ) 

235 

236 def escape(text: str) -> str: 

237 return escape_pattern.sub( 

238 rf"{self.MARKDOWN_ESCAPE_CHARACTER:s}\\1", text 

239 ) 

240 

241 for text_line in text_lines: 

242 text_parts: list[str] = [] 

243 last_emphasis: TextEmphasis = TextEmphasis.NONE 

244 for frag in text_line.fragments: 

245 if markdown: 245 ↛ 264line 245 didn't jump to line 264 because the condition on line 245 was always true

246 next_emphasis = TextEmphasis.coerce( 

247 frag.font_style 

248 + ("U" if frag.underline else "") 

249 + ("S" if frag.strikethrough else "") 

250 ) 

251 # If fragment has a link and link underline is true, 

252 # the underline marker must not be added 

253 if frag.link and self.MARKDOWN_LINK_UNDERLINE: 

254 next_emphasis &= ~TextEmphasis.U 

255 removed_emphasis = last_emphasis & ~next_emphasis 

256 for te in reversed(TextEmphasis): 

257 if removed_emphasis & te: 

258 text_parts.append(emphasis_markers[te]) 

259 added_emphasis = next_emphasis & ~last_emphasis 

260 for te in TextEmphasis: 

261 if added_emphasis & te: 

262 text_parts.append(emphasis_markers[te]) 

263 last_emphasis = next_emphasis 

264 text = "".join(frag.characters) 

265 # NOTE: Currently, markdown format inside of links is not handled 

266 # so only escape markdown markers outside of links 

267 text_parts.append( 

268 f"[{text:s}]({frag.link!s:s})" 

269 if frag.link 

270 else escape(text) 

271 ) 

272 next_emphasis = TextEmphasis.NONE 

273 removed_emphasis = last_emphasis & ~next_emphasis 

274 for te in reversed(TextEmphasis): 

275 if removed_emphasis & te: 

276 text_parts.append(emphasis_markers[te]) 

277 output_lines.append("".join(text_parts)) 

278 return output_lines 

279 

280 def _parse_chars(self, text: str, markdown: bool) -> Iterator[Fragment]: 

281 if ( 

282 not markdown 

283 and not self.text_shaping 

284 and not self._fallback_font_ids 

285 ): 

286 if self.str_alias_nb_pages: 286 ↛ 304line 286 didn't jump to line 304 because the condition on line 286 was always true

287 for seq, fragment_text in enumerate( 

288 text.split(self.str_alias_nb_pages) 

289 ): 

290 if seq > 0: 290 ↛ 291line 290 didn't jump to line 291 because the condition on line 290 was never true

291 yield TotalPagesSubstitutionFragment( 

292 self.str_alias_nb_pages, 

293 self._get_current_graphics_state(), 

294 self.k, 

295 ) 

296 if fragment_text: 296 ↛ 287line 296 didn't jump to line 287 because the condition on line 296 was always true

297 yield Fragment( 

298 fragment_text, 

299 self._get_current_graphics_state(), 

300 self.k, 

301 ) 

302 return 

303 

304 yield Fragment(text, self._get_current_graphics_state(), self.k) 

305 return 

306 txt_frag: list[str] = [] 

307 in_bold: bool = "B" in self.font_style 

308 in_italics: bool = "I" in self.font_style 

309 in_strikethrough: bool = bool(self.strikethrough) 

310 in_underline: bool = bool(self.underline) 

311 current_fallback_font = None 

312 current_text_script = None 

313 

314 def frag() -> Fragment: 

315 nonlocal txt_frag, current_fallback_font, current_text_script 

316 gstate = self._get_current_graphics_state() 

317 gstate.font_style = ("B" if in_bold else "") + ( 

318 "I" if in_italics else "" 

319 ) 

320 gstate.strikethrough = in_strikethrough 

321 gstate.underline = in_underline 

322 if current_fallback_font: 322 ↛ 323line 322 didn't jump to line 323 because the condition on line 322 was never true

323 style = "".join(c for c in current_fallback_font if c in ("BI")) 

324 family = current_fallback_font.replace("B", "").replace("I", "") 

325 gstate.font_family = family 

326 gstate.font_style = style 

327 gstate.current_font = self.fonts[current_fallback_font] 

328 current_fallback_font = None 

329 current_text_script = None 

330 fragment = Fragment( 

331 txt_frag, 

332 gstate, 

333 self.k, 

334 ) 

335 txt_frag = [] 

336 return fragment 

337 

338 if self.is_ttf_font: 338 ↛ 339line 338 didn't jump to line 339 because the condition on line 338 was never true

339 font_glyphs = self.current_font.cmap # type: ignore[union-attr] 

340 else: 

341 font_glyphs = [] 

342 

343 escape_next_marker = 0 

344 escape_run = 0 

345 

346 while text: 

347 if markdown and text[0] == self.MARKDOWN_ESCAPE_CHARACTER: 347 ↛ 348line 347 didn't jump to line 348 because the condition on line 347 was never true

348 escape_run += 1 

349 text = text[1:] 

350 continue 

351 

352 if markdown and escape_run: 352 ↛ 353line 352 didn't jump to line 353 because the condition on line 352 was never true

353 is_escape_target = text[:2] in ( 

354 self.MARKDOWN_BOLD_MARKER, 

355 self.MARKDOWN_ITALICS_MARKER, 

356 self.MARKDOWN_STRIKETHROUGH_MARKER, 

357 self.MARKDOWN_UNDERLINE_MARKER, 

358 ) 

359 if is_escape_target and escape_run % 2 == 1: 

360 for _ in range(escape_run - 1): 

361 txt_frag.append(self.MARKDOWN_ESCAPE_CHARACTER) 

362 if current_fallback_font: 

363 if txt_frag: 

364 yield frag() 

365 current_fallback_font = None 

366 escape_next_marker = 2 

367 escape_run = 0 

368 continue 

369 for _ in range(escape_run): 

370 txt_frag.append(self.MARKDOWN_ESCAPE_CHARACTER) 

371 escape_run = 0 

372 

373 is_marker = text[:2] in ( 

374 self.MARKDOWN_BOLD_MARKER, 

375 self.MARKDOWN_ITALICS_MARKER, 

376 self.MARKDOWN_STRIKETHROUGH_MARKER, 

377 self.MARKDOWN_UNDERLINE_MARKER, 

378 ) 

379 if markdown and escape_next_marker: 379 ↛ 380line 379 didn't jump to line 380 because the condition on line 379 was never true

380 is_marker = False 

381 half_marker = text[0] 

382 text_script = get_unicode_script(text[0]) 

383 if text_script not in ( 

384 UnicodeScript.COMMON, 

385 UnicodeScript.UNKNOWN, 

386 current_text_script, 

387 ): 

388 if txt_frag and current_text_script: 388 ↛ 389line 388 didn't jump to line 389 because the condition on line 388 was never true

389 yield frag() 

390 current_text_script = text_script 

391 

392 if self.str_alias_nb_pages: 392 ↛ 414line 392 didn't jump to line 414 because the condition on line 392 was always true

393 if ( 393 ↛ 397line 393 didn't jump to line 397 because the condition on line 393 was never true

394 text[: len(self.str_alias_nb_pages)] 

395 == self.str_alias_nb_pages 

396 ): 

397 if txt_frag: 

398 yield frag() 

399 gstate = self._get_current_graphics_state() 

400 gstate.font_style = ("B" if in_bold else "") + ( 

401 "I" if in_italics else "" 

402 ) 

403 gstate.strikethrough = in_strikethrough 

404 gstate.underline = in_underline 

405 yield TotalPagesSubstitutionFragment( 

406 self.str_alias_nb_pages, 

407 gstate, 

408 self.k, 

409 ) 

410 text = text[len(self.str_alias_nb_pages) :] 

411 continue 

412 

413 # Check that previous & next characters are not identical to the marker: 

414 if markdown: 414 ↛ 467line 414 didn't jump to line 467 because the condition on line 414 was always true

415 if ( 

416 is_marker 

417 and (not txt_frag or txt_frag[-1] != half_marker) 

418 and (len(text) < 3 or text[2] != half_marker) 

419 ): 

420 if txt_frag: 

421 yield frag() 

422 if text[:2] == self.MARKDOWN_BOLD_MARKER: 

423 in_bold = not in_bold 

424 if text[:2] == self.MARKDOWN_ITALICS_MARKER: 

425 in_italics = not in_italics 

426 if text[:2] == self.MARKDOWN_STRIKETHROUGH_MARKER: 

427 in_strikethrough = not in_strikethrough 

428 if text[:2] == self.MARKDOWN_UNDERLINE_MARKER: 

429 in_underline = not in_underline 

430 text = text[2:] 

431 continue 

432 

433 is_link = self.MARKDOWN_LINK_REGEX.match(text) 

434 if is_link: 

435 link_text, link_dest, text = is_link.groups() 

436 # BUGFIX: enable escaped square brackets in links 

437 link_text = self._MARKDOWN_LINK_TEXT_UNESCAPE.sub( 

438 r"\1", link_text 

439 ) 

440 if txt_frag: 

441 yield frag() 

442 gstate = self._get_current_graphics_state() 

443 # BUGFIX: https://github.com/py-pdf/fpdf2/issues/1826 

444 gstate.font_style = ("B" if in_bold else "") + ( 

445 "I" if in_italics else "" 

446 ) 

447 gstate.strikethrough = in_strikethrough 

448 gstate.underline = ( 

449 self.MARKDOWN_LINK_UNDERLINE or in_underline 

450 ) 

451 if self.MARKDOWN_LINK_COLOR: 

452 gstate.text_color = convert_to_device_color( 

453 self.MARKDOWN_LINK_COLOR 

454 ) 

455 try: 

456 page = int(link_dest) 

457 link_dest = self.add_link(page=page) 

458 except ValueError: 

459 pass 

460 yield Fragment( 

461 list(link_text), 

462 gstate, 

463 self.k, 

464 link=link_dest, 

465 ) 

466 continue 

467 if ( 467 ↛ 472line 467 didn't jump to line 472 because the condition on line 467 was never true

468 self.is_ttf_font 

469 and text[0] != "\n" 

470 and not ord(text[0]) in font_glyphs 

471 ): 

472 style = ("B" if in_bold else "") + ("I" if in_italics else "") 

473 fallback_font = self.get_fallback_font(text[0], style) 

474 if fallback_font: 

475 if fallback_font == current_fallback_font: 

476 txt_frag.append(text[0]) 

477 text = text[1:] 

478 continue 

479 if txt_frag: 

480 yield frag() 

481 current_fallback_font = fallback_font 

482 txt_frag.append(text[0]) 

483 text = text[1:] 

484 continue 

485 if current_fallback_font: 485 ↛ 486line 485 didn't jump to line 486 because the condition on line 485 was never true

486 if txt_frag: 

487 yield frag() 

488 current_fallback_font = None 

489 txt_frag.append(text[0]) 

490 text = text[1:] 

491 if markdown and escape_next_marker: 491 ↛ 492line 491 didn't jump to line 492 because the condition on line 491 was never true

492 escape_next_marker -= 1 

493 if escape_next_marker == 0: 

494 yield frag() 

495 if markdown and escape_run: 495 ↛ 496line 495 didn't jump to line 496 because the condition on line 495 was never true

496 for _ in range(escape_run): 

497 txt_frag.append(self.MARKDOWN_ESCAPE_CHARACTER) 

498 escape_run = 0 

499 if txt_frag: 

500 yield frag() 

501 

502 # BUGFIX: https://github.com/py-pdf/fpdf2/issues/1826 

503 def _render_styled_text_line( 

504 self, 

505 text_line: TextLine, 

506 h: Optional[float] = None, 

507 border: Union[str, int] = 0, 

508 new_x: XPos = XPos.RIGHT, 

509 new_y: YPos = YPos.TOP, 

510 fill: bool = False, 

511 link: Optional[str | int] = "", 

512 center: bool = False, 

513 padding: Optional[Padding] = None, 

514 prevent_font_change: bool = False, 

515 ) -> bool: 

516 if isinstance(border, int) and border not in (0, 1): 516 ↛ 517line 516 didn't jump to line 517 because the condition on line 516 was never true

517 warnings.warn( 

518 'Integer values for "border" parameter other than 1 are currently ignored', 

519 stacklevel=get_stack_level(), 

520 ) 

521 border = 1 

522 elif isinstance(border, str) and set(border).issuperset("LTRB"): 522 ↛ 523line 522 didn't jump to line 523 because the condition on line 522 was never true

523 border = 1 

524 

525 if padding is None: 

526 padding = Padding(0, 0, 0, 0) 

527 l_c_margin = r_c_margin = float(0) 

528 if padding.left == 0: 528 ↛ 530line 528 didn't jump to line 530 because the condition on line 528 was always true

529 l_c_margin = self.c_margin 

530 if padding.right == 0: 530 ↛ 533line 530 didn't jump to line 533 because the condition on line 530 was always true

531 r_c_margin = self.c_margin 

532 

533 styled_txt_width = text_line.text_width 

534 if not styled_txt_width: 

535 for i, frag in enumerate(text_line.fragments): 

536 unscaled_width = frag.get_width(initial_cs=i != 0) 

537 styled_txt_width += unscaled_width 

538 

539 w = text_line.max_width 

540 if w is None: 

541 if not text_line.fragments: 541 ↛ 542line 541 didn't jump to line 542 because the condition on line 541 was never true

542 raise ValueError( 

543 "'text_line' must have fragments if 'text_line.text_width' is None" 

544 ) 

545 w = styled_txt_width + l_c_margin + r_c_margin 

546 elif w == 0: 546 ↛ 547line 546 didn't jump to line 547 because the condition on line 546 was never true

547 w = self.w - self.r_margin - self.x 

548 if center: 548 ↛ 549line 548 didn't jump to line 549 because the condition on line 548 was never true

549 self.x = self.l_margin + (self.epw - w) / 2 

550 elif text_line.align == Align.X: 550 ↛ 551line 550 didn't jump to line 551 because the condition on line 550 was never true

551 self.x -= w / 2 

552 

553 max_font_size: float = 0 # how much height we need to accommodate. 

554 # currently all font sizes within a line are vertically aligned on the baseline. 

555 fragments = text_line.get_ordered_fragments() 

556 for frag in fragments: 

557 if FloatTolerance.greater_than(frag.font_size, max_font_size): 

558 max_font_size = frag.font_size 

559 if h is None: 559 ↛ 560line 559 didn't jump to line 560 because the condition on line 559 was never true

560 h = max_font_size 

561 page_break_triggered = self._perform_page_break_if_need_be(h) 

562 sl: list[str] = [] 

563 

564 k = self.k 

565 

566 # pre-calc border edges with padding 

567 

568 left = (self.x - padding.left) * k 

569 right = (self.x + w + padding.right) * k 

570 top = (self.h - self.y + padding.top) * k 

571 bottom = (self.h - (self.y + h) - padding.bottom) * k 

572 

573 if fill: 573 ↛ 574line 573 didn't jump to line 574 because the condition on line 573 was never true

574 op = "B" if border == 1 else "f" 

575 sl.append( 

576 f"{left:.2f} {top:.2f} {right - left:.2f} {bottom - top:.2f} re {op}" 

577 ) 

578 elif border == 1: 578 ↛ 579line 578 didn't jump to line 579 because the condition on line 578 was never true

579 sl.append( 

580 f"{left:.2f} {top:.2f} {right - left:.2f} {bottom - top:.2f} re S" 

581 ) 

582 # pylint: enable=invalid-unary-operand-type 

583 

584 if isinstance(border, str): 584 ↛ 585line 584 didn't jump to line 585 because the condition on line 584 was never true

585 if "L" in border: 

586 sl.append(f"{left:.2f} {top:.2f} m {left:.2f} {bottom:.2f} l S") 

587 if "T" in border: 

588 sl.append(f"{left:.2f} {top:.2f} m {right:.2f} {top:.2f} l S") 

589 if "R" in border: 

590 sl.append( 

591 f"{right:.2f} {top:.2f} m {right:.2f} {bottom:.2f} l S" 

592 ) 

593 if "B" in border: 

594 sl.append( 

595 f"{left:.2f} {bottom:.2f} m {right:.2f} {bottom:.2f} l S" 

596 ) 

597 

598 if self._record_text_quad_points: 598 ↛ 599line 598 didn't jump to line 599 because the condition on line 598 was never true

599 self._add_quad_points(self.x, self.y, w, h) 

600 

601 s_start = self.x 

602 s_width: float = 0 

603 # We try to avoid modifying global settings for temporary changes. 

604 current_ws = frag_ws = 0.0 

605 current_lift = 0.0 

606 current_char_vpos = CharVPos.LINE 

607 current_font = self.current_font 

608 current_font_size_pt = self.font_size_pt 

609 current_font_style = self.font_style 

610 current_text_mode = self.text_mode 

611 current_font_stretching = self.font_stretching 

612 current_char_spacing = self.char_spacing 

613 fill_color_changed = False 

614 last_used_color = self.fill_color 

615 if fragments: 

616 if text_line.align == Align.R: 616 ↛ 617line 616 didn't jump to line 617 because the condition on line 616 was never true

617 dx = w - l_c_margin - styled_txt_width 

618 elif text_line.align in [Align.C, Align.X]: 618 ↛ 619line 618 didn't jump to line 619 because the condition on line 618 was never true

619 dx = (w - styled_txt_width) / 2 

620 else: 

621 dx = l_c_margin 

622 s_start += dx 

623 word_spacing: float = 0 

624 if text_line.align == Align.J and text_line.number_of_spaces: 

625 word_spacing = ( 

626 w - l_c_margin - r_c_margin - styled_txt_width 

627 ) / text_line.number_of_spaces 

628 sl.append( 

629 f"BT {(self.x + dx) * k:.2f} " 

630 f"{(self.h - self.y - 0.5 * h - 0.3 * max_font_size) * k:.2f} Td" 

631 ) 

632 if ( 632 ↛ 641line 632 didn't jump to line 641 because the condition on line 632 was never true

633 not prevent_font_change 

634 and not self.current_font_is_set_on_page 

635 and self.current_font is not None 

636 and fragments[0].font.fontkey in self._fallback_font_ids 

637 and self.current_font.fontkey not in self._fallback_font_ids 

638 ): 

639 # The first fragment uses a fallback font. Establish the current font for 

640 # the page in this text object to avoid promoting the fallback font. 

641 sl.append( 

642 self._set_font_for_page( 

643 self.current_font, 

644 self.font_size_pt, 

645 wrap_in_text_object=False, 

646 ) 

647 ) 

648 underlines: list[ 

649 tuple[ 

650 float, 

651 float, 

652 CoreFont | TTFFont, 

653 float, 

654 DeviceRGB | DeviceGray | DeviceCMYK | None, 

655 ] 

656 ] = [] 

657 strikethroughs: list[ 

658 tuple[ 

659 float, 

660 float, 

661 CoreFont | TTFFont, 

662 float, 

663 DeviceRGB | DeviceGray | DeviceCMYK | None, 

664 ] 

665 ] = [] 

666 for i, frag in enumerate(fragments): 

667 if isinstance(frag, TotalPagesSubstitutionFragment): 667 ↛ 668line 667 didn't jump to line 668 because the condition on line 667 was never true

668 self.pages[self.page].add_text_substitution(frag) 

669 if frag.text_color != last_used_color: 

670 # allow to change color within the line of text. 

671 last_used_color = frag.text_color 

672 assert last_used_color is not None 

673 sl.append(last_used_color.serialize().lower()) 

674 fill_color_changed = True 

675 if word_spacing and frag.font_stretching != 100: 675 ↛ 677line 675 didn't jump to line 677 because the condition on line 675 was never true

676 # Space character is already stretched, extra spacing is absolute. 

677 frag_ws = word_spacing * 100 / frag.font_stretching 

678 else: 

679 frag_ws = word_spacing 

680 if current_font_stretching != frag.font_stretching: 680 ↛ 681line 680 didn't jump to line 681 because the condition on line 680 was never true

681 current_font_stretching = frag.font_stretching 

682 sl.append(f"{frag.font_stretching:.2f} Tz") 

683 if current_char_spacing != frag.char_spacing: 683 ↛ 684line 683 didn't jump to line 684 because the condition on line 683 was never true

684 current_char_spacing = frag.char_spacing 

685 sl.append(f"{frag.char_spacing:.2f} Tc") 

686 if not self.current_font_is_set_on_page: 

687 if prevent_font_change: 

688 # This is "local" to the current BT / ET context: 

689 current_font = frag.font 

690 current_font_size_pt = frag.font_size_pt 

691 current_font_style = frag.font_style 

692 sl.append( 

693 f"/F{current_font.i} {current_font_size_pt:.2f} Tf" 

694 ) 

695 self._resource_catalog.add( 

696 PDFResourceType.FONT, current_font.i, self.page 

697 ) 

698 current_char_vpos = frag.char_vpos 

699 else: 

700 # This is "global" to the page, 

701 # as it is rendered in the content stream 

702 # BEFORE the text_lines /fragments, 

703 # wrapped into BT / ET operators: 

704 current_font = self.current_font = frag.font 

705 current_font_size_pt = self.font_size_pt = ( 

706 frag.font_size_pt 

707 ) 

708 current_font_style = self.font_style = frag.font_style 

709 self._out( 

710 self._set_font_for_page( 

711 current_font, 

712 current_font_size_pt, 

713 ) 

714 ) 

715 current_char_vpos = frag.char_vpos 

716 elif ( 716 ↛ 723line 716 didn't jump to line 723 because the condition on line 716 was never true

717 current_font != frag.font 

718 or current_font_size_pt != frag.font_size_pt 

719 or current_font_style != frag.font_style 

720 or current_char_vpos != frag.char_vpos 

721 ): 

722 # This is "local" to the current BT / ET context: 

723 current_font = frag.font 

724 current_font_size_pt = frag.font_size_pt 

725 current_font_style = frag.font_style 

726 sl.append( 

727 self._set_font_for_page( 

728 current_font, 

729 current_font_size_pt, 

730 wrap_in_text_object=False, 

731 ) 

732 ) 

733 current_char_vpos = frag.char_vpos 

734 lift = frag.lift 

735 if lift != current_lift: 735 ↛ 737line 735 didn't jump to line 737 because the condition on line 735 was never true

736 # Use text rise operator: 

737 sl.append(f"{lift:.2f} Ts") 

738 current_lift = lift 

739 if ( 739 ↛ 743line 739 didn't jump to line 743 because the condition on line 739 was never true

740 frag.text_mode != TextMode.FILL 

741 or frag.text_mode != current_text_mode 

742 ): 

743 current_text_mode = frag.text_mode 

744 sl.append(f"{frag.text_mode} Tr {frag.line_width:.2f} w") 

745 

746 r_text = frag.render_pdf_text( 

747 frag_ws, 

748 current_ws, 

749 word_spacing, 

750 self.x + dx + s_width, 

751 self.y + (0.5 * h + 0.3 * max_font_size), 

752 self.h, 

753 ) 

754 if r_text: 754 ↛ 757line 754 didn't jump to line 757 because the condition on line 754 was always true

755 sl.append(r_text) 

756 

757 frag_width = frag.get_width( 

758 initial_cs=i != 0 

759 ) + word_spacing * frag.characters.count(" ") 

760 if frag.underline: 

761 underlines.append( 

762 ( 

763 self.x + dx + s_width, 

764 frag_width, 

765 frag.font, 

766 frag.font_size, 

767 frag.text_color, 

768 ) 

769 ) 

770 if frag.strikethrough: 

771 strikethroughs.append( 

772 ( 

773 self.x + dx + s_width, 

774 frag_width, 

775 frag.font, 

776 frag.font_size, 

777 frag.text_color, 

778 ) 

779 ) 

780 if frag.link: 

781 self.link( 

782 x=self.x + dx + s_width, 

783 y=self.y + (0.5 * h) - (0.5 * frag.font_size), 

784 w=frag_width, 

785 h=frag.font_size, 

786 link=frag.link, 

787 ) 

788 if not frag.is_ttf_font: 788 ↛ 790line 788 didn't jump to line 790 because the condition on line 788 was always true

789 current_ws = frag_ws 

790 s_width += frag_width 

791 

792 sl.append("ET") 

793 

794 # Underlines & strikethrough must be rendred OUTSIDE BT/ET contexts, 

795 # cf. https://github.com/py-pdf/fpdf2/issues/1456 

796 if underlines: 

797 for start_x, width, font, font_size, text_color in underlines: 

798 # Change color of the underlines 

799 if text_color != last_used_color: 

800 last_used_color = text_color 

801 assert last_used_color is not None 

802 sl.append(last_used_color.serialize().lower()) 

803 fill_color_changed = True 

804 sl.append( 

805 self._do_underline( 

806 start_x, 

807 self.y + (0.5 * h) + (0.3 * font_size), 

808 width, 

809 font, 

810 ) 

811 ) 

812 if strikethroughs: 

813 for ( 

814 start_x, 

815 width, 

816 font, 

817 font_size, 

818 text_color, 

819 ) in strikethroughs: 

820 # Change color of the strikethroughs 

821 if text_color != last_used_color: 

822 last_used_color = text_color 

823 assert last_used_color is not None 

824 sl.append(last_used_color.serialize().lower()) 

825 fill_color_changed = True 

826 sl.append( 

827 self._do_strikethrough( 

828 start_x, 

829 self.y + (0.5 * h) + (0.3 * font_size), 

830 width, 

831 font, 

832 ) 

833 ) 

834 if link: 834 ↛ 835line 834 didn't jump to line 835 because the condition on line 834 was never true

835 self.link( 

836 self.x + dx, 

837 self.y 

838 + (0.5 * h) 

839 - ( 

840 0.5 * frag.font_size # pyright: ignore[reportPossiblyUnboundVariable] 

841 ), 

842 styled_txt_width, 

843 frag.font_size, # pyright: ignore[reportPossiblyUnboundVariable] 

844 link, 

845 ) 

846 

847 if sl: 

848 # If any PDF settings have been left modified, wrap the line 

849 # in a local context. 

850 # pylint: disable=too-many-boolean-expressions 

851 if ( 

852 current_ws != 0.0 

853 or current_lift != 0.0 

854 or current_char_vpos != CharVPos.LINE 

855 or current_font != self.current_font 

856 or current_font_size_pt != self.font_size_pt 

857 or current_font_style != self.font_style 

858 or current_text_mode != self.text_mode 

859 or fill_color_changed 

860 or current_font_stretching != self.font_stretching 

861 or current_char_spacing != self.char_spacing 

862 ): 

863 s = f"q {' '.join(sl)} Q" 

864 else: 

865 s = " ".join(sl) 

866 # pylint: enable=too-many-boolean-expressions 

867 self._out(s) 

868 # If the text is empty, h = max_font_size ends up as 0. 

869 # We still need a valid default height for self.ln() (issue #601). 

870 self._lasth = h or self.font_size 

871 

872 # XPos.LEFT -> self.x stays the same 

873 if new_x == XPos.RIGHT: 

874 self.x += w 

875 elif new_x == XPos.START: 875 ↛ 876line 875 didn't jump to line 876 because the condition on line 875 was never true

876 self.x = s_start 

877 elif new_x == XPos.END: 877 ↛ 878line 877 didn't jump to line 878 because the condition on line 877 was never true

878 self.x = s_start + s_width 

879 elif new_x == XPos.WCONT: 879 ↛ 880line 879 didn't jump to line 880 because the condition on line 879 was never true

880 if s_width: 

881 self.x = s_start + s_width - r_c_margin 

882 else: 

883 self.x = s_start 

884 elif new_x == XPos.CENTER: 884 ↛ 885line 884 didn't jump to line 885 because the condition on line 884 was never true

885 self.x = s_start + s_width / 2.0 

886 elif new_x == XPos.LMARGIN: 

887 self.x = self.l_margin 

888 elif new_x == XPos.RMARGIN: 888 ↛ 889line 888 didn't jump to line 889 because the condition on line 888 was never true

889 self.x = self.w - self.r_margin 

890 

891 # YPos.TOP: -> self.y stays the same 

892 # YPos.LAST: -> self.y stays the same (single line) 

893 if new_y == YPos.NEXT: 893 ↛ 895line 893 didn't jump to line 895 because the condition on line 893 was always true

894 self.y += h 

895 if new_y == YPos.TMARGIN: 895 ↛ 896line 895 didn't jump to line 896 because the condition on line 895 was never true

896 self.y = self.t_margin 

897 if new_y == YPos.BMARGIN: 897 ↛ 898line 897 didn't jump to line 898 because the condition on line 897 was never true

898 self.y = self.h - self.b_margin 

899 

900 return page_break_triggered 

901 

902 @check_page 

903 def insert_toc_placeholder( 

904 self, 

905 render_toc_function: Callable[[fpdf.FPDF, list[OutlineSection]], None], 

906 pages: int = 1, 

907 allow_extra_pages: bool = False, 

908 reset_page_indices: bool = True, 

909 ) -> None: 

910 if pages < 1: 

911 raise ValueError( 

912 f"'pages' parameter must be equal or greater than 1: {pages}" 

913 ) 

914 if not callable(render_toc_function): 

915 raise TypeError( 

916 f"The first argument must be a callable, got: {type(render_toc_function)}" 

917 ) 

918 if self.toc_placeholder: 

919 raise FPDFException( 

920 "A placeholder for the table of contents has already been defined" 

921 f" on page {self.toc_placeholder.start_page}" 

922 ) 

923 self.toc_placeholder = ToCPlaceholder( 

924 render_toc_function, 

925 self.page, 

926 self.y, 

927 self.cur_orientation, 

928 pages, 

929 reset_page_indices, 

930 ) 

931 self._toc_allow_page_insertion = allow_extra_pages 

932 for _ in range(pages): 

933 self._perform_page_break() 

934 

935 @check_page 

936 @support_deprecated_txt_arg 

937 def multi_cell( 

938 self, 

939 w: float, 

940 h: Optional[float] = None, 

941 text: str = "", 

942 border: Literal[0, 1] | str = 0, 

943 align: str | Align = Align.J, 

944 fill: bool = False, 

945 split_only: bool = False, # DEPRECATED 

946 link: Optional[int | str] = None, 

947 ln: Literal["DEPRECATED"] = "DEPRECATED", 

948 max_line_height: Optional[float] = None, 

949 markdown: bool = False, 

950 print_sh: bool = False, 

951 new_x: str | XPos = XPos.RIGHT, 

952 new_y: str | YPos = YPos.NEXT, 

953 wrapmode: WrapMode = WrapMode.WORD, 

954 dry_run: bool = False, 

955 output: str | MethodReturnValue = MethodReturnValue.PAGE_BREAK, 

956 center: bool = False, 

957 padding: int | Sequence[int] | Padding = 0, 

958 ) -> fpdf.fpdf.FPDF.MultiCellResult: 

959 padding = Padding.new(padding) 

960 wrapmode = WrapMode.coerce(wrapmode) 

961 

962 if split_only: 

963 warnings.warn( 

964 ( 

965 'The parameter "split_only" is deprecated since v2.7.4.' 

966 ' Use instead dry_run=True and output="LINES".' 

967 ), 

968 DeprecationWarning, 

969 stacklevel=get_stack_level(), 

970 ) 

971 if dry_run or split_only: 

972 with self._disable_writing(): 

973 return self.multi_cell( 

974 w=w, 

975 h=h, 

976 text=text, 

977 border=border, 

978 align=align, 

979 fill=fill, 

980 link=link, 

981 ln=ln, 

982 max_line_height=max_line_height, 

983 markdown=markdown, 

984 print_sh=print_sh, 

985 new_x=new_x, 

986 new_y=new_y, 

987 wrapmode=wrapmode, 

988 dry_run=False, 

989 split_only=False, 

990 output=MethodReturnValue.LINES if split_only else output, 

991 center=center, 

992 padding=padding, 

993 ) 

994 if not self.font_family: 

995 raise FPDFException( 

996 "No font set, you need to call set_font() beforehand" 

997 ) 

998 if isinstance(w, str) or isinstance(h, str): 

999 raise ValueError( 

1000 "Parameter 'w' and 'h' must be numbers, not strings." 

1001 " You can omit them by passing string content with text=" 

1002 ) 

1003 new_x = XPos.coerce(new_x) 

1004 new_y = YPos.coerce(new_y) 

1005 if ln != "DEPRECATED": 

1006 # For backwards compatibility, if "ln" is used we overwrite "new_[xy]". 

1007 if ln == 0: 

1008 new_x = XPos.RIGHT 

1009 new_y = YPos.NEXT 

1010 elif ln == 1: 

1011 new_x = XPos.LMARGIN 

1012 new_y = YPos.NEXT 

1013 elif ln == 2: 

1014 new_x = XPos.LEFT 

1015 new_y = YPos.NEXT 

1016 elif ln == 3: 

1017 new_x = XPos.RIGHT 

1018 new_y = YPos.TOP 

1019 else: 

1020 raise ValueError( 

1021 f'Invalid value for parameter "ln" ({ln}),' 

1022 " must be an int between 0 and 3." 

1023 ) 

1024 warnings.warn( 

1025 ( 

1026 'The parameter "ln" is deprecated since v2.5.2.' 

1027 f" Instead of ln={ln} use new_x=XPos.{new_x.name}, new_y=YPos.{new_y.name}." 

1028 ), 

1029 DeprecationWarning, 

1030 stacklevel=get_stack_level(), 

1031 ) 

1032 align = Align.coerce(align) 

1033 

1034 page_break_triggered = False 

1035 

1036 if h is None: 

1037 h = self.font_size 

1038 

1039 # If width is 0, set width to available width between margins 

1040 if w == 0: 

1041 w = self.w - self.r_margin - self.x 

1042 

1043 # Store the starting position before applying padding 

1044 prev_x, prev_y = self.x, self.y 

1045 

1046 # Apply padding to contents 

1047 # decrease maximum allowed width by padding 

1048 # shift the starting point by padding 

1049 maximum_allowed_width = w = w - padding.right - padding.left 

1050 clearance_margins: list[float] = [] 

1051 # If we don't have padding on either side, we need a clearance margin. 

1052 if not padding.left: 

1053 clearance_margins.append(self.c_margin) 

1054 if not padding.right: 

1055 clearance_margins.append(self.c_margin) 

1056 if align != Align.X: 

1057 self.x += padding.left 

1058 self.y += padding.top 

1059 

1060 # Center overrides padding 

1061 if center: 

1062 self.x = ( 

1063 self.w / 2 

1064 if align == Align.X 

1065 else self.l_margin + (self.epw - w) / 2 

1066 ) 

1067 prev_x = self.x 

1068 

1069 # Calculate text length 

1070 text = self.normalize_text(text) 

1071 normalized_string = text.replace("\r", "") 

1072 styled_text_fragments = ( 

1073 self._preload_bidirectional_text(normalized_string, markdown) 

1074 if self.text_shaping 

1075 else self._preload_font_styles(normalized_string, markdown) 

1076 ) 

1077 

1078 prev_current_font = self.current_font 

1079 prev_font_style = self.font_style 

1080 prev_underline = self.underline 

1081 total_height: float = 0 

1082 

1083 text_lines: list[TextLine] = [] 

1084 multi_line_break = MultiLineBreak( 

1085 styled_text_fragments, 

1086 maximum_allowed_width, 

1087 clearance_margins, 

1088 align=align, 

1089 print_sh=print_sh, 

1090 wrapmode=wrapmode, 

1091 ) 

1092 text_line = multi_line_break.get_line() 

1093 while (text_line) is not None: 

1094 text_lines.append(text_line) 

1095 text_line = multi_line_break.get_line() 

1096 

1097 if ( 

1098 not text_lines 

1099 ): # ensure we display at least one cell - cf. issue #349 

1100 text_lines = [ 

1101 TextLine( 

1102 [], 

1103 text_width=0, 

1104 number_of_spaces=0, 

1105 align=align, 

1106 height=h, 

1107 max_width=w, 

1108 trailing_nl=False, 

1109 ) 

1110 ] 

1111 

1112 if max_line_height is None or len(text_lines) == 1: 

1113 line_height = h 

1114 else: 

1115 line_height = min(h, max_line_height) 

1116 

1117 box_required = fill or border 

1118 page_break_triggered = False 

1119 

1120 for text_line_index, text_line in enumerate(text_lines): 

1121 start_of_new_page = self._perform_page_break_if_need_be( 

1122 h + padding.bottom 

1123 ) 

1124 if start_of_new_page: 

1125 page_break_triggered = True 

1126 self.y += padding.top 

1127 

1128 if box_required and (text_line_index == 0 or start_of_new_page): 

1129 # estimate how many cells can fit on this page 

1130 top_gap = self.y # Top padding has already been added 

1131 bottom_gap = padding.bottom + self.b_margin 

1132 lines_before_break = int( 

1133 (self.h - top_gap - bottom_gap) // line_height 

1134 ) 

1135 # check how many cells should be rendered 

1136 num_lines = min( 

1137 lines_before_break, len(text_lines) - text_line_index 

1138 ) 

1139 box_height = max( 

1140 h - text_line_index * line_height, num_lines * line_height 

1141 ) 

1142 # render the box 

1143 x = self.x - (w / 2 if align == Align.X else 0) 

1144 draw_box_borders( 

1145 self, 

1146 x - padding.left, 

1147 self.y - padding.top, 

1148 x + w + padding.right, 

1149 self.y + box_height + padding.bottom, 

1150 border, 

1151 self.fill_color if fill else None, 

1152 ) 

1153 is_last_line = text_line_index == len(text_lines) - 1 

1154 self._render_styled_text_line( 

1155 text_line, 

1156 h=line_height, 

1157 new_x=new_x if is_last_line else XPos.LEFT, 

1158 new_y=new_y if is_last_line else YPos.NEXT, 

1159 border=0, # already rendered 

1160 fill=False, # already rendered 

1161 link=link, 

1162 padding=Padding(0, padding.right, 0, padding.left), 

1163 prevent_font_change=markdown, 

1164 ) 

1165 total_height += line_height 

1166 if not is_last_line and align == Align.X: 

1167 # prevent cumulative shift to the left 

1168 self.x = prev_x 

1169 

1170 if total_height < h: 

1171 # Move to the bottom of the multi_cell 

1172 if new_y == YPos.NEXT: 

1173 self.y += h - total_height 

1174 total_height = h 

1175 

1176 if page_break_triggered and new_y == YPos.TOP: 

1177 # When a page jump is performed and the requested y is TOP, 

1178 # pretend we started at the top of the text block on the new page. 

1179 # cf. test_multi_cell_table_with_automatic_page_break 

1180 prev_y = self.y 

1181 

1182 last_line = text_lines[-1] 

1183 if ( 

1184 last_line 

1185 and last_line.trailing_nl 

1186 and new_y in (YPos.LAST, YPos.NEXT) 

1187 ): 

1188 # The line renderer can't handle trailing newlines in the text. 

1189 self.ln() 

1190 

1191 if new_y == YPos.TOP: # We may have jumped a few lines -> reset 

1192 self.y = prev_y 

1193 elif new_y == YPos.NEXT: # move down by bottom padding 

1194 self.y += padding.bottom 

1195 

1196 if markdown: 

1197 self.font_style = prev_font_style 

1198 self.current_font = prev_current_font 

1199 self.underline = prev_underline 

1200 

1201 if ( 

1202 new_x == XPos.RIGHT 

1203 ): # move right by right padding to align outer RHS edge 

1204 self.x += padding.right 

1205 elif ( 

1206 new_x == XPos.LEFT 

1207 ): # move left by left padding to align outer LHS edge 

1208 self.x -= padding.left 

1209 

1210 output = MethodReturnValue.coerce(output) 

1211 return_value = () 

1212 if output & MethodReturnValue.PAGE_BREAK: 

1213 return_value += (page_break_triggered,) # type: ignore[assignment] 

1214 if output & MethodReturnValue.LINES: 

1215 # Bugfix: https://github.com/py-pdf/fpdf2/issues/1840, multi-cell-md-result 

1216 output_lines = self._join_text_lines(text_lines, markdown=markdown) 

1217 return_value += (output_lines,) # type: ignore[assignment] 

1218 if output & MethodReturnValue.HEIGHT: 

1219 return_value += (total_height + padding.top + padding.bottom,) # type: ignore[assignment] 

1220 if len(return_value) == 1: 

1221 return return_value[0] 

1222 return return_value # type: ignore[return-value] 

1223 

1224 

1225# Monkey-patch 

1226fpdf.fpdf.FPDF = FPDF # type: ignore[misc] 

1227fpdf.FPDF = FPDF # type: ignore[misc]