diff --git a/CHANGELOG.md b/CHANGELOG.md index 80ef55326..5b4f163a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ This can also be enabled programmatically with `warnings.simplefilter('default', ## [2.8.8] - Not released yet ### Fixed +* font state (family, style, size, current font, and the page-level "font is set" flag) no longer leaks back onto the `FPDF` instance after a `text_columns()` / `text_region()` context exits, so a subsequent `pdf.cell()` / `pdf.write()` renders at the caller's font instead of the last paragraph's - _cf._ [issue #1804](https://github.com/py-pdf/fpdf2/issues/1804) * text rendering when the first text on a page starts with a fallback glyph - _cf._ [issue #1772](https://github.com/py-pdf/fpdf2/issues/1772) * preserve boundary-neutral formatting during bidirectional text preprocessing - _cf._ [issue #1779](https://github.com/py-pdf/fpdf2/issues/1779) * transform application on user space gradients - _cf._ [issue #1784](https://github.com/py-pdf/fpdf2/issues/1784) diff --git a/fpdf/text_region.py b/fpdf/text_region.py index 0129f7517..7dfa28a49 100644 --- a/fpdf/text_region.py +++ b/fpdf/text_region.py @@ -429,7 +429,33 @@ def __exit__( self.pdf.clear_text_region() self.pdf.page = self._page self.pdf._pop_local_stack() # pyright: ignore[reportPrivateUsage] - self.render() + # Preserve font state across `render()`. Internally, rendering each + # paragraph temporarily adjusts font_size_pt / font_family / + # font_style / current_font, but those mutations must not leak back + # onto the calling FPDF instance because the caller did not ask to + # change fonts. Cursor position and other render-produced state are + # intentionally left alone, since subsequent draws rely on them. + # Fixes GH issue #1804. + saved_font_family = self.pdf.font_family + saved_font_style = self.pdf.font_style + saved_font_size_pt = self.pdf.font_size_pt + saved_current_font = self.pdf.current_font + try: + self.render() + finally: + self.pdf.font_family = saved_font_family + self.pdf.font_style = saved_font_style + self.pdf.font_size_pt = saved_font_size_pt + self.pdf.current_font = saved_current_font + # `render()` may have emitted `Tf` operators to the page for + # paragraph-level fonts. After restoring the Python-side font + # attributes, also invalidate `current_font_is_set_on_page` so + # the next text operation re-emits a `Tf` for the restored + # (outer) font instead of inheriting whatever the last + # paragraph wrote to the page. Without this, `pdf.cell()` + # after the context silently renders at the inner paragraph's + # font size even though `pdf.font_size_pt` reads correctly. + self.pdf.current_font_is_set_on_page = False def _check_paragraph(self) -> None: if self._active_paragraph == "EXPLICIT": diff --git a/test/text_region/tcols_balance.pdf b/test/text_region/tcols_balance.pdf index c2e6a1c31..8b738091f 100644 Binary files a/test/text_region/tcols_balance.pdf and b/test/text_region/tcols_balance.pdf differ diff --git a/test/text_region/test_text_columns.py b/test/text_region/test_text_columns.py index ef72d6ef8..1f4ad4c5a 100644 --- a/test/text_region/test_text_columns.py +++ b/test/text_region/test_text_columns.py @@ -391,3 +391,55 @@ def test_text_columns_with_shorter_2nd_column(tmp_path): # issue 1442 pdf.write(text="More text after columns.") pdf.ln() assert_pdf_equal(pdf, HERE / "text_columns_with_shorter_2nd_column.pdf", tmp_path) + + +def test_tcols_font_size_does_not_leak(): + """Regression test for issue #1804. + + When a text_columns() block is the first text rendered on a page and + the paragraphs inside it change the font size, the caller's + font_size_pt should be unchanged after the context manager exits. + """ + pdf = FPDF() + pdf.add_page() + pdf.set_font("Helvetica", size=12) + size_before = pdf.font_size_pt + + cols = pdf.text_columns() + with cols: + pdf.set_font("Helvetica", size=24) + with cols.paragraph() as p: + p.write("Large heading") + pdf.set_font("Helvetica", size=10) + with cols.paragraph() as p: + p.write("Small body text") + + assert pdf.font_size_pt == size_before + + +def test_text_columns_restore_page_font_after_context(tmp_path): + """Second-layer regression test for issue #1804. + + Even with the Python-side font_size_pt restored, the PDF content stream + must also re-emit a `Tf` operator for the restored font on the next + text operation. Otherwise the page state still carries the last + paragraph's font selection and a following `pdf.cell()` silently + renders at the wrong size. This test exercises the full render path + rather than just the attribute, so the assertion is on the output PDF. + """ + pdf = FPDF() + pdf.add_page() + pdf.set_font("Helvetica", size=12) + + with pdf.text_columns() as cols: + pdf.set_font("Helvetica", size=24) + cols.write("Large heading") + pdf.set_font("Helvetica", size=10) + cols.write("Small body text") + + pdf.cell(text="after columns") + assert_pdf_equal( + pdf, + HERE / "text_columns_restore_page_font_after_context.pdf", + tmp_path, + ) diff --git a/test/text_region/text_columns_restore_page_font_after_context.pdf b/test/text_region/text_columns_restore_page_font_after_context.pdf new file mode 100644 index 000000000..0c35f1609 Binary files /dev/null and b/test/text_region/text_columns_restore_page_font_after_context.pdf differ diff --git a/test/text_region/text_columns_with_shorter_2nd_column.pdf b/test/text_region/text_columns_with_shorter_2nd_column.pdf index ab538789e..b09daf02b 100644 Binary files a/test/text_region/text_columns_with_shorter_2nd_column.pdf and b/test/text_region/text_columns_with_shorter_2nd_column.pdf differ