View · Index
No registered users in community rubick
in last 10 minutes

Dynamically generating PDF files with ReportLab

Introduction

It is pretty easy to generate dynamic PDF files from OpenACS. The hardest part is learnng Python and the Reportlab API. Getting to a 'Hello World' PDF is an easy process. In my case, I pulled the current date from the database and displayed it tiled on the PDF file.

 

You'll need to be familiar enough with packages that you can set up a new package. You should probably refer to the following thread:

 

Thread on PDF generation

 

This document is released under GPL.

 

Install Reportlab

First of all, you need to install Reportlab, a Python based program which generates PDF files for you. For Debian Linux:
  • apt-get install python2.2 (other Linux distributions: install python in some way)
  • Download the tgz file for ReportLab
  • You then tar -xzf ReportLab.tgz
  • For Debian, you then want to move the reportlab directory to /usr/local/lib/python2.2/site-packages/
  • become another user
  • $ python2.2
  • import reportlab (see if it works)

 

Download the User Guide (in PDF format, of course), and go to the installation section. Follow their directions.

 

Install the Python Imaging Library

 

If you'd like to include bitmap images in your PDF files, you need to install the Python Imaging Library. I didn't do this, but I may come back to it later. Directions for installing this are in the User Guide, but the link in it was incorrect. Go to http://www.pythonware.com/products/pil/index.htm instead.

 

Create a new package, and set up procedures

If you're creating an OpenACS package, you probably should set it up now (or copy Tillmann's file as a starting point). In your packagename/tcl directory, place the following procedures.
# /packagename/tcl/pdf-defs.tcl

ad_library {

    by Jade Rubick (jade@bread.com)
    based on code by Tilmann Singer (tils@tils.net)

    License: GPL

    Generate pdf, preferably in the background.

    Note that there is no locking yet, so there may be more than 1 process
    generating the same pdf. But besides being inefficient this has no
    side-effects. Note this should also use namespaces!
}



ad_proc pdf_file_stub { type id } {

    # add in a security checking field, so that
    # you aren't vulnerable to attacks of the variety:
    # pdf_file_stub \"../../wherever/i/want/to/go\"
    #if { regexp { ([\.\.]) } $type } {
        #return "thats illegal bucko"
    #}

    return "/tmp/$type-$id"
}


ad_proc pdf_generate_pdf_if_necessary { type id } {

    if { ![file exists "[pdf_file_stub $type $id].pdf"] } {
        pdf_generate_pdf $type $id
    }
}


ad_proc pdf_return_pdf { type id } {

    pdf_generate_pdf_if_necessary $type $id
    ns_returnfile 200 application/pdf "[pdf_file_stub $type $id].pdf"
}

ad_proc pdf_generate_pdf { type id } {

    ns_log notice "!> Starting to generate pdf for type: $type id $id"

    file delete "[pdf_file_stub $type $id].txt"
    file delete "[pdf_file_stub $type $id].pdf"

    # there is probably a better way to do this, but you
    # can put checks in here for each type of PDF that you
    # generate, according to "type", and use this procedure
    # to generate the PDF file.

    # the PDF file is generated from a text file, which is
    # read directly from the database. Yes, this is less than
    # an optimal solution. So improve it and share your code
    # with me!

    # example
    if {$type == "test"} {

        # pull something from the database
        db_1row date "select to_char(sysdate,'Mon fmDD, YYYY HH:MI am') as today from dual"
        set txt_file [open "[pdf_file_stub $type $id].txt" w]

        fconfigure $txt_file -encoding "iso8859-1"
        puts $txt_file "$today"
        close $txt_file

        # the logic for creating the businesscard is in the Python file.
        exec "/web/intranet/bin/pdf-test.py" [pdf_file_stub $type $id] >> /tmp/$type.log 2>> /tmp/$type.log

    } elseif {$type == "business_card"} {
        db_1row card "select * from bu_cards where card_id=:card_id"

        # uh, ugly. no xml, just one value per line. saves me typing.
        set txt_file [open "[pdf_file_stub $type $id].txt" w]
        fconfigure $txt_file -encoding "iso8859-1"
        puts $txt_file "$name\n$subtitle\n$line1\n$line2\n$line3"
        close $txt_file

        # the logic for creating the businesscard is in the Python file.
        exec "/web/bin/pdf-businesscard.py" [pdf_file_stub $type $id] >> /tmp/$type.log 2>> /tmp/$type.log

    }
    ns_log notice "!> Finished with type: $type id $id"

}


