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

387 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-24 15:45 +0000

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

2 

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

4 

5from collections.abc import Iterator 

6from contextlib import contextmanager 

7import re 

8import types 

9from typing import Optional, Union 

10import warnings 

11 

12from fpdf.deprecation import get_stack_level 

13from fpdf.drawing_primitives import DeviceCMYK 

14from fpdf.drawing_primitives import DeviceGray 

15from fpdf.drawing_primitives import DeviceRGB 

16from fpdf.drawing_primitives import convert_to_device_color 

17from fpdf.enums import Align 

18from fpdf.enums import CharVPos 

19from fpdf.enums import PDFResourceType 

20from fpdf.enums import TextMode 

21from fpdf.enums import XPos 

22from fpdf.enums import YPos 

23from fpdf.fonts import CoreFont 

24from fpdf.fonts import TTFFont 

25import fpdf.fpdf 

26from fpdf.line_break import Fragment 

27from fpdf.line_break import TextLine 

28from fpdf.line_break import TotalPagesSubstitutionFragment 

29from fpdf.syntax import PDFArray 

30from fpdf.unicode_script import UnicodeScript 

31from fpdf.unicode_script import get_unicode_script 

32from fpdf.util import FloatTolerance 

33from fpdf.util import Padding 

34 

35 

36class FPDF(fpdf.fpdf.FPDF): 

37 __PATCHED__: bool = True 

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

39 MARKDOWN_ESCAPE_CHARACTER = "\\" 

40 MARKDOWN_LINK_REGEX = re.compile( 

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

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

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

44 re.DOTALL, 

45 ) 

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

47 

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

49 @contextmanager 

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

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

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

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

54 yield 

55 return 

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

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

58 self.page, 

59 self.pages_count, 

60 self.x, 

61 self.y, 

62 self._toc_inserted_pages, 

63 ) 

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

65 self._push_local_stack() 

66 try: 

67 yield 

68 finally: 

69 self._pop_local_stack() 

70 # restore location: 

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

72 del self.pages[p] 

73 self.page = prev_page 

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

75 self.set_xy(prev_x, prev_y) 

76 # restore inserted pages in toc 

77 self._toc_inserted_pages = prev_toc_inserted_pages 

78 # restore writing function: 

79 del self._out 

80 

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

82 if ( 

83 not markdown 

84 and not self.text_shaping 

85 and not self._fallback_font_ids 

86 ): 

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

88 for seq, fragment_text in enumerate( 

89 text.split(self.str_alias_nb_pages) 

90 ): 

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

92 yield TotalPagesSubstitutionFragment( 

93 self.str_alias_nb_pages, 

94 self._get_current_graphics_state(), 

95 self.k, 

96 ) 

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

98 yield Fragment( 

99 fragment_text, 

100 self._get_current_graphics_state(), 

101 self.k, 

102 ) 

103 return 

104 

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

106 return 

107 txt_frag: list[str] = [] 

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

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

110 in_strikethrough: bool = bool(self.strikethrough) 

111 in_underline: bool = bool(self.underline) 

112 current_fallback_font = None 

113 current_text_script = None 

114 

115 def frag() -> Fragment: 

116 nonlocal txt_frag, current_fallback_font, current_text_script 

117 gstate = self._get_current_graphics_state() 

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

119 "I" if in_italics else "" 

120 ) 

121 gstate.strikethrough = in_strikethrough 

122 gstate.underline = in_underline 

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

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

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

126 gstate.font_family = family 

127 gstate.font_style = style 

128 gstate.current_font = self.fonts[current_fallback_font] 

129 current_fallback_font = None 

130 current_text_script = None 

131 fragment = Fragment( 

132 txt_frag, 

133 gstate, 

134 self.k, 

135 ) 

136 txt_frag = [] 

137 return fragment 

138 

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

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

141 else: 

142 font_glyphs = [] 

143 

144 escape_next_marker = 0 

145 escape_run = 0 

146 

147 while text: 

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

149 escape_run += 1 

150 text = text[1:] 

151 continue 

152 

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

154 is_escape_target = text[:2] in ( 

155 self.MARKDOWN_BOLD_MARKER, 

156 self.MARKDOWN_ITALICS_MARKER, 

157 self.MARKDOWN_STRIKETHROUGH_MARKER, 

158 self.MARKDOWN_UNDERLINE_MARKER, 

159 ) 

160 if is_escape_target and escape_run % 2 == 1: 

161 for _ in range(escape_run - 1): 

162 txt_frag.append(self.MARKDOWN_ESCAPE_CHARACTER) 

