Skip to content

Flask x ReportLab

In this tutorial we will make a small web app, utilising ReportLab to combine two of our favourite things...the web and PDF's!

json2pdf

Here at ReportLab we already provide a service of turning json into PDF's with our json2pdf service. We do this by using hosting an endpoint which ingests json and returns a PDF using ReportLab Plus and our own templating format, preppy.

Have a look at our example. If you think this is something your business could do with, don't hesitate to get in touch.

Flask

We will use Flask for our web application! It's super flexible and lightweight, perfect for our use case.

Setup

First things first, our virtual environment. For this tutorial you will need ReportLab Plus.

python3.11 -m venv env
source env/bin/activate
pip install rlextra -i https://www.reportlab.com/pypi/
pip install flask

The flask app only needs one file to run, so our directory will look like this:

project/
|- app.py

where app.py looks like this:

from flask import Flask

app = Flask(__name__)


@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

To run the minimal flask app, use flask run or flask run --debug (I prefer the latter as it has autoreload). When run, navigate your browser to the correct local host port (for me it is port 5000). It should produce an output like this:

hello_flask.png

Form Data

The web app itself will be used to send variable data over a post request using a form! Let's Create a html page with a form. We will use this template from mdn web docs, a form for greetings.

index.html:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>

    <form method="post" action="/generate">
        <div>
            <label for="say">What greeting do you want to say?</label>
            <input name="say" id="say" value="Hi" />
        </div>
        <div>
            <label for="to">Who do you want to say it to?</label>
            <input name="to" id="to" value="Mom" />
        </div>
        <div>
            <button>Send my greetings</button>
        </div>
    </form>

    </form>

</body>

</html>

Notice how the form will send a post request to /generate, we will have to make sure we have that route in our app.py. Now we have to slightly adjust the file system to allow for a template in Flask:

project/
|- app.py
|- templates/
   |- index.html

Where our app.py now looks like:

from flask import Flask, render_template, request, redirect

app = Flask(__name__)


@app.route("/generate", methods=["POST", "GET"])
def generate():
    if request.method == "POST":
        redirect("/")
    else:
        return redirect("/")


@app.route("/")
def index():
    return render_template("index.html")

For now we will just re-direct the request back to the homepage, as not to cause errors.

Our new web app looks like this:

form_data.png

Preppy

For our templating system we will use Preppy! Our very own python templating system.

We will follow the form data and make a small letter to send to someone. For this we will use our template.prep file:

<?xml version="1.0" encoding="utf-8" standalone="no" ?> 
<!DOCTYPE document SYSTEM "rml.dtd">
<document filename="template.pdf" colorSpace="CMYK">
    <docinit useCropMarks="0">
        <color id="silver" CMYK="0, 0, 0, 0.09"/>
    </docinit>

    <template>
        <pageTemplate id="main">
            <pageGraphics>
            </pageGraphics>
            <frame id="first" x1="72" y1="72" width="420" height="595"/>
        </pageTemplate>
    </template>

    <stylesheet>
    <!-- This is your stylesheet -->
    </stylesheet>

    <!-- The story (your actual content) starts below this comment -->

    <story>
        <p>Dear {{ to }},</p>
        <p>{{ say }}/<p>
    </story>

</document>

Within the curly braces ({{ X }}) represents the variable which will be available in a namespace.

Now our filesystem looks as follows:

project/
|- app.py
|- templates/
   |- index.html
   |- template.prep

Creating RML from Preppy

In order to create a PDF using RML, we first need to take our .prep and input our variables. Normally it would look something like this:

import preppy
# namespace
ns = dict()
# find template
template = preppy.getModule('template.prep')
# make rml
rml = template.getOutput(
    ns,
    quoteFunc=preppy.stdQuote,
    lquoteFunc=None,
)
# make PDF
from rlextra.rml2pdf.rml2pdf import go
go(
    rml,
    outputFileName="template.pdf",
)

However we have to: - Fill the namespace with variables from our POST request. - Save the PDF in a memory buffer so we can use our flask app without a saving and loading a PDF. - Return a inline file in the response headers.

Our app.py will now look like:

from flask import Flask, render_template, request, redirect, make_response
import io
import preppy
from rlextra.rml2pdf.rml2pdf import go

app = Flask(__name__)


@app.route("/generate", methods=["POST", "GET"])
def generate():
    if request.method == "POST":
        # find template
        template = preppy.getModule('templates/template.prep')
        # buffer
        buf = io.BytesIO()
        # make rml
        rml = template.getOutput(
            request.form,
            quoteFunc=preppy.stdQuote,
            lquoteFunc=None,
        )
        # make PDF
        go(
            rml,
            outputFileName=buf,
        )
        response = make_response(buf.getvalue())
        response.headers['Content-Type'] = 'application/pdf'
        response.headers['Content-Disposition'] = \
            'inline; filename=%s.pdf' % 'yourfilename'
        return response

    else:
        return redirect("/")


@app.route("/")
def index():
    return render_template("index.html")

Now when we press send my greetings, we should see this:

pdf.png

Success! A very very simple PDF, fully integrated with a minimal flask app. Completely customizable for your data.

Have a go at makiing your own flask app, perhaps you can make the PDF a bit more exciting in the preppy file.