ScreenshotNeo

BlogHTML to image & PDF

How to Keep a Fixed Footer at the Bottom of Every PDF Page

Learn how to repeat a fixed PDF footer with ReportLab, HTML/CSS, authoring tools, and reliable layout checks.

By the ScreenshotNeo team30 September 20269 min read

How to Keep a Fixed Footer at the Bottom of Every PDF Page

Direct answer: draw the footer in the page-layout or page-rendering layer, not as a one-time body element. In ReportLab, use page callbacks or draw the footer before showPage(). In HTML-to-PDF, use the renderer’s supported paged-media footer mechanism. Reserve bottom margin space so flowing content cannot overlap the footer.

The correct implementation depends on how your PDF is produced: a ReportLab flowable document, a low-level canvas, HTML/CSS conversion, a word processor, or an already-exported PDF.

Workflow Recommended method What to verify
ReportLab SimpleDocTemplate onFirstPage and onLaterPages callbacks Bottom margin, first-page differences, page number state
ReportLab canvas.Canvas Draw footer before each showPage() Actual page size and coordinate origin
HTML converted to PDF @page margins, margin boxes, or running elements Renderer and version support
Word-processed source Insert a repeating footer before export Exported page breaks and accessibility
Existing PDF only PDF authoring or repair tool Every page, overlays, and tagged-PDF behavior

ReportLab’s canvas uses a lower-left origin, with coordinates measured in points. Calculate positions from the selected page size instead of assuming Letter or A4. The ReportLab graphics guide documents the canvas model, page sizes, and state handling. CSS Paged Media defines the page, margin, header, and footer model for paginated output in the W3C specification.

2. ReportLab flowing documents with page callbacks

For a SimpleDocTemplate, put footer drawing in a callback. The callback receives the canvas and document for each page. Pass the same function to onFirstPage and onLaterPages when the footer belongs on every page.

A page-level footer repeats independently from the flowing document body.
A page-level footer repeats independently from the flowing document body.
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import mm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak

OUTPUT = "report-with-footer.pdf"


def draw_footer(canvas, doc):
    canvas.saveState()
    page_width, page_height = A4
    footer_y = 12 * mm

    canvas.setStrokeColorRGB(0.75, 0.75, 0.75)
    canvas.setLineWidth(0.5)
    canvas.line(18 * mm, footer_y + 6 * mm, page_width - 18 * mm, footer_y + 6 * mm)

    canvas.setFont("Helvetica", 8)
    canvas.setFillColorRGB(0.3, 0.3, 0.3)
    canvas.drawString(18 * mm, footer_y, "Internal report")
    canvas.drawRightString(page_width - 18 * mm, footer_y, f"Page {doc.page}")
    canvas.restoreState()


styles = getSampleStyleSheet()
story = []
for section in range(1, 6):
    story.append(Paragraph(f"Section {section}", styles["Heading1"]))
    for paragraph in range(8):
        story.append(Paragraph(
            "This paragraph exists to demonstrate normal flowing content around a repeated footer. "
            "The footer is painted at page level and does not become part of the story.",
            styles["BodyText"]
        ))
        story.append(Spacer(1, 4 * mm))


doc = SimpleDocTemplate(
    OUTPUT,
    pagesize=A4,
    leftMargin=18 * mm,
    rightMargin=18 * mm,
    topMargin=18 * mm,
    bottomMargin=25 * mm,
)
doc.build(story, onFirstPage=draw_footer, onLaterPages=draw_footer)
print(f"Wrote {OUTPUT}")

The bottomMargin is deliberately larger than the footer’s drawing area. Without that reserved space, a paragraph can occupy the same vertical region as the footer. saveState() and restoreState() prevent footer fonts, colors, and line settings from leaking into later drawing operations. ReportLab documents this pattern in its user guide.

Different first-page treatment

Use one callback for the cover and another for later pages when the first page needs no footer or a different design:

doc.build(
    story,
    onFirstPage=draw_cover_footer,
    onLaterPages=draw_footer,
)

Keep the later-page callback independent of assumptions made by the cover. Check page one separately after export.

