Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
47 changes: 46 additions & 1 deletion fpdf/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,23 @@ 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, row_layout_info, cell_x_positions, **kwargs):
Comment thread
Neyhlo marked this conversation as resolved.
Outdated
def _render_table_row(
self,
i: int,
Expand All @@ -318,6 +335,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(
Expand Down Expand Up @@ -460,7 +500,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
Expand Down
57 changes: 57 additions & 0 deletions test/table/table_colspan_break.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from fpdf import FPDF

class TablePDF(FPDF):
def active_spanning_table(self, data_list, rows_per_page=25):
self.set_font("helvetica", size=11)
col_widths = [60, 60, 60]
self.set_fill_color(200, 200, 200)
headers = ["Header 1", "Header 2", "Header 3"]

def draw_header():
for i, header in enumerate(headers):
self.cell(col_widths[i], 10, header, border=1, fill=True)
self.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 self.get_y() > 250:
self.add_page()
draw_header()

display_label = f"{label}\n" if chunk_idx > 0 else label

y_start = self.get_y()
self.multi_cell(col_widths[0], 10, display_label, border=1, align='C')
y_after_label = self.get_y()

self.set_xy(self.l_margin + col_widths[0], y_start)
self.cell(col_widths[1], 10, "", border=1)
self.cell(col_widths[2], 10, chunk[0], border=1)
self.ln()

for item in chunk[1:]:
if self.get_y() > 270:
self.add_page()
draw_header()
self.multi_cell(col_widths[0], 10, f"{label}\n(suite)", border=1, align='C')
self.set_xy(self.l_margin + col_widths[0], self.get_y() - 10)

self.set_x(self.l_margin + col_widths[0])
self.cell(col_widths[1], 10, "", border=1)
self.cell(col_widths[2], 10, item, border=1)
self.ln()

pdf = TablePDF()
pdf.add_page()

data = [
{"label": "Data 1", "items": [f"Ligne {i}" for i in range(100)]}
]

pdf.active_spanning_table(data, rows_per_page=25)
pdf.output("spantest2.pdf")
Comment thread
Neyhlo marked this conversation as resolved.
Outdated
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
from fpdf import FPDF
class TablePDF(FPDF):
def active_spanning_table(self, data_list, rows_per_page=25):
self.set_font("helvetica", size=11)
col_widths = [60, 60, 60]
self.set_fill_color(200, 200, 200)
headers = ["Header 1", "Header 2", "Header 3"]
def draw_header():
for i, header in enumerate(headers):
self.cell(col_widths[i], 10, header, border=1, fill=True)
self.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 self.get_y() > 250:
self.add_page()
draw_header()
display_label = f"{label}\n" if chunk_idx > 0 else label
y_start = self.get_y()
self.multi_cell(col_widths[0], 10, display_label, border=1, align='C')
y_after_label = self.get_y()
self.set_xy(self.l_margin + col_widths[0], y_start)
self.cell(col_widths[1], 10, "", border=1)
self.cell(col_widths[2], 10, chunk[0], border=1)
self.ln()
for item in chunk[1:]:
if self.get_y() > 270:
self.add_page()
draw_header()
self.multi_cell(col_widths[0], 10, f"{label}\n(suite)", border=1, align='C')
self.set_xy(self.l_margin + col_widths[0], self.get_y() - 10)
self.set_x(self.l_margin + col_widths[0])
self.cell(col_widths[1], 10, "", border=1)
self.cell(col_widths[2], 10, item, border=1)
self.ln()
pdf = TablePDF()
pdf.add_page()
data = [
{"label": "Data 1", "items": [f"Ligne {i}" for i in range(100)]}
]
pdf.active_spanning_table(data, rows_per_page=25)
pdf.output("spantest2.pdf")
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, generate=True)

Loading