ProvenPin Install

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

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.