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
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-24 15:45 +0000
1"""Fixes bugs in :py:class:`fpdf.FPDF`."""
3# ruff: noqa: E501, E713, RUF069, SIM102, SIM108, UP007, UP045
5from collections.abc import Iterator
6from contextlib import contextmanager
7import re
8import types
9from typing import Optional, Union
10import warnings
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
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"\\([\[\]])")
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
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
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
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
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 = []
144 escape_next_marker = 0
145 escape_run = 0
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
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
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
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
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
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()
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
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
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
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
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] = []
365 k = self.k
367 # pre-calc border edges with padding
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
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
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 )
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)
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")
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)
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
593 sl.append("ET")
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 )
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
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
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
701 return page_break_triggered
704# Monkey-patch
705fpdf.fpdf.FPDF = FPDF # type: ignore[misc]
706fpdf.FPDF = FPDF # type: ignore[misc]