pythonreportlab

Changing the page numbering index in reportlab


UPDATE: See second code block for the solution

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,

<namedString id="page1">

<evalString default="XXX">

<pageNumber/>-+1

</evalString>

</namedString>...

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.

Before it is asked, no this answer is not what I am looking for (unless I tried it wrongly).


Below is the solution I came up with following Salt answer:

import os

from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.platypus import BaseDocTemplate, PageTemplate, Frame, Paragraph, PageBreak, NextPageTemplate
from reportlab.platypus.tables import TableStyle
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)    
        
        self.start_page_number = None
        
        frame_example = Frame(self.leftMargin - 0.5*inch, 
                              self.bottomMargin + 0.25*inch, 
                              self.width + 1*inch, 
                              self.height - 0.5*inch, 
                              id='example')
        
        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 style 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)
        
        
        defaultTableStyle = TableStyle([
                                ('VALIGN', (0,0), (-1,-1), 'TOP'),
                                ('RIGHTPADDING', (0,0), (-1,-1), 0),
                                ('LEFTPADDING', (0,0), (-1,-1), 10),
                                ('LINEBEFORE', (0,0), (0, -1), 2.5, colors.blue)
                            ])
        
        # Generate each page one by one
        self.doFrontPage()
        
        # Initialize the TOC
        toc = TableOfContents(dotsMinLevel=0)
        toc.levelStyles = [self.styleSheet['header_PS']]
        toc.tableStyle = defaultTableStyle
        
        # 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':
                if self.start_page_number is None:
                    self.start_page_number = self.page
                    
                page_number = self.page - self.start_page_number + 1
                self.notify('TOCEntry', (0, text, page_number))
                
                
    def doFootings(self, canvas, doc):
        # Create the footer
        x = A4[0]-128
        y = 40
        
        if self.start_page_number is not None:
            page_number = self.page - self.start_page_number + 1
            if page_number < 1:
                return
            canvas.saveState()
            txtFooting = "Page {}".format(int(page_number))
            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)

I followed Salt steps with the main difference being that I estimate the displayed page number instead of incrementing it. The displayed page number is then considered as nothing more than Reporlab's logical_page_number shifted by a fixed offset (self.start_page_number in my updated code). The +1 in page_number = self.page - self.start_page_number + 1 is only here to avoid indexing starting from 0.


Solution

  • ReportLab always uses physical page numbers, so to start numbering from a specific page (e.g., after the TOC), you need to manage it manually.

    Track your own logical_page_number, set a flag when the real content starts, and from that point on:

    This way, the visible numbering and TOC both start from 1 at the point you choose.