3. ReportLab low-level canvas

A low-level canvas gives you complete control, but you must draw the footer during your page loop. Call the footer function before showPage(); that method finishes the current page and starts the next one.

from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter
from reportlab.lib.units import mm

OUTPUT = "canvas-footer.pdf"
PAGE_SIZE = letter


def draw_footer(pdf, page_number):
    width, height = PAGE_SIZE
    y = 12 * mm
    pdf.saveState()
    pdf.setStrokeColorRGB(0.75, 0.75, 0.75)
    pdf.line(18 * mm, y + 6 * mm, width - 18 * mm, y + 6 * mm)
    pdf.setFont("Helvetica", 8)
    pdf.setFillColorRGB(0.3, 0.3, 0.3)
    pdf.drawString(18 * mm, y, "Generated report")
    pdf.drawRightString(width - 18 * mm, y, f"Page {page_number}")
    pdf.restoreState()


pdf = canvas.Canvas(OUTPUT, pagesize=PAGE_SIZE)
for page_number in range(1, 4):
    pdf.setFont("Helvetica", 16)
    pdf.drawString(25 * mm, 260 * mm, f"Content for page {page_number}")
    pdf.setFont("Helvetica", 10)
    pdf.drawString(25 * mm, 245 * mm, "Body content is painted before the page footer.")
    draw_footer(pdf, page_number)
    pdf.showPage()
pdf.save()
print(f"Wrote {OUTPUT}")

If you support multiple page sizes, pass the current width and height into draw_footer rather than using a module constant. This avoids misplaced footers when a document switches between Letter, A4, landscape, or custom dimensions.

4. HTML-to-PDF footers with CSS Paged Media

CSS Paged Media specifies page rules and margin regions. A renderer that supports bottom margin boxes can place a page counter there:

HTML footer rules depend on the features supported by the PDF renderer.
HTML footer rules depend on the features supported by the PDF renderer.
@page {
  size: A4;
  margin: 18mm 18mm 24mm 18mm;
  @bottom-right {
    content: "Page " counter(page) " of " counter(pages);
    font-size: 8pt;
    color: #555;
  }
}

html, body {
  margin: 0;
  font-family: sans-serif;
}

.report {
  break-after: page;
}

The CSS is standards-based, but support varies by converter. The Anvil example demonstrates page counters and repeating regions, while DocRaptor’s documentation describes running elements for repeating a complete footer. Confirm the exact feature set for your renderer and version before depending on margin boxes, running elements, or counter(pages).

Some HTML-to-PDF engines let you move an element into a page margin. A typical pattern is conceptually:

.footer {
  position: running(report-footer);
}

@page {
  @bottom-center {
    content: element(report-footer);
  }
}

This syntax is not portable across all open-source converters. If it is ignored, the footer may appear once in normal document flow or disappear entirely. Treat renderer documentation as authoritative.

5. Word processors and existing PDFs

If you still have the authored document, create the repeating footer in the authoring application before exporting. The W3C PDF14 technique describes headers and footers as pagination aids and identifies authoring tools such as Microsoft Word. For an exported PDF, a PDF authoring or repair tool can add or replace a footer across pages; the same W3C technique names Adobe Acrobat Pro’s Header & Footer tools as an example.

Editing the source is usually easier when available because it preserves text flow, page numbering, and accessibility structure. Post-export overlays are useful when the source is unavailable, but inspect every page for collisions and incorrect rotation.

6. Layout rules that prevent overlap

  1. Reserve the full footer height. Set a bottom margin larger than the footer’s text, rule, logo, and padding.
  2. Measure from the actual page size. Use the canvas width and height or the renderer’s page dimensions.
  3. Keep body content out of the footer zone. A footer drawn on top of content can hide selectable text and fail visual review.
  4. Handle long footer text. Wrap, reduce the font, or reserve two lines; never assume a short title.
  5. Account for rotation. Landscape pages and rotated existing pages need their displayed dimensions checked.
  6. Check first and last pages separately. Covers, appendices, and trailing blank pages often use different templates.
  7. Validate counters. Confirm that page labels match the physical page sequence, especially after inserting blank pages.
