The ReportLab software provides a very useful route to creating PDF documents and reports from within Python in a nice platform independent fashion. It provides a basic ‘canvas’ interface for rendering directly to the document, and a more useful basic layout system called PLATYPUS. I guess they’ve already got their eyes on an O’Reilly animal. In this article I will cover some of the basics of getting started, with a few samples.
The installation procedure for all the necessary components is well covered in the documentation. In short, download the respective archives (assuming you already have Python installed), pick up the Python Imaging Library so all the test cases pass, and for each one run:
This should place everything in the appropriate location in your python tree.
Run the Test Cases
First things first, let’s run the test cases. They’re located either in the installation directory, or in the Lib/site-packages/reportlab/test directory. You can perform all the tests by running the ‘runAll.py’ python file in that directory.
$ cd /usr/local/Python.2.4.3/Lib/site-packages/reportlab/test $ python runAll.py ........................................................................ ....................................................................... ---------------------------------------------------------------------- Ran 143 tests in 45.886s OK Logs and output files written to folder ".../ReportLab_tmp/reportlab_test"
Now, if you’re like me, and running this on some screwball cygwin system, it won’t work at first, complaining that “” isn’t a valid directory. Fair enough, in runAll.py around line 37 add the following line:
except: folder = os.path.dirname(sys.argv) or os.getcwd() + folder = '.' from reportlab.lib.utils import isSourceDistro haveSRC = isSourceDistro()
If you would like to change where the test files are written to, then set your ‘TEMP’ environment variable to something useful, like, say, ‘.’ (at least for the duration of the testsuite), otherwise it will tell you where the PDF files are written after the test suite ends.
Choosing not to install the Imaging Library will generate some warnings and errors as various test cases try to use that functionality, but a majority of them should succeed. If they don’t, then you’ve failed the install process at some point. Try following the steps one more time, making sure the paths are set properly, and looking closely at the error generated.
Go take a look at some of the files generated, they should give you a good idea on what’s possible. There’s a lot of stuff here, so grab a piece of paper, and scribble some notes on what looks particularly useful for your own reports. This is your last chance to relax before we start working :)
Polyglots are wonderful things to look at…
A friend of mine once showed me a particular polyglot that, amongst other things, supported the PS format (which is sorta the PDF for the UNIX world, strangely enough also written by Adobe). What does this polyglot do? Same thing every programmer has done since the beginning of time, it displays “Hello World!”.
There’s a couple different routes we could take here to achieve our modest goal. The first, and simplest, is to directly render our chosen string to the document. While this benefits from simplicity, it’s also extremely well covered in the documentation. I’ll include it here to give us a springboard for related topics:
from reportlab.pdfgen import canvas c = canvas.Canvas("hello.pdf") c.drawString(100,100,"Hello World") c.showPage() c.save()
Not much too it, eh? Just to summarize, after importing in the ‘canvas’ module from the ReportLabs library, we create a Canvas. A Canvas is the blank surface upon which we paint, draw, print strings, and assorted other links. Then, we use one of the many drawing methods to put the string “Hello World” at the point 100 units by 100 units on the canvas. For anyone who’s used to working in screen graphics, the coordinate system here is a little strange. Instead of the origin (that is the (0, 0) location) being in the top left of the screen, Reportlab uses a origin in the bottom left. So as you move from left to right, you go from (0, 0) to (MAX, 0), and from bottom to top similarly (0, 0)->(0, MAX). So when we say we’re printing at (100, 100), we’re talking about 100 up from the bottom, and 100 in from the left.
The last two commands simply complete the page on the PDF, and save it to the file we specified when we created the Canvas.
Pretty simple, really.
You can draw on the core canvas any way you want, using all the functions listed in the documentation. If you’ve ever played with good ol’ Logo Turtle for the Apple II/e (ahh, I miss my Apple II… lemme dig out my emulator…) then a lot of this will seem familiar. Drawing a line can move a virtual cursor to a new point, which you can then connect to another point, so forth and so on. How 1990.
PLATYPUS and Us
While this is amusing, it’s not very useful for us since I, for one, don’t want to deal with pixel-level precision in my documents. I like tables, layout, spacers, automatic justification, and all those little things that make life easy for rendering. So let’s move on to something that is useful, namely the wonderfully partially documented core library called ‘PLATYPUS’. Yes, it does stand for something. No, I’m not going to bother telling you what it is.
Platypus provides a set of wrapper classes for the core drawing routines to facilitate things like sizing, tables, text layout, styles, page breaks, and is so useful for someone starting out that, as you can see, we’re going to jump right into it:
from reportlab.lib.styles import getSampleStyleSheet from reportlab.platypus import * # Our container for 'Flowable' objects elements =  # A large collection of style sheets pre-made for us styles = getSampleStyleSheet() # A basic document for us to write to 'rl_hello_platypus.pdf' doc = SimpleDocTemplate('rl_hello_platypus.pdf') # Create two 'Paragraph' Flowables and add them to our 'elements' elements.append(Paragraph("The Platypus", styles['Heading1'])) elements.append(Paragraph("Very <i>Special</i>!", styles['Normal'])) # Write the document to disk doc.build(elements)
A little hello world using Platypus
This produces text like the snippet you see to the right of this paragraph; a couple lines of text in different styles, both of which are pulled from the large list automatically generated by the
We start off with a couple of the usual imports. In case you’re wondering where I got these, I lifted them wholesale from the documentation. There isn’t, as far as I’ve found, any master index of what’s where, but by picking and choosing (or, if you’re like me, pointing and sweeping) you’ll get by.
elements list contains the
Flowable elements that will be rendered into the document. A
Flowable Object can be a Paragraph, a PageBreak, a Spacer, an Image, or any of a variety of other objects that derive from the base object and implement the proper methods. You can think of a
Flowable at the core as a frame in the document: it can stretch, shrink, have various styles be applied to it, and contain a variety of things.
In this example we create two
Paragraph objects, each with a different style, and add them to our elements list. You’ll note that in the second
Paragraph there are some basic markup commands, namely the normal italics we see on webpages all the time. The Platypus library supports a basic set of these markups including bold. You can read more about supported markups in Chapter 6.3 of the documentation.
Last, we tell the document to write the elements to the file, and we’ve built our first PDF using ReportLab!
Tables and Organization
Plain text is all good and well, but sometimes your reports need a little more. Lets take a look at adding Tables to your report. Platypus provides a
Table type that fits right in with the existing
Paragraph and other
Flowable types. If we take our example above, lets add a small table tallying the number of Snarks in cave. While the table itself is small, the example is larger, so I’ll quote it here and then talk about interesting pieces of it in depth.
from reportlab.lib.styles import getSampleStyleSheet from reportlab.platypus import * from reportlab.lib import colors # Our container for 'Flowable' objects elements =  # A large collection of style sheets pre-made for us styles = getSampleStyleSheet() # A basic document for us to write to 'rl_hello_table.pdf' doc = SimpleDocTemplate('rl_hello_table.pdf') elements.append(Paragraph("Wumpus vs Cave Population Report", styles['Title'])) data = [['Caves', 'Wumpus Population'], ['Deep Ditch', 50], ['Death Gully', 5000], ['Dire Straits', 600], ['Deadly Pit', 5], ['Conclusion', 'Run!']] # First the top row, with all the text centered and in Times-Bold, # and one line above, one line below. ts = [('ALIGN', (1,1), (-1,-1), 'CENTER'), ('LINEABOVE', (0,0), (-1,0), 1, colors.purple), ('LINEBELOW', (0,0), (-1,0), 1, colors.purple), ('FONT', (0,0), (-1,0), 'Times-Bold'), # The bottom row has one line above, and three lines below of # various colors and spacing. ('LINEABOVE', (0,-1), (-1,-1), 1, colors.purple), ('LINEBELOW', (0,-1), (-1,-1), 0.5, colors.purple, 1, None, None, 4,1), ('LINEBELOW', (0,-1), (-1,-1), 1, colors.red), ('FONT', (0,-1), (-1,-1), 'Times-Bold')] # Create the table with the necessary style, and add it to the # elements list. table = Table(data, style=ts) elements.append(table) # Write the document to disk doc.build(elements)
The latest Wumpus populations are a bit worrying.
This generates a nicely formatted table, a few lines at various points, and enough Wumpus’ to scare a Snark. Got your Arrows handy?
We start off with some of the usual setup, importing in the necessary modules and creating our
elements and style sheet, along with our basic Document We throw a bit of text on the screen as a heading, and then create the 2-dimensional array for the table data. There are a couple points I’d like to make regarding the data:
- While our array is fully populated, you can place
Nonevalues in for table elements to render them as blank strings.
- It’s currently open in discussion, but for now make sure your data is fully ‘normalized’, which is to say all rows have the same number of entries in them. We have a 2×6 grid here, each entry is filled in. If you have to leave something blank then use a
Nonevalue. If your code is no where near normalized, you can use these routines to either left or right pad with None’s as appropriate
def left_justify(data): return zip(*map(None, *data)) # For a memory bonus you can use: # max_col = reduce(lambda a,b: max(len(a),len(b)), data) # instead of what's below def right_justify(data): max_col = max(map(len, data)) return [(None,)*(max_col - len(entry)) + tuple(entry) for entry in data]
- It doesn’t have to be just text and strings in here. The
Tableclass supports other
Flowableobjects, as we will see in the next example, including text and graphics.
Tableaccepts any combination of Lists or Tuples (or new-classes emulating either of them) for it’s records, so feel free to directly pass values from your database or other data generation system. Just be careful that the text is properly escaped, otherwise you may end up with strange formatting errors if you try to process fields with angle-brackets.
You should also note that we have both the “header” row and the “conclusion” row in the data table. The
Table class is fairly agnostic about what the data means, so we pass it all together. The style section, which we declare next, creates the visual separation for the different conceptual areas of data.
Let’s take a look at this style specification now:
ts = [('ALIGN', (1,1), (-1,-1), 'CENTER'), ('LINEABOVE', (0,0), (-1,0), 1, colors.purple), ...
In order to render the table in an attractive fashion, we need to provide styles for different sections. The styles can contain a large list of modifiers, from font or alignment to BOX, GRID, and other line drawing commands. It’s important to note that each style gets processed in order, so further styles can overwrite previous styles leading to occasionally unexpected results. You can read more about the various styles available in Chapter 7 of the User Guide, but for the ones we’ve used here, they are pretty easy to interpret.
The coordinate system used starts from the top left (0, 0), and goes to the bottom right which can be represented either as (max-x, max-y) or as, in true Python form, (-1, -1). The latter syntax is most handy, as it doesn’t require any calculations for changing table sizes. After the coordinates there is a
weight, indicating the weight (or thickness) of the drawn line. Finally is the
color, which is either one of a set of predefined ones, or an RGB value.
It’s worth noting the second line for the bottom row’s styles and it’s few additional parameters. Those parameters are taken directly from the graphics line drawing controls discussed in Chapter 2.12 of the manual. For your edification, they are (in order after the color):
- Line Cap: This controls how the lines end, either rounded, flat, or pointy.
- Dash: Controls if the line is continuous, or separated into smaller segments.
- Join: Determines how two lines join together, similar to the cap argument.
- Count: Easily repeat lines a particular number of times.
- Space: The separation, in the same unit as the line weight, between repeated lines.
With that taken care of, we’re clear to add the table and render the PDF to disk.
How about a Chart?
Tables are all good and well, but rendering a nice chart to the screen can really aid understanding of your data. There’s a fairly extensive library of
Chart classes available, ranging from the mundane Bar and Line charts to the more interesting Spider chart. You can see all about them in the ReportLab Graphics Library Reference Documentation but for now, we’ll take a look at a small little example of a Bar Chart.
import string from reportlab.lib.styles import getSampleStyleSheet from reportlab.lib.units import cm from reportlab.graphics.charts.barcharts import VerticalBarChart from reportlab.graphics.shapes import Drawing from reportlab.platypus import * from reportlab.lib import colors # The box() method we declare here is a convience method to # demonstrate the spaceing various units take up in the # document by surrounding their area, appropriately enough, # with a box. def box(flt, center=False): box_style = [('BOX', (0, 0), (-1, -1), 0.5, colors.lightgrey)] if center: box_style += [('ALIGN', (0, 0), (-1, -1), 'CENTER')] return Table([[flt]], style=box_style) elements =  styles = getSampleStyleSheet() doc = SimpleDocTemplate('rl_wumpus_chart.pdf') elements.append(box(Paragraph("Wumpus Incident Sitings", styles['Title']))) # A Spacer, fairly intuitively, occupies a certain amount of Space # in the document. elements.append(Spacer(0, 0.5 * cm)) # Create a chart 'Drawing' chart = VerticalBarChart() # Set the starting point to be (0, 0). Changing this value changes # the position that the chart is rendered at within it's 'Drawing' # space, which we create below. chart.x = 0 chart.y = 0 # This determines the width, in centimeters (you could use 'inch' # as well) of the chart on the paper, as well as the height. chart_width = 8*cm chart_height = 3*cm chart.height = chart_height chart.width = chart_width # The vertical ticks will be labeled from 0 with a value every # 15 units, spaced so that the maximum value is 60. chart.valueAxis.valueMin = 0 chart.valueAxis.valueMax = 60 chart.valueAxis.valueStep = 15 # Put the labels at the bottom with an interval of 8 units, # -2 units below the axis, rotated 30 degrees from horizontal chart.categoryAxis.labels.dx = 8 chart.categoryAxis.labels.dy = -2 chart.categoryAxis.labels.angle = 30 # The text box's NE corner (top right) is located at the above # coordinate chart.categoryAxis.labels.boxAnchor = 'ne' # Our various horizontal axis labels catNames = ['Jan-06', 'Feb-06', 'Mar-06', 'Apr-06', 'May-06', 'Jun-06', 'Jul-06', 'Aug-06'] chart.categoryAxis.categoryNames = catNames # Some random data to populate the chart with. chart.data = [(8, 5, 20, 22, 37, 28, 30, 47)] # Since the 'Chart' class itself isn't a 'Flowable', we need to # create a 'Drawing' flowable to contain it. We want the basic # area to be the same size as the chart itself, so we will # initialize it with the chart's size. drawing = Drawing(chart_width, chart_height) drawing.add(chart) # Add the Chart containing Drawing to our elements list elements.append(box(drawing)) elements.append(Spacer(0, 2 * cm)) # Add the same chart again, but this time centered on the document. elements.append(box(drawing, True)) elements.append(Spacer(0, 2 * cm)) elements.append(box(Paragraph("All signs are worrying!", styles['Heading1']))) # Write the document to disk doc.build(elements)
Where’s my Vorpal Blade?
This is our longest example yet, so we’ll break it apart into several sections. In total, we write a phrase as a Title heading for the document, render two
VerticalBarCharts (well, really we cheat and use the same one twice!), one of them centered, and the other one aligned to the left. Most of the code is self explanatory, so I’ll just pick and choose the items that I think are comment-worthy and leave the rest as an exercise for the reader.
We start off with our usual set of imports, this time a little more extensive to pull in the various parts we are using here. If you are using the Imperial system (sadly), then you’ll probably want to import
inch instead of
cm from the
The first interesting bit of code we see is the nice little utility function up at the top called ‘box’:
def box(flt, center=False): box_style = [('BOX', (0, 0), (-1, -1), 0.5, colors.lightgrey)] if center: box_style += [('ALIGN', (0, 0), (-1, -1), 'CENTER')] return Table([[flt]], style=box_style) ... elements.append(box(Paragraph("Wumpus Incident Sitings", styles['Title'])))
box method is really very simple. It creates a
Table, with a simple style of a light grey box around the entire table, of one cell, with the passed in
Flowable as the single entry in that cell. Poor little
Optionally, it also adds the ‘CENTER” style to the
Table, which as you’ll see later can center the table within the screen. This is not the only way to center an object, several of the
Flowable classes accept a
hAlign parameter which, when set to
'CENTER' will result in the same behavior. However, the major advantage that
box() gives us in this example is the nice light grey box it puts around each objects reserved area. This makes visualizing the layout of the document much easier! An example of how we use the
box() method is included above.
It’s worth noting that if you put a
Paragraph into a
Table in this fashion and try to center it, it will not work. This is because alignment for
Paragraph objects is stored internally, thus you need to set the
style for the
Paragraph directly. In our case, we’re often using the
'Title' style which centers for us.
elements.append(Spacer(0, 0.5 * cm)) # Create a chart 'Drawing' chart = VerticalBarChart()
Spacer objects are particularly useful for separating out various
Flowable’s themselves, they can be placed anywhere you’d expect.
We create a
VerticalBarChart, which could have easily been one of the many other types, and continue on.
This specifies the chart’s drawing origin, relative to the
Drawing object we’ll talk about below. If you play around with these settings (Do! It’s so much fun!) you’ll see that it’s quite easy to have the chart draw over other objects on the page. There’s no clipping involved here, just blind placement. For now we’ll keep this at (0,0) which is, remember, the lower left corner.
Skipping down a bit, we set a variety of basic parameters, including the width and height in centimeters, the number of ticks on the vertical axis, and how the labels on the horizontal axis will be rendered. I would strongly recommend sitting down and playing with these values a bit to get a good feel for how they interact with the rest of the system.
Let’s take a look at the
Drawing object we use to wrap the
Drawing object is a
Flowable, whereas the
VerticalBarChart is not. This allows it to be rendered and formatted in line with the rest of the
Flowable objects we’ve been playing with so far, such as the
Paragraph and the
Table. We pass it two parameters, the chart_width and the chart_height, as it’s initial sizes. We could increase these values and take up a larger region, but for now we’ll keep it the same basic size. If we did increase it, visually it would move the chart down as the lower-left corner of the
Drawing object descended down the page.
The rest of the example is fairly self-explanatory, and you can look at the end result of your ReportLab Chart PDF. Download the code, play around with it, see how it works! That’s the best way to get a feel for layout tools like this!
Where to go from here?
Download and read the ReportLab documentation, paying especial attention to the User Guide Chapters 5, 6, and skim Chapters 7 and 8. Chapter 5 introduces Platypus in more detail, and Chapter 6’s coverage of the
Paragraph flowable is essential. Chapter 7, covering Tables and Table Styles is should be skimmed just so you know what’s available, and the same for Chapter 8’s quick coverage of a few of the other
Set up a few test cases to exercise different aspects of your usage. Once you’re comfortable with the various objects available, sit down and draw out on paper what you need your reports to look like. This gives you a nice reference to work from, and a concrete target. Look back through the various test cases if you need a particular effect but can’t find a reference to it in the documentation, chances are good most of the basic approaches you’ll be interested in will be represented somewhere in those tests! Last, if you’re stuck, or simply want someone to talk to, check out the ReportLab Mailing List.
Sorry, the comment form is closed at this time.