163 if current_fallback_font: 

164 if txt_frag: 

165 yield frag() 

166 current_fallback_font = None 

167 escape_next_marker = 2 

168 escape_run = 0 

169 continue 

170 for _ in range(escape_run): 

171 txt_frag.append(self.MARKDOWN_ESCAPE_CHARACTER) 

172 escape_run = 0 

173 

174 is_marker = text[:2] in ( 

175 self.MARKDOWN_BOLD_MARKER, 

176 self.MARKDOWN_ITALICS_MARKER, 

177 self.MARKDOWN_STRIKETHROUGH_MARKER, 

178 self.MARKDOWN_UNDERLINE_MARKER, 

179 ) 

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

181 is_marker = False 

182 half_marker = text[0] 

183 text_script = get_unicode_script(text[0]) 

184 if text_script not in ( 

185 UnicodeScript.COMMON, 

186 UnicodeScript.UNKNOWN, 

187 current_text_script, 

188 ): 

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

190 yield frag() 

191 current_text_script = text_script 

192 

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

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

195 text[: len(self.str_alias_nb_pages)] 

196 == self.str_alias_nb_pages 

197 ): 

198 if txt_frag: 

199 yield frag() 

200 gstate = self._get_current_graphics_state() 

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

202 "I" if in_italics else "" 

203 ) 

204 gstate.strikethrough = in_strikethrough 

205 gstate.underline = in_underline 

206 yield TotalPagesSubstitutionFragment( 

207 self.str_alias_nb_pages, 

208 gstate, 

209 self.k, 

210 ) 

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

212 continue 

213 

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

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

216 if ( 

217 is_marker 

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

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

220 ): 

221 if txt_frag: 

222 yield frag() 

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

224 in_bold = not in_bold 

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

226 in_italics = not in_italics 

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

228 in_strikethrough = not in_strikethrough 

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

230 in_underline = not in_underline 

231 text = text[2:] 

232 continue 

233 

234 is_link = self.MARKDOWN_LINK_REGEX.match(text) 

235 if is_link: 

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

237 # BUGFIX: enable escaped square brackets in links 

238 link_text = self._MARKDOWN_LINK_TEXT_UNESCAPE.sub( 

239 r"\1", link_text 

240 ) 

241 if txt_frag: 

242 yield frag() 

243 gstate = self._get_current_graphics_state() 

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

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

246 "I" if in_italics else "" 

247 ) 

248 gstate.strikethrough = in_strikethrough 

249 gstate.underline = ( 

250 self.MARKDOWN_LINK_UNDERLINE or in_underline 

251 ) 

252 if self.MARKDOWN_LINK_COLOR: 

253 gstate.text_color = convert_to_device_color( 

254 self.MARKDOWN_LINK_COLOR 

255 ) 

256 try: 

257 page = int(link_dest) 

258 link_dest = self.add_link(page=page) 

259 except ValueError: 

260 pass 

261 yield Fragment( 

262 list(link_text), 

263 gstate, 

264 self.k, 

265 link=link_dest, 

266 ) 

267 continue 

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

269 self.is_ttf_font 

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

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

272 ): 

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

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

275 if fallback_font: 

276 if fallback_font == current_fallback_font: 

277 txt_frag.append(text[0]) 

278 text = text[1:] 

279 continue 

280 if txt_frag: 

281 yield frag() 

282 current_fallback_font = fallback_font 

283 txt_frag.append(text[0]) 

284 text = text[1:] 

285 continue 

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

287 if txt_frag: 

288 yield frag() 

289 current_fallback_font = None 

290 txt_frag.append(text[0]) 

291 text = text[1:] 

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

293 escape_next_marker -= 1 

294 if escape_next_marker == 0: 

295 yield frag() 

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

297 for _ in range(escape_run): 

298 txt_frag.append(self.MARKDOWN_ESCAPE_CHARACTER) 

299 escape_run = 0 

300 if txt_frag: 

301 yield frag() 

302 

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

304 def _render_styled_text_line( 

305 self, 

306 text_line: TextLine, 

307 h: Optional[float] = None, 

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

309 new_x: XPos = XPos.RIGHT, 

310 new_y: YPos = YPos.TOP, 

311 fill: bool = False, 

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

313 center: bool = False, 

314 padding: Optional[Padding] = None, 

315 prevent_font_change: bool = False, 

316 ) -> bool: 

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

318 warnings.warn( 

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

320 stacklevel=get_stack_level(), 

321 ) 

322 border = 1 

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

324 border = 1 

