Changing the page numbering index in reportlab

The question may seem simple but I haven’t been able to find anything related to it:

How to start the page numbering index at a given page?

For example, let’s consider a document composed of a front page, a table of content and then the document’s content itself. How can I start the page numbering at the first section of this document instead of starting it at the first page?

Note that I am not trying not to display the page number (which is trivial) but change which page is considered as the first page.

Below is an example:

import os

from reportlab.lib.pagesizes import A4
from reportlab.platypus import BaseDocTemplate, PageTemplate, Frame, Paragraph, PageBreak, NextPageTemplate
from reportlab.platypus.tableofcontents import TableOfContents
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch  # Importing inch unit


class Example(BaseDocTemplate):
    
    def __init__(self, fname, **kwargs):
        super().__init__(fname, **kwargs)    
        
        
        frame_example = Frame(self.leftMargin - 0.5*inch, 
                              self.bottomMargin + 0.25*inch, 
                              self.width + 1*inch, 
                              self.height - 0.5*inch, 
                              id='example')
        
        # Define some page templates to display the page number
        self.addPageTemplates([PageTemplate(id='front_page', frames=frame_example, onPage=self.doFootingsFrontPage),
                               PageTemplate(id='other_pages', frames=frame_example, onPage=self.doFootings)])
        self.elements = []
        
        # Defining some basic paragrah styles for the example
        ps1 = ParagraphStyle(fontName='Helvetica', fontSize=14, name='frontPage_PS',
                             leftIndent=20, firstLineIndent=-20, spaceBefore=5, leading=16)
        
        ps2 = ParagraphStyle(fontName='Helvetica', fontSize=12, name='header_PS',
                             leftIndent=20, firstLineIndent=-20, spaceBefore=5, leading=16)
        
        ps3 = ParagraphStyle(fontName='Helvetica', fontSize=10, name='text_PS',
                             leftIndent=20, firstLineIndent=-20, spaceBefore=5, leading=16)
        
        # Storing the styles into a class attribute so that we can call them later on
        self.styleSheet = getSampleStyleSheet()
        for style in [ps1, ps2, ps3]:
            self.styleSheet.add(style)

        # Generate the front page
        self.doFrontPage()
        
        # Initialize the TOC
        toc = TableOfContents(dotsMinLevel=0)
        toc.levelStyles = [self.styleSheet['header_PS']]
        
        # Add the TOC
        self.elements.append(toc)
        self.elements.append(PageBreak())
        
        for n in range(2):
            self.doOtherPage(n)
        
        # Build the document
        self.multiBuild(self.elements)
        
        
    def afterFlowable(self, flowable):
        "Registers TOC entries."
        if flowable.__class__.__name__ == 'Paragraph':
            text = flowable.getPlainText()
            style = flowable.style.name
            if style == 'header_PS':
                self.notify('TOCEntry', (0, text, self.page))
                
    def doFootings(self, canvas, doc):
        # Create the footer
        x = A4[0]-128
        y = 40
        
        canvas.saveState()
        txtFooting = "Page {}".format(int(canvas._pageNumber))
        canvas.drawString(x, y, txtFooting)
        canvas.restoreState()
        
    def doFootingsFrontPage(self, canvas, doc):
        # Create the footer
        x = 50
        y = 40
        
        canvas.saveState()
        txtFooting = "I am the front page - I dont want to have a page number"
        canvas.drawString(x, y, txtFooting)
        canvas.restoreState()
        
    def doFrontPage(self):
        txt = 'This is the front page'
        self.elements.append(Paragraph(txt, self.styleSheet['frontPage_PS']))
        self.elements.append(NextPageTemplate("other_pages"))
        self.elements.append(PageBreak())
        
    def doOtherPage(self, n):
        txt = 'Header {:.0f}'.format(n+1)
        self.elements.append(Paragraph(txt, self.styleSheet['header_PS']))
        
        txt ='Who stole my page number? I should be Page {:.0f}'.format(n+1)
        
        for ii in range(10):
            self.elements.append(Paragraph(txt, self.styleSheet['text_PS']))
            
        self.elements.append(PageBreak())



