diff --git a/fpdf/table.py b/fpdf/table.py index d6bd2d91a..8a42bd286 100644 --- a/fpdf/table.py +++ b/fpdf/table.py @@ -307,6 +307,22 @@ def render(self) -> None: self._fpdf.l_margin = prev_l_margin self._fpdf.x = self._fpdf.l_margin + def _get_span_origin(self, row_idx, col_idx): + """ + Helper to find the origin cell of a rowspan when encountering a None placeholder. + Returns (cell, row_index_of_origin) + """ + # On remonte les lignes vers le haut pour trouver la cellule non-None + for k in range(row_idx - 1, -1, -1): + cell = self.rows[k].cells[col_idx] + if cell is not None: + # On a trouvé la cellule mère. + # On vérifie si son rowspan est assez grand pour arriver jusqu'à nous + if cell.rowspan > (row_idx - k): + return cell, k + return None, None + return None, None + def _render_table_row( self, i: int, @@ -318,6 +334,29 @@ def _render_table_row( y = self._fpdf.y # remember current y position, reset after each cell for j, cell in enumerate(row.cells): + if cell is None: + # If the cell is None, it might be the continuation of a rowspan. + origin_cell, origin_idx = self._get_span_origin(i, j) + + if origin_cell and origin_cell.border: + # Calculate position and width as if it were a real cell + col_width = self._get_col_width(origin_idx, j, origin_cell.colspan) + x1 = cell_x_positions[j] + y1 = self._fpdf.y + x2 = x1 + col_width + y2 = y1 + row_layout_info.height + + # Draw vertical borders (Left and Right) + # Note: We assume a full border (1) or 'LTRB'. + # Ideally we should parse origin_cell.border to check if L or R are required. + self._fpdf.line(x1, y1, x1, y2) # Left + self._fpdf.line(x2, y1, x2, y2) # Right + current_span_progress = i - origin_idx + 1 + total_span_rows = origin_cell.rowspan + is_end_of_span = current_span_progress == total_span_rows + is_last_table_row = i == len(self.rows) - 1 + if is_end_of_span or is_last_table_row: + self._fpdf.line(x1, y2, x2, y2) # Bottom if not isinstance(cell, Cell): continue self._render_table_cell( @@ -460,7 +499,12 @@ def _render_table_cell( x1 = self._fpdf.l_margin x2 = x1 + float(self._width) y1 = y1 - self._outer_border_margin[1] - y2 = y2 + self._outer_border_margin[1] + # If the cell height exceeds the page break threshold, we clip the border + # so that it does not visually extend beyond the page limit. + if y1 + cell_height > self._fpdf.page_break_trigger: + y2 = self._fpdf.page_break_trigger + else: + y2 = y1 + cell_height if j == 0: # lhs border diff --git a/test/table/table_colspan_break.py b/test/table/table_colspan_break.py new file mode 100644 index 000000000..9f5195f23 --- /dev/null +++ b/test/table/table_colspan_break.py @@ -0,0 +1,63 @@ +from fpdf import FPDF +from pathlib import Path +from test.conftest import assert_pdf_equal + +HERE = Path(__file__).resolve().parent + +def test_table_colspan_break(tmp_path): + + def active_spanning_table(pdf, data_list, rows_per_page=25): + pdf.set_font("helvetica", size=11) + col_widths = [60, 60, 60] + pdf.set_fill_color(200, 200, 200) + headers = ["Header 1", "Header 2", "Header 3"] + + def draw_header(): + for i, header in enumerate(headers): + pdf.cell(col_widths[i], 10, header, border=1, fill=True) + pdf.ln() + + draw_header() + + for group in data_list: + items = group['items'] + label = group['label'] + for chunk_idx in range(0, len(items), rows_per_page): + chunk = items[chunk_idx:chunk_idx + rows_per_page] + if pdf.get_y() > 250: + pdf.add_page() + draw_header() + + display_label = f"{label}\n" if chunk_idx > 0 else label + + y_start = pdf.get_y() + pdf.multi_cell(col_widths[0], 10, display_label, border=1, align='C') + y_after_label = pdf.get_y() + + pdf.set_xy(pdf.l_margin + col_widths[0], y_start) + pdf.cell(col_widths[1], 10, "", border=1) + pdf.cell(col_widths[2], 10, chunk[0], border=1) + pdf.ln() + + for item in chunk[1:]: + if pdf.get_y() > 270: + pdf.add_page() + draw_header() + pdf.multi_cell(col_widths[0], 10, f"{label}\n(suite)", border=1, align='C') + pdf.set_xy(pdf.l_margin + col_widths[0], pdf.get_y() - 10) + + pdf.set_x(pdf.l_margin + col_widths[0]) + pdf.cell(col_widths[1], 10, "", border=1) + pdf.cell(col_widths[2], 10, item, border=1) + pdf.ln() + + pdf = FPDF() + pdf.add_page() + + data = [ + {"label": "Data 1", "items": [f"Ligne {i}" for i in range(100)]} + ] + + active_spanning_table(pdf, data, rows_per_page=25) + + assert_pdf_equal(pdf, HERE / "table_colspan_break.pdf", tmp_path)