Symptom Likely cause Fix
Footer appears only once It was placed in body flow or only the first-page callback was supplied Use onLaterPages or draw before every showPage().
Text overlaps the footer Bottom margin is too small Increase the reserved bottom margin by the footer’s measured height plus padding.
Footer is off the page Hard-coded page dimensions or wrong coordinate origin Read the current page width and height; remember ReportLab’s origin is lower-left.
Page total is blank or wrong Renderer lacks counter(pages) support or performs one-pass rendering Use a supported total-page feature, a two-pass process, or omit the total.
HTML footer is ignored Converter does not implement margin boxes or running elements Check that engine’s documentation and choose a supported mechanism.
Footer styling changes body text Canvas state was not isolated Wrap footer drawing in saveState() and restoreState().
Footer vanishes on a rotated page Overlay coordinates use the unrotated box Normalize rotation or calculate coordinates from the displayed page box.
Footer is duplicated A flowable footer and page callback both render it Keep the footer in one layer only.
Tagged PDF has noisy repeated content Footer was added as ordinary reading content Use the authoring tool’s artifact or pagination settings and inspect the tagged structure.

8. Performance, reliability, and cost

Page callbacks and canvas drawing are inexpensive compared with layout, font embedding, image decoding, and HTML rendering. Keep footer work deterministic: avoid network requests, expensive image processing, or data queries inside a per-page callback. Load reusable fonts and assets once, then draw them repeatedly.

For reliable output, pin the PDF renderer version, define page size and margins explicitly, and retain a small visual regression set containing a one-page document, a long flowing document, a cover, a landscape page, and a page with long footer text. If page totals are required, use the renderer’s documented multi-pass or counter behavior rather than guessing.

Cost is mainly driven by the rendering engine, runtime, fonts, and document length. A fixed footer itself adds negligible processing, but a footer containing a high-resolution image increases file size on every page unless the PDF reuses the embedded resource.

9. Verify the generated PDF

  1. Open the PDF and inspect the first, middle, and last pages.
  2. Confirm the footer baseline is the same distance from the bottom edge.
  3. Copy body text near the bottom to detect hidden overlap.
  4. Test both short and long footer strings.
  5. Check page numbers after inserting or removing pages.
  6. Inspect rotated, landscape, and mixed-size pages if your workflow allows them.
  7. For accessible documents, inspect the tagged structure and ensure repeated pagination artifacts are handled according to your authoring tool’s guidance.

10. Or skip the browser setup

If your workflow starts with a web page that must become a PDF or screenshot, ScreenshotNeo provides a single GET request instead of maintaining browser automation. Cookie and consent banners are accepted and removed before capture, along with 60+ known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and whether it was billed. The service can return PNG, JPEG, WebP, or PDF; its MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf.

See the ScreenshotNeo documentation for the complete option list and response details.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', data);

ScreenshotNeo includes full-page capture with lazy images loaded, custom CSS and JavaScript, waits for selectors, delays or network idle, device presets, arbitrary viewports, PDF paper size and margins, page ranges, headers, cookies, user agents, request blocking, caching, signed links, asynchronous jobs, bulk capture, usage reporting, and an OpenAPI specification. Its parameter names are compatible with those used by other screenshot APIs, which can simplify migration.

Plans include 1,000 screenshots per month free with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Create a free ScreenshotNeo account.

FAQ

Usually no. A page-level callback, margin box, or authoring-tool footer repeats it reliably and keeps body flow independent.

Yes. In ReportLab, pass the same callback to onFirstPage and onLaterPages. Use separate callbacks when the cover differs.

CSS Paged Media features are implemented unevenly. Check the chosen engine and version for margin boxes, running elements, and total-page counters.

No. A post-export tool usually adds an overlay. It can solve a missing source problem, but you must check collisions, rotation, page labels, and accessibility.

Where should page numbers be calculated?

Use the renderer’s page counter or the callback’s current page number. Avoid deriving numbers from content length, because page breaks can change.