Ask Anvil

Answers to questions about automating PDFs, e-signatures, Webforms, and other paperwork problems.
PDFs
Categories

How do I add a QR code to a PDF I generate from a template?

Generate the QR code as an image first, then place it like any other image. Which path you take depends on whether you control the template.

If you render the document from an HTML template

Build the PNG in memory and inline it as a base64 data URI. The data URI carries the image bytes in the src attribute itself, so the renderer never has to fetch the file over the network or off disk.

# pip install "qrcode[pil]"
import base64
import io

import qrcode


def qr_data_uri(data):
    qr = qrcode.QRCode(
        box_size=8,
        border=4,
        error_correction=qrcode.constants.ERROR_CORRECT_M,
    )
    qr.add_data(data)
    qr.make(fit=True)

    buf = io.BytesIO()
    qr.make_image(fill_color="black", back_color="white").save(buf, format="PNG")
    return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()


uri = qr_data_uri("https://example.com/d/9f2c41a8")
img_tag = f'<img src="{uri}" width="96" height="96" alt="Verify this document">'

Pass img_tag into the template wherever the code should print.

If the PDF already exists

Stamp it. Draw the QR code onto a blank page the same size as the content page with ReportLab, then merge that page onto each page with pypdf. Coordinates are in points (1/72 inch), so the 72 below is a one inch square and the 36 is a half inch bottom margin.

# pip install "qrcode[pil]" pypdf reportlab
import io

import qrcode
from pypdf import PdfReader, PdfWriter
from reportlab.lib.utils import ImageReader
from reportlab.pdfgen import canvas


def qr_stamp_page(data, page_size, x, y, size):
    qr = qrcode.QRCode(box_size=10, border=4)
    qr.add_data(data)
    qr.make(fit=True)

    png = io.BytesIO()
    qr.make_image().save(png, format="PNG")
    png.seek(0)

    buf = io.BytesIO()
    c = canvas.Canvas(buf, pagesize=page_size)
    c.drawImage(ImageReader(png), x, y, width=size, height=size)
    c.save()
    buf.seek(0)
    return PdfReader(buf).pages[0]


writer = PdfWriter(clone_from="agreement.pdf")
for page in writer.pages:
    width = float(page.mediabox.width)
    height = float(page.mediabox.height)
    stamp = qr_stamp_page(
        "https://example.com/d/9f2c41a8", (width, height), width - 108, 36, 72
    )
    page.merge_page(stamp, over=True)

writer.write("agreement-with-qr.pdf")

merge_page(stamp, over=True) draws the stamp on top of the existing page content. Pass over=False and it goes behind the content instead, which is what you want for a watermark.

Caveats

Keep the payload short. Encode a URL with an opaque ID rather than the document data. More data forces a higher QR version, and versions run from 1 (a 21x21 matrix) to 40, so a fat payload means finer modules that scan badly at small print sizes.

Leave the defaults alone unless you have a reason. border=4 is the quiet zone, and 4 is both the library default and the minimum the QR spec allows, so trimming it to save room is a common way to end up with codes that will not scan. The default error correction level, ERROR_CORRECT_M, recovers about 15 percent or less of errors, and that headroom matters once a document gets printed, scanned, and emailed back.

A QR code is not evidence of anything by itself. Anyone can copy the image into a different PDF, so keep the truth server side: the URL should resolve to a record you look up at scan time, and the ID in it should be a random unguessable token rather than a sequential number.

Back to All Questions

The fastest way to build software for documents

Anvil Document SDK is a comprehensive toolbox for product teams launching document flows where PDF filling, signing, and complex conditional scenarios are necessary.
Explore Anvil
Anvil Webforms