If you benefit from web2py hope you feel encouraged to pay it forward by contributing back to society in whatever form you choose!

ReportLab II


I wanted to generate a simple booking confirmation PDF. Below is the result. ReportLab is certainly powerful enough to create these kind of docs. It is very flexible, but this flexibiliy comes at the price of some verbosity. I created a simple wrapper module to meet my requirements and perhaps yours. If you are a diver, book your next scuba holiday on web2py at www.unlogged.co.uk. :)




The module I wrote consists of the following files:



   |-- common.py   # common constants such as H1, H2, ... for headers 

   |-- __init__.py # the main PDF class

   |-- theme.py    # DefaultTheme class you can extend to match your needs

   |-- util.py     # currently one helper to calculate table column widths


Here's a quick example of how I used it to create the PDF above. For the module source please see further below.



# this is a helper in my controller
# it takes the divecentre (dc) record and the shopping cart
def confirmation_pdf(dc, cart):

    # Let's import the wrapper
    import pdf
    from pdf.theme import colors, DefaultTheme

    # and define a constant
    TABLE_WIDTH = 540 # this you cannot do in rLab which is why I wrote the helper initially

    # then let's extend the Default theme. I need more space so I redefine the margins
    # also I don't want tables, etc to break across pages (allowSplitting = False)
    # see http://www.reportlab.com/docs/reportlab-userguide.pdf
    class MyTheme(DefaultTheme):
        doc = {
            'leftMargin': 25,
            'rightMargin': 25,
            'topMargin': 20,
            'bottomMargin': 25,
            'allowSplitting': False
    # let's create the doc and specify title and author
    doc = pdf.Pdf('Booking Confirmation', 'unlogged.co.uk')

    # now we apply our theme

    # time to add the logo at the top right
    logo_path = request.folder + 'static/unlogged/images/logo-2.png'
    doc.add_image(logo_path, 180, 67, pdf.RIGHT)

    # give me some space
    # this header defaults to H1
    doc.add_header('Booking confirmation')

    # here's how to add a paragraph
    doc.add_paragraph("We are pleased to confirm your reservation with <b>%s</b>...")

    # a subheader - H2
    doc.add_header("Divecenter details", pdf.H2)

    # as in pre-css days we wrap the address and the Google Map Image in a table
    # my wrapper module just reexposes the reportlab Paragraph and Table classes. 
    # See __init__.py in the source section below 
    address = pdf.Paragraph("""
        GPS: %(latitude)s, %(longitude)s<br/><br/>
        Tel: %(tel)s<br/>
        """ % dc, MyTheme.paragraph) # when we use the Paragraph class directly we have to specify a
                                     # style. Here we a using one from the underlying DefaultTheme.

    # let's get the map image
    gps = "%(latitude)s,%(longitude)s" % dc
    gmap = pdf.Image("http://maps.google.com/maps/api/staticmap?center=" +
        gps + "&zoom=13&markers=color:orange|" + gps +
        "&size=250x150&sensor=false", 250,150)

    # and add both the address and the map wrapped in a table to our doc
    doc.add(pdf.Table([[address,gmap]], style=[('VALIGN', (0,0), (-1,-1), 'TOP')])) # UGLY inline stuff

    doc.add_header('Diver details', pdf.H2)

   # let's move on to the divers table

    diver_table = [['Name', 'Qualification', 'Last dive']] # this is the header row 

    for diver in cart.get_divers():

        diver_table.append([diver.name, diver.qualification, diver.last_dive]) # these are the other rows

    doc.add_table(diver_table, TABLE_WIDTH)


    # all the rest I omitted here but you get the picture.

    # read the reportLab docs and the source below to figure out how to tewak things.

    # again, see http://www.reportlab.com/docs/reportlab-userguide.pdf

    # ...

    # ...


    return doc.render()


Look at my last slice (PDF with ReportLab) to see how to set the response headers in order to show the pdf in the browser or to init a download.

Module source



    Author: H.C. v. Stockhausen < hc at vst.io >
    Date:    2012-10-14

# Header levels
H1, H2, H3, H4, H5, H6 = 1, 2, 3, 4, 5, 6 

# List styles
UL, OL = 0, 1

# Alignment




    __init__.py "

    Author: H.C. v. Stockhausen < hc at vst.io >
    Date:    2012-10-14


import cStringIO
import urllib
from reportlab.platypus.doctemplate import SimpleDocTemplate
from reportlab.platypus.flowables import Image
from reportlab.platypus import Paragraph, Spacer, KeepTogether
from reportlab.lib import colors
from reportlab.platypus.tables import Table, TableStyle

from theme import DefaultTheme
from util import calc_table_col_widths
from common import *

class Pdf(object):

    story = []    
    theme = DefaultTheme
    def __init__(self, title, author):
        self.title = title
        self.author = author
    def set_theme(self, theme):
        self.theme = theme
    def add(self, flowable):
    def add_header(self, text, level=H1):
        p = Paragraph(text, self.theme.header_for_level(level))
    def add_spacer(self, height_inch=None):
        height_inch = height_inch or self.theme.spacer_height
        self.add(Spacer(1, height_inch)) # magic 1? no, first param not yet implemented by rLab guys
    def add_paragraph(self, text, style=None):
        style = style or self.theme.paragraph
        p = Paragraph(text, style)
    def add_list(self, items, list_style=UL):
        raise NotImplementedError
    def add_table(self, rows, width=None, col_widths=None, align=CENTER,
        style = self.theme.table_style + extra_style
        if width and col_widths is None: # one cannot spec table width in rLab only col widths
            col_widths = calc_table_col_widths(rows, width) # this helper calcs it for us
        table = Table(rows, col_widths, style=style, hAlign=align)
    def add_image(self, src, width, height, align=CENTER):
        img = Image(src, width, height)
        img.hAlign = align
    def add_qrcode(self, data, size=150, align=CENTER):
        "FIXME: ReportLib also supports QR-Codes. Check it out."
        src = "http://chart.googleapis.com/chart?"
        src += "chs=%sx%s&" % (size, size)
        src += "cht=qr&"
        src += "chl=" + urllib.quote(data)
        self.add_image(src, size, size, align)
    def render(self):
        buffer = cStringIO.StringIO()
        doc_template_args = self.theme.doc_template_args()
        doc = SimpleDocTemplate(buffer, title=self.title, author=self.author,
        pdf = buffer.getvalue()
        return pdf



    Author: H.C. v. Stockhausen < hc at vst.io >
    Date:   2012-10-14

from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import inch
from reportlab.lib import colors

from common import *

class DefaultTheme(object):

    _s = getSampleStyleSheet()
    doc = {
        'leftMargin': None,
        'rightMargin': None,
        'topMargin': None,
        'bottomMargin': None
    headers = {
         H1: _s['Heading1'],
         H2: _s['Heading2'],
         H3: _s['Heading3'],
         H4: _s['Heading4'],
         H5: _s['Heading5'],
         H6: _s['Heading6'],
    paragraph = _s['Normal'] 
    spacer_height = 0.25 * inch
    table_style = [
        ('ALIGN', (0,0), (-1,-1), 'LEFT'),
        ('VALIGN', (0,0), (-1,-1), 'TOP'),
        ('FONT', (0,0), (-1,0), 'Helvetica-Bold'),
        ('LINEBELOW', (0,0), (-1,0), 1, colors.black),
        ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#C0C0C0')),
        ('ROWBACKGROUNDS', (0,1), (-1, -1), [colors.white, colors.HexColor('#E0E0E0')])
    def doc_template_args(cls):
        return dict([(k, v) for k, v in cls.doc.items() if v is not None])
    def header_for_level(cls, level):
        return cls.headers[level]
    def __new__(cls, *args, **kwargs):
        raise TypeError("Theme classes may not be instantiated.")


    Author: H.C. v. Stockhausen < hc at vst.io >
    Date:   2012-10-14

def calc_table_col_widths(rows, table_width):
    max_chars_per_col = [0] * len(rows[0])
    for row in rows:
        for idx, col in enumerate(row):
            for line in str(col).split('\n'):
                max_chars_per_col[idx] = max(len(line),
    sum_chars = sum(max_chars_per_col)
    return [(x * table_width / sum_chars) for x in max_chars_per_col]




Related slices

Comments (1)

  • Login to post

  • 0
    kkdoc-10636 6 years ago

    Hi Hans Christian,

    which version of reportlab do you prefer?

    I have problems on debian 6.0.5 with build-essential installed:

    easy_install reportlab


    Best match: reportlab 2.6


    /tmp/easy_install-TdH3sF/reportlab-2.6/src/rl_addons/rl_accel/_rl_accel.c:1280: error: ‘ttfonts_add32L’ undeclared here (not in a function)
    /tmp/easy_install-TdH3sF/reportlab-2.6/src/rl_addons/rl_accel/_rl_accel.c:1281: error: ‘hex32’ undeclared here (not in a function)
    /tmp/easy_install-TdH3sF/reportlab-2.6/src/rl_addons/rl_accel/_rl_accel.c:1282: error: expected ‘}’ before ‘unicode2T1’

    error: Setup script exited with error: command 'gcc' failed with exit status 1


    replies (1)
    • hcvst 6 years ago


      I installed it quite some time ago. I am running 2.5.

      >>> import reportlab >>> reportlab.Version '2.5'

      But since this is a build error you should be able to find some help. I had a search for '‘ttfonts_add32L’ undeclared here' and it looks like you need to also 'apt-get install' your python-dev first.

      Regards, HC

Hosting graciously provided by:
Python Anywhere