325 

326 if padding is None: 

327 padding = Padding(0, 0, 0, 0) 

328 l_c_margin = r_c_margin = float(0) 

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

330 l_c_margin = self.c_margin 

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

332 r_c_margin = self.c_margin 

333 

334 styled_txt_width = text_line.text_width 

335 if not styled_txt_width: 

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

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

338 styled_txt_width += unscaled_width 

339 

340 w = text_line.max_width 

341 if w is None: 

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

343 raise ValueError( 

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

345 ) 

346 w = styled_txt_width + l_c_margin + r_c_margin 

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

348 w = self.w - self.r_margin - self.x 

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

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

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

352 self.x -= w / 2 

353 

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

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

356 fragments = text_line.get_ordered_fragments() 

357 for frag in fragments: 

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

359 max_font_size = frag.font_size 

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

361 h = max_font_size 

362 page_break_triggered = self._perform_page_break_if_need_be(h) 

363 sl: list[str] = [] 

364 

365 k = self.k 

366 

367 # pre-calc border edges with padding 

368 

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

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

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

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

373 

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

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

376 sl.append( 

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

378 ) 

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

380 sl.append( 

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

382 ) 

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

384 

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

386 if "L" in border: 

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

388 if "T" in border: 

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

390 if "R" in border: 

391 sl.append( 

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

393 ) 

394 if "B" in border: 

395 sl.append( 

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

397 ) 

398 

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

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

401 

402 s_start = self.x 

403 s_width: float = 0 

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

405 current_ws = frag_ws = 0.0 

406 current_lift = 0.0 

407 current_char_vpos = CharVPos.LINE 

408 current_font = self.current_font 

409 current_font_size_pt = self.font_size_pt 

410 current_font_style = self.font_style 

411 current_text_mode = self.text_mode 

412 current_font_stretching = self.font_stretching 

413 current_char_spacing = self.char_spacing 

414 fill_color_changed = False 

415 last_used_color = self.fill_color 

416 if fragments: 

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

418 dx = w - l_c_margin - styled_txt_width 

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

420 dx = (w - styled_txt_width) / 2 

421 else: 

422 dx = l_c_margin 

423 s_start += dx 

424 word_spacing: float = 0 

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

426 word_spacing = ( 

427 w - l_c_margin - r_c_margin - styled_txt_width 

428 ) / text_line.number_of_spaces 

429 sl.append( 

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

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

432 ) 

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

434 not prevent_font_change 

435 and not self.current_font_is_set_on_page 

436 and self.current_font is not None 

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

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

439 ): 

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

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

442 sl.append( 

443 self._set_font_for_page( 

444 self.current_font, 

445 self.font_size_pt, 

446 wrap_in_text_object=False, 

447 ) 

448 ) 

449 underlines: list[ 

450 tuple[ 

451 float, 

452 float, 

453 CoreFont | TTFFont, 

454 float, 

455 DeviceRGB | DeviceGray | DeviceCMYK | None, 

456 ] 

457 ] = [] 

458 strikethroughs: list[ 

459 tuple[ 

460 float, 

461 float, 

462 CoreFont | TTFFont, 

463 float, 

464 DeviceRGB | DeviceGray | DeviceCMYK | None, 

465 ] 

466 ] = [] 

467 for i, frag in enumerate(fragments): 

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

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

470 if frag.text_color != last_used_color: 

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

472 last_used_color = frag.text_color 

473 assert last_used_color is not None 

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

475 fill_color_changed = True 

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

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

478 frag_ws = word_spacing * 100 / frag.font_stretching 

479 else: 

480 frag_ws = word_spacing 

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

482 current_font_stretching = frag.font_stretching 

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

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

485 current_char_spacing = frag.char_spacing 

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

487 if not self.current_font_is_set_on_page: 

488 if prevent_font_change: 

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

490 current_font = frag.font 

491 current_font_size_pt = frag.font_size_pt 

492 current_font_style = frag.font_style 

493 sl.append( 

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

495 ) 

496 self._resource_catalog.add( 

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

498 ) 

499 current_char_vpos = frag.char_vpos 

500 else: 

501 # This is "global" to the page, 

502 # as it is rendered in the content stream 

503 # BEFORE the text_lines /fragments, 

504 # wrapped into BT / ET operators: 

505 current_font = self.current_font = frag.font 

506 current_font_size_pt = self.font_size_pt = ( 

507 frag.font_size_pt 

508 ) 

509 current_font_style = self.font_style = frag.font_style 

