Reportlab: How to Combine Static Content and Multipage Tables

This week I was trying to figure out how to make Reportlab do something I had never attempted before. Namely, I wanted to create about a half page’s worth of static text positioned exactly and then have a table of line items that could potentially fill the rest of the page and continue for N pages thereafter. The problem is that mixing Reportlab’s canvas object with flowables can be messy. Reportlab talks a little about using templates in its user guide, but it only shows how to add header and footer type information. That’s actually all the information I needed, but it took me quite a while to realize that. I asked about how to do this sort of thing on the Reportlab mailing list. At the time of this writing, no one on there told me how to do it. Anyway, I figured it out on my own and now I’m going to show you! If you’d like to follow along, you’ll probably need to go get a free copy of Reportlab yourself.

Diving Into the Code

reportlab_multipage

I had to go spelunking into Reportlab’s source code to figure out how to do this fun activity. All you really need to do is create a method in your class that does all your static canvas drawing. Then when you’re done, you create a list object with a Reportlab Spacer in it that tells Reportlab that it needs to skip over the area you drew in. Then you can create your Table object and add it to the list. Finally, you just need to build your document. Yes, if you’re new to Reportlab, that probably all sounded like Greek to you. It’s easier just to show you, so check out the code below:


from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle, TA_CENTER
from reportlab.lib.units import inch, mm
from reportlab.pdfgen import canvas
from reportlab.platypus import Paragraph, Table, SimpleDocTemplate, Spacer

########################################################################
class Test(object):
    """"""

    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        self.width, self.height = letter
        self.styles = getSampleStyleSheet()
        
    #----------------------------------------------------------------------
    def coord(self, x, y, unit=1):
        """
        http://stackoverflow.com/questions/4726011/wrap-text-in-a-table-reportlab
        Helper class to help position flowables in Canvas objects
        """
        x, y = x * unit, self.height -  y * unit
        return x, y
        
    #----------------------------------------------------------------------
    def run(self):
        """
        Run the report
        """
        self.doc = SimpleDocTemplate("test.pdf")
        self.story = [Spacer(1, 2.5*inch)]
        self.createLineItems()
        
        self.doc.build(self.story, onFirstPage=self.createDocument)
        print "finished!"
        
    #----------------------------------------------------------------------
    def createDocument(self, canvas, doc):
        """
        Create the document
        """
        self.c = canvas
        normal = self.styles["Normal"]
        
        header_text = "This is a test header"
        p = Paragraph(header_text, normal)
        p.wrapOn(self.c, self.width, self.height)
        p.drawOn(self.c, *self.coord(100, 12, mm))
        
        ptext = """Lorem ipsum dolor sit amet, consectetur adipisicing elit,
        sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 
        Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris 
        nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
        reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
        pariatur. Excepteur sint occaecat cupidatat non proident, sunt in 
        culpa qui officia deserunt mollit anim id est laborum."""
        
        p = Paragraph(ptext, style=normal)
        p.wrapOn(self.c, self.width-50, self.height)
        p.drawOn(self.c, 30, 700)
        
        ptext = """
        At vero eos et accusamus et iusto odio dignissimos ducimus qui 
        blanditiis praesentium voluptatum deleniti atque corrupti quos dolores 
        et quas molestias excepturi sint occaecati cupiditate non provident, 
        similique sunt in culpa qui officia deserunt mollitia animi, id est laborum
        et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. 
        Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit
        quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est,
        omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut 
        rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et 
        molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus,
        ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis
        doloribus asperiores repellat.
        """
        p = Paragraph(ptext, style=normal)
        p.wrapOn(self.c, self.width-50, self.height)
        p.drawOn(self.c, 30, 600)
        
    #----------------------------------------------------------------------
    def createLineItems(self):
        """
        Create the line items
        """
        text_data = ["Line", "DOS", "Procedure
/Modifier", "Description", "Units", "Billed
Charges", "Type1
Reductions", "Type2
Reductions", "Type3
Reductions", "Allowance", "Qualify
Code"] d = [] font_size = 8 centered = ParagraphStyle(name="centered", alignment=TA_CENTER) for text in text_data: ptext = "%s" % (font_size, text) p = Paragraph(ptext, centered) d.append(p) data = [d] line_num = 1 formatted_line_data = [] for x in range(200): line_data = [str(line_num), "04/12/2013", "73090", "Test Reflexes", "1", "131.00", "0.00", "0.00", "0.00", "0.00", "1234"] for item in line_data: ptext = "%s" % (font_size-1, item) p = Paragraph(ptext, centered) formatted_line_data.append(p) data.append(formatted_line_data) formatted_line_data = [] line_num += 1 table = Table(data, colWidths=[20, 40, 45, 120, 30, 40, 50, 50, 50, 50, 30]) self.story.append(table) #---------------------------------------------------------------------- if __name__ == "__main__": t = Test() t.run()

Now we’ll need to spend a little bit of time going over what’s happening here. First off, we import a whole bunch of various items from Reportlab. Next we create our Test class. We initialize a few things and then we start getting to the good stuff. The coord method is something fun I found on StackOverflow that helps a lot in positioning flowables on the canvas object. We’ll just skip that method and head on over to run. Here we create our document object and our story list. You’ll note that we’ve already put a Spacer into it that’s 2.5 inches in width. That’s the amount of space that is reserved for the canvas to draw in. Next we call our createLineItems method which creates a 200 row Table object that is added to our story.

Then we call the doc object’s build method and tell it to execute createDocument. As you may have guessed, build will actually create the PDF itself. The createDocument method contains the canvas drawing code. Once everything is done, we print out a message to stdout to let the user know that their new document is ready for viewing!

Wrapping Up

At this point, you have the knowledge needed to write a mash-up of static content and flowables. You might like to know that you can also pass the build method an onLastPages parameter which tells it to call another method of your own. Most examples that show onFirstPage and onLastPages use them for headers and footers. Maybe that’s their main purpose, but you can use them for yourself. If you go digging in the source, you’ll discover you can also pass a list of PageTemplates which could make really complex layouts that much easier. Anyway, I hope this article was helpful to you and that you’ll be able to use this new information in your own code. Have fun!

Related Articles

6 thoughts on “Reportlab: How to Combine Static Content and Multipage Tables”

Comments are closed.