Generating PDFs with QR Codes in the Browser

Generating QR codes is supported by various high quality libraries. Generating PDF using JavaScript in browsers is also very well-supported nowadays. In this post I want to present a method which has the following features:

  • Uses stable and trusted libraries
  • QR codes are embedded as vector graphic
  • Support for advanced QR code options

We will use the well-known @zxing/library and jsPDF libraries. The former is responsible for encoding text as QR codes. The latter library can generate PDFs in browsers. jsPDF especially supports to render vector graphics within PDFs.

Note: I will use TypeScript in this post. A JavaScript solution is available here.

Generate QR codes

The library @zxing/library actually already support creating SVGs from QR codes. We will adopt the code of BrowserQRCodeSvgWriter in order to create instructions which will allow us to embed QR codes in PDFs.

The following snippet is an adopted version of the renderResult function of @zxing/library. The createQRCode function encodes a string text as a QR code.

  • We can specify a size of the QR code using the size parameter.
  • The hints parameter allows us to set additional options.
  • Finally, we need to provide two functions: renderRect and renderBoundary. These two functions define the interface which jsPDF needs to fulfill.
 1import {
 2    EncodeHintType, IllegalStateException,
 3    QRCodeDecoderErrorCorrectionLevel as ErrorCorrectionLevel, QRCodeEncoder,
 4    QRCodeEncoderQRCode as QRCode
 5} from "@zxing/library";
 6
 7const DEFAULT_QUIET_ZONE_SIZE = 10
 8
 9// Adapted from https://github.com/zxing-js/library/blob/d1a270cb8ef3c4dba72966845991f5c876338aac/src/browser/BrowserQRCodeSvgWriter.ts#L91
10const createQRCode = (text: string,
11                      renderRect: (x: number, y: number, size: number) => void,
12                      renderBoundary: (x: number, y: number, width: number, height: number) => void,
13                      size: number, hints?: Map<EncodeHintType, any>) => {
14    let errorCorrectionLevel = ErrorCorrectionLevel.L;
15    let quietZone = DEFAULT_QUIET_ZONE_SIZE;
16    const {width, height} = {width: size, height: size}
17
18    if (hints) {
19        if (hints.get(EncodeHintType.ERROR_CORRECTION)) {
20            errorCorrectionLevel = ErrorCorrectionLevel.fromString(hints.get(EncodeHintType.ERROR_CORRECTION).toString());
21        }
22
23        if (hints.get(EncodeHintType.MARGIN) !== undefined) {
24            quietZone = Number.parseInt(hints.get(EncodeHintType.MARGIN).toString(), 10);
25        }
26    }
27
28    const code: QRCode = QRCodeEncoder.encode(text, errorCorrectionLevel, hints);
29
30    const input = code.getMatrix();
31
32    if (input === null) {
33        throw new IllegalStateException();
34    }
35
36    const inputWidth = input.getWidth();
37    const inputHeight = input.getHeight();
38    const qrWidth = inputWidth + (quietZone * 2);
39    const qrHeight = inputHeight + (quietZone * 2);
40    const outputWidth = Math.max(width, qrWidth);
41    const outputHeight = Math.max(height, qrHeight);
42
43    const multiple = Math.min(Math.floor(outputWidth / qrWidth), Math.floor(outputHeight / qrHeight));
44
45
46    // Padding includes both the quiet zone and the extra white pixels to accommodate the requested
47    // dimensions. For example, if input is 25x25 the QR will be 33x33 including the quiet zone.
48    // If the requested size is 200x160, the multiple will be 4, for a QR of 132x132. These will
49    // handle all the padding from 100x100 (the actual QR) up to 200x160.
50    const leftPadding = Math.floor((outputWidth - (inputWidth * multiple)) / 2);
51    const topPadding = Math.floor((outputHeight - (inputHeight * multiple)) / 2);
52
53    renderBoundary(0, 0, outputWidth, outputHeight)
54
55
56    for (let inputY = 0; inputY < inputHeight; inputY++) {
57        // Write the contents of this row of the barcode
58        for (let inputX = 0; inputX < inputWidth; inputX++) {
59            if (input.get(inputX, inputY) === 1) {
60                let outputX = leftPadding + inputX * multiple;
61                let outputY = topPadding + inputY * multiple;
62                renderRect(outputX, outputY, multiple)
63            }
64        }
65    }
66}

Rendering a QR code in a PDF

The following snippet shows how to integrate the above snippet with jsPDF. The drawQrCode function encodes the parameter text as QR code and then renders it.

  • The parameters x and y determine where to draw the QR code within the pdfDocument
  • The parameter size determines the size of the QR code.
  • pdfDocument is the handle to the jsPDF document
  • border optionally draws a border around the quiet zone around the QR code.
  • version sets the version of the QR code.
 1import {jsPDF} from "jspdf";
 2import {EncodeHintType} from "@zxing/library";
 3
 4const drawQrCode = (text: string, x: number, y: number, size: number, pdfDocument: jsPDF, border: boolean = true, version = 7) => {
 5    const hints = new Map()
 6    hints.set(EncodeHintType.MARGIN, 0)
 7    hints.set(EncodeHintType.QR_VERSION, version)
 8    createQRCode(text, (rectX: number, rectY: number, rectSize: number) => {
 9            pdfDocument.rect(x + rectX, y + rectY, rectSize, rectSize, "FD");
10        },
11        (rectX: number, rectY: number, rectWidth: number, rectHeight: number) => {
12            if (border) {
13                pdfDocument.setLineWidth(.4)
14                pdfDocument.roundedRect(x + rectX, y + rectY, rectWidth, rectHeight, 10, 10, "D");
15                pdfDocument.setLineWidth(0)
16            }
17        }, size, hints)
18}

The function above calls createQRCode and provides two lambda functions. Those arrow-functions are responsible to draw the correct rectangles and boundaries.

Example Usage

There is a quick example on how to use the code above.

1var doc = new jsPDF();
2drawQrCode("test", 0, 0, 100, doc)
3
4doc.save('Test.pdf');

A full example can be seen here.

Bonus: Create a Single SVG Path

Based on the code above we can also create a single SVG path. This is interesting for embedding a QR code in a website. The code below generates a single string which can be visualized using this tool for example.

 1import {EncodeHintType} from "@zxing/library";
 2
 3const drawSVGPath = (text: string, size: number, version = 7) => {
 4    const hints = new Map()
 5    hints.set(EncodeHintType.MARGIN, 0)
 6    hints.set(EncodeHintType.QR_VERSION, version)
 7
 8    let pathData = ""
 9    createQRCode(text, (x: number, y: number, size: number) => {
10            return pathData += 'M' + x + ',' + y + ' v' + size + ' h' + size + ' v' + (-size) + ' Z ';
11        },
12        (rectX: number, rectY: number, rectWidth: number, rectHeight: number) => {
13            return pathData += 'M' + rectX + ',' + rectY + ' v' + rectHeight + ' h' + rectWidth + ' v' + (-rectHeight) + ' Z ';
14        }, size, hints)
15    return pathData
16}
17``

Do you have questions? Send an email to max@maxammann.org