Getting Started with ReportLab's Canvas

ReportLab is a very powerful library. With a little effort, you can make pretty much any layout that you can think of. I have used it to replicate many complex page layouts over the years. In this tutorial, you will be learning how to use ReportLab's pdfgen package. You will discover how to do the following:

  • Draw text
  • Learn about fonts and text colors
  • Creating a text object
  • Draw lines
  • Draw various shapes

The pdfgen package is very low level. You will be drawing or "painting" on a canvas to create your PDF. The canvas gets imported from the pdfgen package. When you go to paint on your canvas, you will need to specify X/Y coordinates that tell ReportLab where to start painting. The default is (0,0) whose origin is at the lowest left corner of the page. Many desktop user interface kits, such as wxPython, Tkinter, etc, also have this concept. You can place buttons absolutely in many of these kits using X/Y coordinates as well. This allows for very precise placement of the elements that you are adding to the page.

The other item that you need to know is that when you are positioning an item in a PDF, you are positioning by the number of points you are from the origin. It's points, not pixels or millimeters or inches. Points! Let's take a look at how many points are on a letter-sized page:

>>> from reportlab.lib.pagesizes import letter
>>> letter
(612.0, 792.0)

Here you learn that a letter is 612 points wide and 792 points high. Let's find out how many points are in an inch and a millimeter, respectively:

>>> from reportlab.lib.units import inch
>>> inch
72.0
>>> from reportlab.lib.units import mm
>>> mm
2.834645669291339

This information will help you position your drawings on your painting. At this point, you're ready to create a PDF!

The Canvas Object

The canvas object lives in the pdfgen package. Let's import it and paint some text:

# hello_reportlab.py

from reportlab.pdfgen import canvas

c = canvas.Canvas("hello.pdf")
c.drawString(100, 100, "Welcome to Reportlab!")
c.showPage()
c.save()

In this example, you import the canvas object and then instantiate a Canvas object. You will note that the only required argument is a filename or path. Next, you call drawString() on your canvas object and tell it to start drawing the string 100 points to the right of the origin and 100 points up. After that, you call showPage() method. The showPage() method will save the current page of the canvas. It's actually not required, but it is recommended.

The showPage() method also ends the current page. If you draw another string or some other element after calling showPage(), that object will be drawn to a new page. Finally, you call the canvas object's save() method, which saves the document to disk. Now you can open it up and see what our PDF looks like:

Hello World in ReportLab

What you might notice is that your text is near the bottom of the document. The reason for this is that the origin, (0,0), is the bottom left corner of the document. So when you told ReportLab to paint your text, you are telling it to start painting 100 points from the left-hand side and 100 points from the bottom. This is in contrast to creating a user interface in a Python GUI framework like Tkinter or wxPython where to origin is the top left.

Also note that since you didn't specify page size, it defaults to whatever is in the ReportLab config, which is usually A4. There are some common page sizes that can be found in reportlab.lib.pagesizes.

Now you are ready to look at the Canvas's constructor to see what it takes for arguments:

def __init__(self,filename,
             pagesize=None,
             bottomup = 1,
             pageCompression=None,
             invariant = None,
             verbosity=0,
             encrypt=None,
             cropMarks=None,
             pdfVersion=None,
             enforceColorSpace=None,
             ):

Here you can see that you can pass in the pagesize as an argument. The pagesize is actually a tuple of width and height in points. If you want to change the origin from the default of bottom left, then you can set the bottomup argument to 0, which will change the origin to the top left.

The pageCompression argument defaults to zero or off. Basically, it will tell ReportLab whether or not to compress each page. When compression is enabled, the file generation process is slowed. If your work needs your PDFs to be generated as quickly as possible, then you'll want to keep the default of zero.

However, if speed isn't a concern and you'd like to use less disk space, then you can turn on page compression. Note that images in PDFs will always be compressed, so the primary use case for turning on page compression is when you have a huge amount of text or lots of vector graphics per page.