if __name__ == '__main__':
    
    fname = os.path.abspath(os.path.join('.', 'example_reportlab.pdf'))
    Report = Example(fname)

In this example a PDF with 4 pages is generated, the front page, the table of content and two filling sections for the example. What I would like to obtain is to have the page numbering starting at the first section of the document (which is currently the page 3) and have it reflected into the Table of Content (i.e., Header 1 would be at page 1, Header 2 page 2 …). Or even better, be able to index the first pages i, ii, iii … then switch to a new numbering scheme 1, 2, 3 … when a certain part of the document is reached.

To my understanding it should be possible to do it; I found this example which states:

In real world documents there is another complication. You might have a fancy cover or front matter, and the logical page number 1 used in printing might not actually be page 1. Likewise, you might be doing a batch of customer docs in one RML job. So, in this case we have a more involved expression, and use the evalString tag to work out the number we want. In this example we did this by creating a name for the first page after the cover,

-+1

This says ‘work out the page number of the cover, add 1 and store that in the variable “page1” for future use’

But I haven’t been able to make anything out of it. I also had a look at the tests implemented by the library (test_platypus_toc.py for an example) but haven’t found anything relevant to my question.

I am using Python 3.12.9, and reportlab 3.6.13.

You’re on the right track, and your example is quite well-structured. What you’re trying to do — changing the logical page number mid-document, and having that reflected both in the page footer and the table of contents (TOC) — is entirely possible in ReportLab using the canvas.setPageNumber() method.

The trick is to:

  1. Reset the page number at the start of your main content section.
  2. Track a logical page number and use that in your footer.
  3. Ensure TOC entries are linked to the logical page number, not the physical one.

Solution Overview

Here’s what you need to do:

1. Introduce a logical page counter

Inside your document class, define a counter that tracks the “logical” page number:

self.logical_page_number = 0
self.start_logical_numbering_on_page = None  # Set later

2. Mark when logical numbering begins

Update doOtherPage() to flag the first content page:

def doOtherPage(self, n):
    if self.start_logical_numbering_on_page is None:
        self.start_logical_numbering_on_page = len(self.elements) + 1  # index in build sequence
    ...

3. Override beforePage() to manage page numbers

Use beforePage() to increment logical_page_number and track where to start logical numbering:

def beforePage(self):
    # This is called before drawing every page
    if self.page >= self.start_logical_numbering_on_page:
        self.logical_page_number += 1

4. Use the logical page number in footers

Modify your doFootings() function to display the logical page number instead:

def doFootings(self, canvas, doc):
    x = A4[0]-128
    y = 40
    canvas.saveState()
    txtFooting = "Page {}".format(self.logical_page_number)
    canvas.drawString(x, y, txtFooting)
    canvas.restoreState()

5. Update TOC entries to reflect logical pages

Your current afterFlowable() uses self.page, which is the physical page. Replace it with self.logical_page_number if you’re past the start of the logical section:

def afterFlowable(self, flowable):
    if flowable.__class__.__name__ == 'Paragraph':
        text = flowable.getPlainText()
        style = flowable.style.name
        if style == 'header_PS':
            logical_page = self.logical_page_number if self.page >= self.start_logical_numbering_on_page else None
            if logical_page:
                self.notify('TOCEntry', (0, text, logical_page))

Optional: Roman Numerals for Front Matter

If you’d like to display front matter page numbers like i, ii, iii, you can enhance your logic:

  • In doFootingsFrontPage(), use canvas.getPageNumber() and convert to Roman:
from reportlab.lib import utils

def to_roman(n):
    roman = utils.rl_get_module('reportlab.platypus.paragraph').toRoman
    return roman(n).lower()

def doFootingsFrontPage(self, canvas, doc):
    x = 50
    y = 40
    canvas.saveState()
    page_num = canvas.getPageNumber()
    txtFooting = to_roman(page_num)  # like "i", "ii", "iii"
    canvas.drawString(x, y, txtFooting)
    canvas.restoreState()

Summary

  • Use beforePage() to increment a logical page counter.
  • Use that counter in doFootings() to show custom page numbers.
  • Feed that same number to the TOC via afterFlowable().

This gives you precise control over page numbering, just like LaTeX’s \pagenumbering{}.