fpdf2 multi-page PDF table with repeated headers and page numbers: working versions (tested 2026-10-16)
Build a table that flows across pages with fpdf2, repeat the heading row on every page and stamp Page X/N footers, then assert on the rendered output offline.
passed6 passed · 0 failed2026-10-16 09:57 UTC- Status
- passed
- Last passed
- 2026-10-16 09:57 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
fpdf2@2.8.9pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38883899161
The code
Exactly the files that passed. Your agent can pull them with get_recipe("fpdf2-multipage-table") (install), or see them on GitHub.
requirements.txt
fpdf2==2.8.9 pytest==9.1.1
report.py
from fpdf import FPDF, FontFace
class Report(FPDF):
"""FPDF subclass that stamps a title header and a page-number footer."""
def __init__(self, title, **kwargs):
super().__init__(**kwargs)
self.title = title
def header(self):
self.set_font("helvetica", "B", 12)
self.cell(0, 8, self.title, align="C", new_x="LMARGIN", new_y="NEXT")
self.ln(2)
def footer(self):
self.set_y(-15)
self.set_font("helvetica", "I", 8)
self.cell(0, 10, f"Page {self.page_no()}/{{nb}}", align="C")
def build_report(title, rows, repeat_headings=1, alias="{nb}"):
"""Render a table that flows across pages, repeating the heading row."""
pdf = Report(title)
pdf.alias_nb_pages(alias)
pdf.set_auto_page_break(auto=True, margin=15)
pdf.set_font("helvetica", size=10)
pdf.add_page()
with pdf.table(
headings_style=FontFace(emphasis="BOLD", fill_color=(230, 230, 230)),
repeat_headings=repeat_headings,
) as table:
table.row(("id", "name", "value"))
for row in rows:
table.row(tuple(str(cell) for cell in row))
return pdf
tests/test_report.py
import re
import zlib
import pytest
from fpdf import FPDF, FPDFException
from report import build_report
def extract_text(pdf: bytes) -> str:
"""Decompress page streams and join the drawn text in document order."""
streams = []
for match in re.finditer(rb"stream\r?\n(.*?)\r?\nendstream", pdf, re.S):
try:
streams.append(zlib.decompress(match.group(1)))
except zlib.error:
continue
content = b"\n".join(streams)
return "".join(
m.group(1).decode("latin-1")
for m in re.finditer(rb"\(((?:\\.|[^\\()])*)\) Tj", content)
)
def rows(count):
return [(i, f"item-{i}", i * 3) for i in range(count)]
def test_single_page_numbers_the_pages():
pdf = build_report("Inventory", rows(3))
text = extract_text(bytes(pdf.output()))
assert pdf.pages_count == 1
assert "Inventory" in text
assert "Page 1/1" in text
assert "item-2" in text
def test_table_flows_to_more_pages_and_repeats_headings():
pdf = build_report("Inventory", rows(80))
text = extract_text(bytes(pdf.output()))
assert pdf.pages_count > 1
# the heading row is re-drawn once per page
assert text.count("name") == pdf.pages_count
assert f"Page 1/{pdf.pages_count}" in text
assert f"Page {pdf.pages_count}/{pdf.pages_count}" in text
assert "item-79" in text
def test_repeat_headings_zero_draws_heading_once():
pdf = build_report("Inventory", rows(80), repeat_headings=0)
text = extract_text(bytes(pdf.output()))
assert pdf.pages_count > 1
assert text.count("name") == 1
def test_disabling_the_alias_leaves_the_placeholder():
pdf = build_report("Inventory", rows(3))
pdf.alias_nb_pages(None)
text = extract_text(bytes(pdf.output()))
assert "{nb}" in text
assert "Page 1/{nb}" in text
def test_building_a_table_before_a_page_raises():
pdf = FPDF()
with pytest.raises(FPDFException, match="add_page"):
with pdf.table():
pass
def test_output_returns_pdf_bytes():
pdf = build_report("Inventory", rows(3))
out = pdf.output()
assert isinstance(out, (bytes, bytearray))
assert bytes(out).startswith(b"%PDF-")
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.