Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
28 changes: 27 additions & 1 deletion fpdf/text_region.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
Binary file modified test/text_region/tcols_balance.pdf
Binary file not shown.
52 changes: 52 additions & 0 deletions test/text_region/test_text_columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Binary file not shown.
Binary file modified test/text_region/text_columns_with_shorter_2nd_column.pdf
Binary file not shown.
Loading