ReportLab's User Guide makes no mention of what the invariant argument is used for, so I took a look at the source code. According to the source, it produces repeatable, identical PDFs with the same timestamp info (for regression testing). I have never seen anyone use this argument in their code and since the source says it is for regression testing, I think you can safely ignore it.

The next argument is verbosity, which is used for logging levels. At zero (0), ReportLab will allow other applications to capture the PDF from standard output. If you set it to one (1), a confirmation message will be printed out every time a PDF is created. There may be additional levels added, but at the time of writing, these are the only two documented.

The encrypt argument is used to determine if the PDF should be encrypted as you'll as how it is encrypted. The default is obviously None, which means no encryption at all. If you pass a string to encrypt, that string will be the password for the PDF. If you want to encrypt the PDF, then you will need to create an instance of reportlab.lib.pdfencrypt.StandardEncryption and pass that to the encrypt argument.

The cropMarks argument can be set to True, False or to an object. Crop marks are used by printing houses to know where to crop a page. When you set cropMarks to True in ReportLab, the page will become 3 mm larger than what you set the page size to and add some crop marks to the corners. The object that you can pass to cropMarks contains the following parameters: borderWidth, markColor, markWidth, and markLength. The object allows you to customize the crop marks.

The pdfVersion argument is used for ensuring that the PDF version is greater than or equal to what was passed in. Currently, ReportLab supports versions 1-4.

Finally, the enforceColorSpace argument is used to enforce appropriate color settings within the PDF. You can set it to one of the following:

  • cmyk
  • rgb
  • sep
  • sep_black
  • sep_cmyk

When one of these is set, a standard _PDFColorSetter callable will be used to do the color enforcement. You can also pass in a callable for color enforcement.

Let's go back to your original example and update it just a bit. In ReportLab you can position your elements (text, images, etc) using points. But thinking in points is kind of hard when you are used to using millimeters or inches. There is a clever function you can use to help you on StackOverflow:

def coord(x, y, height, unit=1):
    x, y = x * unit, height -  y * unit
    return x, y

This function requires your x and y coordinates as well as the height of the page. You can also pass in unit size. This will allow you to do the following:

# canvas_coords.py

from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter
from reportlab.lib.units import mm

def coord(x, y, height, unit=1):
    x, y = x * unit, height -  y * unit
    return x, y

c = canvas.Canvas("hello.pdf", pagesize=letter)
width, height = letter

c.drawString(*coord(15, 20, height, mm), text="Welcome to Reportlab!")
c.showPage()
c.save()

In this example, you pass the coord function the x and y coordinates, but you tell it to use millimeters as your unit. So instead of thinking in points, you are telling ReportLab that we want the text to start 15 mm from the left and 20 mm from the top of the page.

Yes, you read that right. When you use the coord function, it uses the height to swap the origin's y from the bottom to the top. If you had set your Canvas's bottomUp parameter to zero, then this function wouldn't work as expected. In fact, you could simplify the coord function to just the following:

def coord(x, y, unit=1):
    x, y = x * unit, y * unit
    return x, y

Now you can update the previous example like this:

# canvas_coords2.py

from reportlab.pdfgen import canvas	
from reportlab.lib.units import mm

def coord(x, y, unit=1):
    x, y = x * unit, y * unit
    return x, y

c = canvas.Canvas("hello.pdf", bottomup=0)

c.drawString(*coord(15, 20, mm), text="Welcome to Reportlab!")
c.showPage()
c.save()

That seems pretty straightforward. You should take a minute or two and play around with both examples. Try changing the x and y coordinates that you pass in. Then try changing the text too and see what happens!

Canvas Methods

The canvas object has many methods. In this section, you learn how you can use some of these methods to make your PDF documents more interesting. One of the easiest methods to use is setFont, which will let you use a PostScript font name to specify what font you want to use. Here is a simple example:

