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

Download Mastering Decorators

Mastering_decorators_cover

Enjoyed this article? Join the newsletter and get Mastering Decorators - a gentle 22-page introduction to one of the trickiest parts of Python.

Weekly-ish. No spam. Unsubscribe any time. Powered by ConvertKit
  • aidenmchugh

    this seems like a good approach, and is very clearly explained – thanks for sharing!

  • Andreas Dewes

    It seems that this doesn’t work if there are additional resources in the HTML file that need to be loaded (e.g. images). To make it work in that case, you need to execute the event loop via app.exec_() and add a signal that waits for the content to be fully loaded:

    This also includes a loading timeout via a QTimer, in this case 4 seconds.

    • Juan Carlos Asuncion

      app.processEvents() does not work

      • Andreas

        See my other reply for a possible fix.

    • Hriday N Sanghvi

      Is there anything apart from adding the app.processEvents() line that needs to be done to load the images?

      • Andreas

        I created a Github project for this a while ago, do you want to try running that one and create an issue on GH if you encounter any problems? The example includes loading external resources.

        https://github.com/adewes/pdf-printer

        • Hriday N Sanghvi

          Sure thanks 🙂