510 self._out( 

511 self._set_font_for_page( 

512 current_font, 

513 current_font_size_pt, 

514 ) 

515 ) 

516 current_char_vpos = frag.char_vpos 

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

518 current_font != frag.font 

519 or current_font_size_pt != frag.font_size_pt 

520 or current_font_style != frag.font_style 

521 or current_char_vpos != frag.char_vpos 

522 ): 

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

524 current_font = frag.font 

525 current_font_size_pt = frag.font_size_pt 

526 current_font_style = frag.font_style 

527 sl.append( 

528 self._set_font_for_page( 

529 current_font, 

530 current_font_size_pt, 

531 wrap_in_text_object=False, 

532 ) 

533 ) 

534 current_char_vpos = frag.char_vpos 

535 lift = frag.lift 

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

537 # Use text rise operator: 

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

539 current_lift = lift 

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

541 frag.text_mode != TextMode.FILL 

542 or frag.text_mode != current_text_mode 

543 ): 

544 current_text_mode = frag.text_mode 

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

546 

547 r_text = frag.render_pdf_text( 

548 frag_ws, 

549 current_ws, 

550 word_spacing, 

551 self.x + dx + s_width, 

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

553 self.h, 

554 ) 

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

556 sl.append(r_text) 

557 

558 frag_width = frag.get_width( 

559 initial_cs=i != 0 

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

561 if frag.underline: 

562 underlines.append( 

563 ( 

564 self.x + dx + s_width, 

565 frag_width, 

566 frag.font, 

567 frag.font_size, 

568 frag.text_color, 

569 ) 

570 ) 

571 if frag.strikethrough: 

572 strikethroughs.append( 

573 ( 

574 self.x + dx + s_width, 

575 frag_width, 

576 frag.font, 

577 frag.font_size, 

578 frag.text_color, 

579 ) 

580 ) 

581 if frag.link: 

582 self.link( 

583 x=self.x + dx + s_width, 

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

585 w=frag_width, 

586 h=frag.font_size, 

587 link=frag.link, 

588 ) 

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

590 current_ws = frag_ws 

591 s_width += frag_width 

592 

593 sl.append("ET") 

594 

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

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

597 if underlines: 

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

599 # Change color of the underlines 

600 if text_color != last_used_color: 

601 last_used_color = text_color 

602 assert last_used_color is not None 

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

604 fill_color_changed = True 

605 sl.append( 

606 self._do_underline( 

607 start_x, 

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

609 width, 

610 font, 

611 ) 

612 ) 

613 if strikethroughs: 

614 for ( 

615 start_x, 

616 width, 

617 font, 

618 font_size, 

619 text_color, 

620 ) in strikethroughs: 

621 # Change color of the strikethroughs 

622 if text_color != last_used_color: 

623 last_used_color = text_color 

624 assert last_used_color is not None 

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

626 fill_color_changed = True 

627 sl.append( 

628 self._do_strikethrough( 

629 start_x, 

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

631 width, 

632 font, 

633 ) 

634 ) 

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

636 self.link( 

637 self.x + dx, 

638 self.y 

639 + (0.5 * h) 

640 - ( 

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

642 ), 

643 styled_txt_width, 

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

645 link, 

646 ) 

647 

648 if sl: 

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

650 # in a local context. 

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

652 if ( 

653 current_ws != 0.0 

654 or current_lift != 0.0 

655 or current_char_vpos != CharVPos.LINE 

656 or current_font != self.current_font 

657 or current_font_size_pt != self.font_size_pt 

658 or current_font_style != self.font_style 

659 or current_text_mode != self.text_mode 

660 or fill_color_changed 

661 or current_font_stretching != self.font_stretching 

662 or current_char_spacing != self.char_spacing 

663 ): 

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

665 else: 

666 s = " ".join(sl) 

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

668 self._out(s) 

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

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

671 self._lasth = h or self.font_size 

672 

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

674 if new_x == XPos.RIGHT: 

675 self.x += w 

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

677 self.x = s_start 

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

679 self.x = s_start + s_width 

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

681 if s_width: 

682 self.x = s_start + s_width - r_c_margin 

683 else: 

684 self.x = s_start 

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

686 self.x = s_start + s_width / 2.0 

687 elif new_x == XPos.LMARGIN: 

688 self.x = self.l_margin 

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

690 self.x = self.w - self.r_margin 

691 

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

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

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

695 self.y += h 

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

697 self.y = self.t_margin 

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

699 self.y = self.h - self.b_margin 

700 

701 return page_break_triggered 

702 

703 

704# Monkey-patch 

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

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