# font_demo.py

from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas

def font_demo(my_canvas, fonts):
    pos_y = 750
    for font in fonts:
        my_canvas.setFont(font, 12)
        my_canvas.drawString(30, pos_y, font)
        pos_y -= 10

if __name__ == '__main__':
    my_canvas = canvas.Canvas("fonts.pdf",
                              pagesize=letter)
    fonts = my_canvas.getAvailableFonts()
    font_demo(my_canvas, fonts)
    my_canvas.save()

To make things a bit more interesting, you will use the getAvailableFonts() canvas method to grab all the available fonts that you can use on the system that the code is running on. Then you will pass the canvas object and the list of font names to your font_demo() function. Here you loop over the font names, set the font, and call the drawString() method to draw each font's name to the page.

You will also note that you have set a variable for the starting Y position that you then decrement by 10 each time you loop through. This is to make each text string draw on a separate line. If you didn't do this, the strings would write on top of each other and you would end up with a mess.

Here is the result when you run the font demo:

ReportLab Canvas Font Demo

If you want to change the font color using a canvas method, then you would want to look at setFillColor or one of its related methods. As long as you call that before you draw the string, the color of the text will change as well.

You can use the canvas's rotate method to draw text at different angles. You will also learn how to use the translate method. Let's take a look at an example:

# rotating_demo.py

from reportlab.lib.pagesizes import letter
from reportlab.lib.units import inch
from reportlab.pdfgen import canvas

def rotate_demo():
    my_canvas = canvas.Canvas("rotated.pdf",
                              pagesize=letter)
    my_canvas.translate(inch, inch)
    my_canvas.setFont('Helvetica', 14)
    my_canvas.drawString(inch, inch, 'Normal')
    my_canvas.line(inch, inch, inch+100, inch)
    
    my_canvas.rotate(45)
    my_canvas.drawString(inch, -inch, '45 degrees')
    my_canvas.line(inch, inch, inch+100, inch)
    
    my_canvas.rotate(45)
    my_canvas.drawString(inch, -inch, '90 degrees')
    my_canvas.line(inch, inch, inch+100, inch)
    
    my_canvas.save()

if __name__ == '__main__':
    rotate_demo()

Here you use the translate method to set your origin from the bottom left to an inch from the bottom left and an inch up. Then you set out font face and font size. Next, write out some text normally, and then you rotate the coordinate system itself 45 degrees before you draw a string.

According to the ReportLab user guide, you will want to specify the y-coordinate in the negative since the coordinate system is now in a rotated state. If you don't do that, your string will be drawn outside the page's boundary and you won't see it. Finally, you rotate the coordinate system another 45 degrees for a total of 90 degrees, write out one last string and draw the last line.

It is interesting to look at how the lines moved each time you rotated the coordinate system. You can see that the origin of the last line moved all the way to the very left-hand edge of the page.

Here is the result when you run the code:

Rotated Text

Now it's time to learn about alignment.

String Alignment

The canvas supports more string methods than just the plain drawString method. You can also use drawRightString, which will draw your string right-aligned to the x-coordinate. You can also use drawAlignedString, which will draw a string aligned to the first pivot character, which defaults to the period.

This is useful if you want to line up a series of floating-point numbers on the page. Finally, there is the drawCentredString method, which will draw a string that is "centred" on the x-coordinate. Let's take a look:

# string_alignment.py

from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter


def string_alignment(my_canvas):
    width, height = letter
    
    my_canvas.drawString(80, 700, 'Standard String')
    my_canvas.drawRightString(80, 680, 'Right String')
    
    numbers = [987.15, 42, -1,234.56, (456.78)]
    y = 650
    for number in numbers:
        my_canvas.drawAlignedString(80, y, str(number))
        y -= 20
    
    my_canvas.drawCentredString(width / 2, 550, 'Centered String')
    
    my_canvas.showPage()
    

