How to generate PDF reports with Jinja2 and PyQt

There are quite a few options for PDF generation in Python, but nothing fully open-source that ticks all the boxes. The Reportlab library is probably the most fully-featured solution available right now. Unfortunately, the open-source version doesn’t support templates, so unless you cough up for a license you’re going to be stuck manipulating the PDF format at a very low level.

Recently, I had a requirement to generate some simple PDF reports. I didn’t want to write lots of boilerplate and hoped I would be able to use some kind of templating. In the end I came up with a solution based on Jinja2 and Qt’s QWebView widget. First it renders the contents as HTML, then it uses the “Print as PDF” functionality of the QWebView  to save a PDF file. It’s a bit of a hack, but it gets the job done.

Here’s the main file, htmltopdf.py .

I also made a subdirectory for Jinja2 templates called, originally enough, templates. Inside it are two files, base.html  and report.html .

These templates are just examples. This is where you can get creative with CSS, etc. All I’ve done is provide a minimal scaffold so you can see what’s going on.

Let’s walk through htmltopdf.py  and figure out how it works.

The first thing we need to do is set up the Jinja2 environment. Although it is possible to instantiate a jinja2.Template  object by passing it a string holding the template text, instantiating the environment gives us access to template inheritance and other cool features. To set up the environment, we have to pass it a loader object. For our purposes we can use the basic package loader, which takes as arguments the name of the package (or the name of the file, in this case) and the template directory inside it. Our template directory is  templates .

Here is the function that we use to render a particular template. You can get the Template  object from the given template file name by calling get_template  on the environment. Once you have it, you  call render  on it, passing in the necessary information as keyword arguments.

Now let’s take a look at the print_pdf  function, which handles laying out the HTML from our render_template  function and printing it to a file.

We won’t be able to do much with the QWebView  unless we instantiate QApplication , so we bookend print_pdf  by constructing a QApplication  instance and finally calling exit  on it.

Next, we create the QWebView . Seeing as we already have the HTML we want to display in it, we can call setHtml  on the webview. If you want to load an external URL, for snapshotting web pages, etc., you have to use the QWebView.load  function to set its URL. In that case, you will need to register a signal handler to listen for the loadFinished()  signal, but seeing as we are just providing the HTML directly we don’t need to bother with that.

After we have injected the HTML into the webview, we get a QPrinter  and configure it to print an A4-size PDF document, by calling its  setPageSize , setOutputFormat  and setOutputFileName  methods. Other page sizes and output formats are also supported.

That covers everything novel in this approach. The main function just ties it all together and generates some sample data for the template. I found the loremipsum  package handy for quickly getting my hands on placeholder text.

Here’s what our generated PDF looks like:

rendered_pdf

A really simple guide to packaging your PyQt application with cx_Freeze

Python is great for writing programs that run on your own machine or deploy to a web server, but when you want to distribute your applications to friends or customers, things can get very annoying very quickly.

– “It doesn’t work. I don’t know how to run this.”
– “Ok, did you install the Python interpreter?”
– “No, what’s that?”
– “You have to download it from www.python.org. Get the 2.7 version.”
– “Yeah, it’s ok. I’ll just use something else.”

We’ve all been there. If you’re going to distribute your software to people who aren’t Python programmers, you had better package it in a friendly way.

My preferred solution is cx_Freeze. Unlike py2exe, it is cross platform; you can use it to build packages for Windows, OSX and Linux. This post will walk through how to package a simple PyQt4 GUI application for all three platforms. The sample application is a Tetris clone I found in the PyQt4 tutorial on www.zetcode.com.

First, go here, copy the full version of the game and save it in a file called tetris.py.

Make sure you have cx_Freeze installed:

Now go to the directory where you saved tetris.py and run the quickstart command to generate a scaffold setup.py. This is a distutils setup script that tells cx_Freeze how to package your application.

You will be prompted for some information. When it asks for the “Python file to make executable from”, type the name of the script that is the main entry point of your application. In this case it’s tetris.py. The generated setup.py will look something like this:

Not very PEP8, but we can let that pass. Let’s walk through this script to see what is going on.

Build options control what Python packages and modules, and what non-Python files (such as assets) are included in the packaged application. cx_Freeze tries to figure out what is needed on its own, but you may need to manually specify some stuff here if you are using dynamic module imports anywhere in your program.

The “packages” and the “excludes” keys are included in the automatically generated buildOptions dictionary. The main other key that you might need to add are “includes” and “include_files”. ”includes” takes a list of modules that need to be included, and “include_files” takes a list of non-Python files, e.g.

The next interesting line is:

On Windows, GUI applications require a different Python base. They must be executed with the pythonw.exe interpreter, or a command prompt will open and remain open for the duration of the program.

If we read further in the generated setup.py, we see:

An instance of the cxFreeze.Executable class must be instantiated for the file that is the main entry point of your program. In general, you will only have one executable, but you can have more if, for instance, you are packaging a suite of command line tools.

The final section of the file contains the call to the setup function, passing in the buildOptions dictionary and the other options you specified when you ran cxfreeze-quickstart, such as the program name, version and description.

For this tetris.py script, we do not need to modify the autogenerated setup.py. You can go ahead and build the package by running

This will create a new build/ directory below your project. Below that, it will create another directory prefaced by exe. and followed by the name of your current platform. It does this so that the build command can be run on multiple platforms without overwriting.

Inside the platform subfolder, you will see your executable.

There’s a little gotcha that I should note here. When you run the build command, it will complete even if certain imports could not be found. Sometimes it won’t matter, but you should review the output of the command to make sure that everything necessary is included.

The build command will dump everything out into the platform folder. When packaging applications for Linux I recommend that you just distribute a .tar.gz of this, but for OSX and Windows, you should make use of the platform-specific packaging commands.

On OSX, you have the option of building a .dmg or a .app, by executing one of these at the prompt:

On Windows, you can build a .msi package as follows:

Unfortunately, you can’t build packages for one platform on another, so you can only build Windows packages on Windows and OSX packages on OSX, but the setup.py is the same across platforms.

Before I go, I should say something about accessing files inside your packaged application. When your packaged executable is running, the global __file__ variable is not set. If you try to grab the current path using os.path.dirname(__file__) it won’t work. You need to use os.path.dirname(sys.executable) .
Luckily, when your packaged application is running, a “frozen” attribute in sys is set. You can use this fact to grab a handle to files regardless of whether the packaged version of the program is running or not.

The cx_Freeze documentation lists this function as a starting point.

You might need to modify this function slightly, as it assumes that your data files are stored on the same level as your executable, not in a subdirectory.

That’s it! After working through this post, you should have more than enough information to start packaging and distributing your creations.