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 @@ -23,6 +23,7 @@ This can also be enabled programmatically with `warnings.simplefilter('default',
* visual gap in rendering subsequent text after `{nb}` page alias when text shaping is enabled - _cf._ [issue #1090](https://github.com/py-pdf/fpdf2/issues/1090) - thanks to @prateek-dagar
* `FPDF.write_html()` no longer raises `IndexError: pop from empty list` when a `<ul>` or `<ol>` element carries a `line-height` that is not a bare number (_e.g._ `line-height: normal` or `line-height: 1.5em`); such values are now ignored, and the default line height is used, consistently with `<p line-height="x">` - _cf._ [PR #1917](https://github.com/py-pdf/fpdf2/pull/1917)
* `FPDF.write_html()` now renders list bullets with correct font styling instead of inheriting preceding heading (e.g. `<h1>`) styles - _cf._ [issue #1921](https://github.com/py-pdf/fpdf2/issues/1921)
* table cells landing in the wrong column when a row was covered by several rowspans that did not start in column order - _cf._ [issue #1948](https://github.com/py-pdf/fpdf2/issues/1948)


## [2.8.8] - 2026-08-09
Expand Down
5 changes: 4 additions & 1 deletion fpdf/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,10 @@ def convert_spans(
cells.extend([None] * (cell.colspan - 1))
# now we can correctly interpret active_rowspans
remaining_rowspans: dict[int, int] = {}
for k, v in active_rowspans.items():
# ascending, so that an insert never shifts a placeholder that is
# already in place: the keys reach us in the order the rowspans
# started, which is not the column order
for k, v in sorted(active_rowspans.items()):
cells.insert(k, None)
if v > 1:
remaining_rowspans[k] = v - 1
Expand Down
Binary file added test/table/table_with_rowspan_out_of_order.pdf
Binary file not shown.
31 changes: 31 additions & 0 deletions test/table/test_table_rowspan.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,34 @@ def test_table_with_rowspan_images(tmp_path):
row.cell("Four\nlines\nof\ntext")

assert_pdf_equal(pdf, HERE / "table_with_rowspan_images.pdf", tmp_path)


def test_table_with_rowspan_out_of_order(tmp_path):
# A rowspan starting in a later column, followed by one starting in an
# earlier column, leaves the active rowspans in a different order than the
# columns they belong to - cf. issue #1948
pdf = FPDF()
pdf.set_font("Times", size=24)
pdf.add_page()
with pdf.table(text_align="CENTER", first_row_as_headings=False) as table:
row = table.row()
row.cell("A1")
row.cell("B1")
row.cell("C1", rowspan=3)
row.cell("D1")
row = table.row()
row.cell("A2", rowspan=2)
row.cell("B2")
row.cell("D2")
row = table.row()
row.cell("B3")
row.cell("D3")

# the last row keeps a placeholder under both rowspans, so D3 stays in the
# fourth column instead of sliding into the third one
assert [
cell.text if cell is not None else None for cell in table.rows[2].cells
] == [None, "B3", None, "D3"]
assert_pdf_equal(
pdf, HERE / "table_with_rowspan_out_of_order.pdf", tmp_path, generate=False
)