if __name__ == '__main__':
    my_canvas = canvas.Canvas("string_alignment.pdf")
    string_alignment(my_canvas)
    my_canvas.save()

When you run this code, you will quickly see how each of these strings get aligned.

Here is the result of running the code:

String alignment

The next canvas methods you will learn about are how to draw lines, rectangles and grids!

Drawing Lines on the canvas

Drawing a line in ReportLab is actually quite easy. Once you get used to it, you can actually create very complex drawings in your documents, especially when you combine it with some of ReportLab's other features. The method to draw a straight line is simply line.

Here is some example code:

# drawing_lines.py

from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas

def draw_lines(my_canvas):
    my_canvas.setLineWidth(.3)
    
    start_y = 710
    my_canvas.line(30, start_y, 580, start_y)

    for x in range(10):
        start_y -= 10
        my_canvas.line(30, start_y, 580, start_y)


if __name__ == '__main__':
    my_canvas = canvas.Canvas("lines.pdf", pagesize=letter)
    draw_lines(my_canvas)
    my_canvas.save()

Here you create a simple draw_lines function that accepts a canvas object as its sole parameter. Then you set the line's width via the setLineWidth method. Finally, you create a single line. The line method accepts four arguments: x1, y1, x2, y2. These are the beginning x and y coordinates as well as the ending x and y coordinates. You add another 10 lines by using a for loop.

If you run this code, your output will look something like this:

Drawing lines on the canvas

 

The canvas supports several other drawing operations. For example, you can also draw rectangles, wedges and circles. Here's a simple demo:

# drawing_polygons.py

from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas

def draw_shapes():
    c = canvas.Canvas("draw_other.pdf")
    c.setStrokeColorRGB(0.2, 0.5, 0.3)
    c.rect(10, 740, 100, 80, stroke=1, fill=0)
    c.ellipse(10, 680, 100, 630, stroke=1, fill=1)
    c.wedge(10, 600, 100, 550, 45, 90, stroke=1, fill=0)
    c.circle(300, 600, 50)
    c.save()

if __name__ == '__main__':
    draw_shapes()

When you run this code, you should end up with a document that draws something like this:

Drawing polygons

Let's take a few moments to go over the various arguments that each of these polygon methods accepts. The rect's code signature looks like this:

def rect(self, x, y, width, height, stroke=1, fill=0):

That means that you set the lower left-hand corner of the rectangle's position via its x/y parameters. Then you set its width and height. The stroke parameter tells ReportLab if it should draw the lines, so in the demo code I set stroke=1, or True. The fill parameter tells ReportLab to fill the interior of the polygon that I drew with a color.

Now let's look at the ellipse's definition:

def ellipse(self, x1, y1, x2, y2, stroke=1, fill=0):

This one is very similar to the rect. According to method's docstring, the x1, y1, x2, y2 parameters are the corner points of the enclosing rectangle. The stroke and fill parameters operate the same way as the rect's. Just for fun, you went ahead and set the ellipse's fill to 1.

Next, you have the wedge:

def wedge(self, x1,y1, x2,y2, startAng, extent, stroke=1, fill=0):

The x1,y1, x2,y2 parameters for the wedge actually correspond to the coordinates of an invisible enclosing rectangle that goes around a full 360 degree circle version of the wedge. So you will need to imagine that the full circle with a rectangle around it to help you position a wedge correctly. It also has a starting angle parameter (startAng) and the extent parameter, which basically tells the wedge how far out to arc to. The other parameters have already been explained.

Finally, you reach the circle polygon. Its method looks like this:

def circle(self, x_cen, y_cen, r, stroke=1, fill=0):

The circle's arguments are probably the most self-explanatory of all of the polygons you have looked at. The x_cen and y_cen arguments are the x/y coordinates of the center of the circle. The r argument is the radius. The stroke and fill arguments are pretty obvious.