4. Insert the pdf-xxx.py Python files. I put mine in /web/bin/, but you can put it
  where you like, as long as you modify the Tcl procedures. /web/bin is probably a bad idea.

Here are the Python files:

# pdf-test.py

#!/usr/bin/python


import sys, copy, string, os, fileinput

from reportlab.lib.units import cm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet

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



bottom_margin = 1*cm
left_margin = 2*cm

height=5.4*cm
width=8.5*cm

file_stub = sys.argv[1]

# never got around dealing with stdin that comes unicode encoded from
# aolserver, grr. The file approach works though.

lines = fileinput.input(file_stub + ".txt")
today = lines[0]


def onecard(c):
    t = c.beginText()
    t.setTextOrigin(0.5*cm, 3.6*cm)
    t.setFont("Helvetica-Bold", 12)
    t.textLines(today)
    c.drawText(t)

    #c.rect(0, 0, width, height)


def grid(c):
    for i in range(0, 6):
        y = bottom_margin + i * height
        c.line(5, y, 20, y)
        c.line(A4[0] - 5, y, A4[0] - 20, y)

    for i in range(0, 3):
        x = left_margin + i * width
        c.line(x, 5, x, 20)
        c.line(x, A4[1] - 5, x, A4[1] - 20)
        


def myFirstPage(c, doc):
    c.line(line_margin,height-margin_from_top,width-line_margin,height-margin_fr
om_top)
    onecard()


        
c = canvas.Canvas(file_stub + ".pdf")

grid(c)


c.translate(left_margin, bottom_margin)

c.saveState()

for i in range(0, 5):
    onecard(c)
    c.translate(0, height)

c.restoreState()

c.translate(width, 0)



for i in range(0, 5):
    onecard(c)
    c.translate(0, height)


c.save()


# pdf-businesscard.py

#!/usr/bin/python


import sys, copy, string, os, fileinput

from reportlab.lib.units import cm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet

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



bottom_margin = 1*cm
left_margin = 2*cm

height=5.4*cm
width=8.5*cm

file_stub = sys.argv[1]

# never got around dealing with stdin that comes unicode encoded from
# aolserver, grr. The file approach works though.

lines = fileinput.input(file_stub + ".txt")
name = lines[0]
subtitle = lines[1]
line1 = lines[2]
line2 = lines[3]
line3 = lines[4]



def onecard(c):
    t = c.beginText()
    t.setTextOrigin(0.5*cm, 3.6*cm)
    t.setFont("Helvetica-Bold", 12)
    t.textLines(name)
    t.setFont("Helvetica", 10)
    t.textLines(subtitle)
    t.moveCursor(0, 10)
    t.textLines(line1)
    t.textLines(line2)
    t.textLines(line3)
    c.drawText(t)

    #c.rect(0, 0, width, height)


def grid(c):
    for i in range(0, 6):
        y = bottom_margin + i * height
        c.line(5, y, 20, y)
        c.line(A4[0] - 5, y, A4[0] - 20, y)

    for i in range(0, 3):
        x = left_margin + i * width
        c.line(x, 5, x, 20)
        c.line(x, A4[1] - 5, x, A4[1] - 20)
        


def myFirstPage(c, doc):
    c.line(line_margin,height-margin_from_top,width-line_margin,height-margin_fr
om_top)
    onecard()


        
c = canvas.Canvas(file_stub + ".pdf")

grid(c)


c.translate(left_margin, bottom_margin)

c.saveState()

for i in range(0, 5):
    onecard(c)
    c.translate(0, height)

c.restoreState()

c.translate(width, 0)



for i in range(0, 5):
    onecard(c)
    c.translate(0, height)


c.save()

How to set up a new PDF file

You need to edit your pdf-defs.tcl file to include the new type in pdf_generate_pdf. Also create a new pdf-xxx.py Python file, and set it up as you like it. That's basically it.