All the polygons have the ability to set the stroke (or line) color via the setStrokeColorRGB method. It accepts Red, Green, Blue values for its parameters. You can also set the stroke color by using the setStrokeColor or the setStrokeColorCMYK method.

There are corresponding fill color setters too (i.e. setFillColor, setFillColorRGB, setFillColorCMYK), although those methods are not demonstrated here. You can use the functions by passing in the correct name or tuple to the appropriate function.

You are now ready to learn about how you would add a page break!

Create a Page Break

One of the first things you will want to know how to do when creating PDFs with ReportLab is how to add a page break so you can have multipage PDF documents. The canvas object allows you to do this via the showPage method.

Note however that for complex documents, you will almost certainly use ReportLab's flowables, which are special classes specifically for "flowing" your documents across multiple pages. Flowables are kind of mind-bending in their own right, but they are also a lot nicer to use than trying to keep track of which page you are on and where your cursor position is at all times.

Canvas Orientation (Portrait vs. Landscape)

ReportLab defaults its page orientation to Portrait, which is what all word processors do as well. But sometimes you will want to use a page in landscape instead. There are at least two ways to tell Reportlab to use a landscape:

from reportlab.lib.pagesizes import landscape, letter
from reportlab.pdfgen import canvas
 
c = canvas.Canvas('test.pdf', pagesize=letter)
c.setPageSize( landscape(letter) )

The other way to set landscape is just set the page size explicitly:

from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
from reportlab.lib.units import inch
 
c = canvas.Canvas('test.pdf', pagesize=letter)
c.setPageSize( (11*inch, 8.5*inch) )

You could make this more generic by doing something like this though:

from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
     
width, height = letter
 
c = canvas.Canvas('test.pdf', pagesize=letter)
c.setPageSize( (height, width) )

This might make more sense, especially if you wanted to use other popular page sizes, like A4.

A Simple Sample Application

Sometimes it's nice to see how you can take what you've learned and see if applied. So let's take some of the methods you've learned about here and create a simple application that creates a form:

# sample_form_letter.py

from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas

def create_form(filename, date, amount, receiver):
    """
    @param date: The date to use
    @param amount: The amount owed
    @param receiver: The person who received the amount owed
    """
    my_canvas = canvas.Canvas(filename, pagesize=letter)
    my_canvas.setLineWidth(.3)
    my_canvas.setFont('Helvetica', 12)

    my_canvas.drawString(30, 750,'OFFICIAL COMMUNIQUE')
    my_canvas.drawString(30, 735,'OF ACME INDUSTRIES')

    my_canvas.drawString(500, 750, date)
    my_canvas.line(480, 747, 580, 747)

    my_canvas.drawString(275, 725,'AMOUNT OWED:')
    my_canvas.drawString(500, 725, amount)
    my_canvas.line(378,723, 580, 723)

    my_canvas.drawString(30, 703,'RECEIVED BY:')
    my_canvas.line(120, 700, 580, 700)
    my_canvas.drawString(120, 703, receiver)

    my_canvas.save()

if __name__ == '__main__':
    create_form('form.pdf', '01/23/2018',
                '$1,999', 'Mike')

Here you create a simple function called create_form that accepts the filename, the date you want for your form, the amount owed and the person who receives the amount owed. Then you paint everything in the desired locations and save the file. When you run this, you will see the following:

Form letter

That looks pretty professional for a short piece of code.

Wrapping Up

You learned a lot about ReportLab's canvas and its many methods in this tutorial. While all the canvas methods weren't covered, you now know how to do the following:

  • Draw text
  • Learn about fonts and text colors
  • Creating a text object
  • Draw lines
  • Draw various shapes

Give ReportLab a try. You will find it is very useful and soon you will be creating amazing PDF reports of your very own!

Related Articles

Want to learn more about what you can do with ReportLab and Python? Check out some of the following resources:

Copyright © 2022 Mouse Vs Python | Powered by Pythonlibrary