Skip to Content
Technical Articles

Microservices – Generating PDF with Node.js & PDFKit

Overview

 

Portable Document Format (PDF) is one of the most common document formats used for electronic documents in businesses due to its nature of being read-only, having rich formatting styles, and being compact in size, etc. Often enough, a business application needs to generate PDF documents based on business data and images (such as receipts, certificates, etc.). This blog post shows examples of how to generate a PDF document in business applications using Node.js.

SAP Business Technology Platform (BTP) provides a fast and easy way to create, run, manage, and scale business applications in the cloud. A business application in BTP typically includes a user interface, microservices that contain business logic, and technical operations. There are also backend systems such as Enterprise Resource Planning (ERP), Supply Chain Management, Blockchain network etc. that store the business data.

 

Typical%20Business%20Application

Typical Business Application

 

The JavaScript PDF document generation library for Node.js used in this example is PDFKit (https://www.npmjs.com/package/pdfkit). The documentation can be found at the PDFKit.org website (https://pdfkit.org/).

Other libraries used in this example are Request (https://www.npmjs.com/package/request) and axios (https://www.npmjs.com/package/axios). During implementation, you only need either Request or axios for requests/responses. However, for demonstration purpose, this article shows how these two libraries can be used to load an image from a web URL and display it in the PDF document.

The sample code shown in this article is partly based on another article Generating a PDF in Nodejs (https://levelup.gitconnected.com/generating-pdf-in-nodejs-201e8d9fa3d8).

 

Sample Code Using PDFKit Library

 

From the command line or terminal, add the dependency libraries after project initialization.

npm init
npm install pdfkit
npm install request
npm install axios

 

Create a Node module (RequestUtil.js) to handle requests. The doRequest function returns a Promise object which eventually will return a resolved state or a rejected state. In the sample code of this article, the doRequest function is called to get the content of an image file from a web URL.

"use strict";
const _REQUEST = require("request");

module.exports =
{
    doRequest: doRequest
}

async function doRequest(requestPayload)
{
    return new Promise(function(resolve, reject)
    {
        _REQUEST(requestPayload, function(err, response)
        {
            if (err)
            {
                reject(err);
            }
            else
            {
                resolve(response);
            }
        });
    });
}

 

Create the main function (GenInvoice.js) to provide the sample invoice data to be displayed in the generated PDF document. The sample invoice data is defined as a JSON object. It is passed as an argument into InvoiceGenerator.js to generate the PDF document.

"use strict"
const _INVOICE_GENERATOR = require("./InvoiceGenerator");

const _kINVOICE_DATA =
{
    invoiceNumber: "9900001",
    dueDate: "March 31, 2021",
    subtotal: 8000,
    paid: 500,
    memo: "Delivery expected in 7 days",
    addresses:
    {
        billing:
        {
            name: "Santa Claus",
            address: "1 Elf Street",
            city: "Arctic City",
            state: "Arctic Circle",
            postalCode: "H0H 0H0",
            country: "North Pole"
        }
    },
    items:
    [
        {
            itemCode: "12341",
            itemDescription: "Best Run laptop computer",
            quantity: 4,
            price: 2985.00
        },
        {
            itemCode: "12342",
            itemDescription: "Best Run desktop computer",
            quantity: 2,
            price: 2295.00
        }
    ]
};

let ig = new _INVOICE_GENERATOR(_kINVOICE_DATA);

void async function main()
{
    await ig.generate();
}()

 

Create a class (InvoiceGenerator.js) for calling the PDFKit library to generate the PDF document.

"use strict"
const _AXIOS = require("axios");
const _FS = require("fs");
const _PDFKIT = require("pdfkit");
const _REQUEST = require("request");
const _REQUEST_UTIL = require("./RequestUtil");

class InvoiceGenerator
{
    constructor(invoice)
    {
        this.invoice = invoice;
    }

    async generate()
    {
        let pdfkit = new _PDFKIT();
        let pdfOutputFile = `./Invoice-${this.invoice.invoiceNumber}.pdf`;
    //  [COMMENT] The PDF file is to be written to the file system.
        pdfkit.pipe(_FS.createWriteStream(pdfOutputFile));
        await this.writeContent(pdfkit);
        pdfkit.end();
    }

    async writeContent(pdfkit)
    {
        this.generateHeaders(pdfkit);
        this.generateTable(pdfkit);
        this.drawBoxes(pdfkit);
        await this.displayImage(pdfkit);
        this.generateBulletList(pdfkit);
        this.generateNumberedList(pdfkit);
        this.generateLetteredList(pdfkit);
        this.generateMutliLevelList(pdfkit);
        this.generateHyperlink(pdfkit);
        this.generateFooter(pdfkit);
    }

    generateHeaders(pdfkit) {...}
    generateTable(pdfkit) {...}
    drawBoxes(pdfkit) {...}
    async displayImage(pdfkit) {...}
//  displayImageBase64(pdfkit) {...}
    generateBulletList(pdfkit) {...}
    generateNumberedList(pdfkit) {...}
    generateLetteredList(pdfkit) {...}
    generateMultiLevelList(pdfkit) {...}
    generateHyperlink(pdfkit) {...}
    generateFooter(pdfkit) {...}
}

module.exports = InvoiceGenerator;

 

Function generateHeaders()

This function generates the header section of the PDF document.

generateHeaders(pdfkit)
{
    let billingAddress = this.invoice.addresses.billing;
    pdfkit.image("./SAP.png", 25, 25, {width: 150})
          .fillColor("#000")
          .fontSize(20)
          .text("INVOICE", 400, 25, {align: "right"})
          .fontSize(10)
          .text(`Invoice Number: ${this.invoice.invoiceNumber}`, {align: "right"})
          .text(`Due Date: ${this.invoice.dueDate}`, {align: "right"})
          .text(`Balance Due: €${this.invoice.subtotal - this.invoice.paid}`, {align: "right"});
//  [COMMENT] A blank line between Balance Due and Billing Address.
    pdfkit.moveDown();
    pdfkit.text(`Billing Address:\n${billingAddress.name}`, {align: "right"})
          .text(`${billingAddress.address}\n${billingAddress.city}`, {align: "right"})
          .text(`${billingAddress.state} ${billingAddress.postalCode}`, {align: "right"})
          .text(`${billingAddress.country}`, {align: "right"});
    const _kPAGE_BEGIN = 25;
    const _kPAGE_END = 580;
//  [COMMENT] Draw a horizontal line.
    pdfkit.moveTo(_kPAGE_BEGIN, 200)
          .lineTo(_kPAGE_END, 200)
          .stroke();
    pdfkit.text(`Memo: ${this.invoice.memo}`, 50, 210);
    pdfkit.moveTo(_kPAGE_BEGIN, 250)
          .lineTo(_kPAGE_END, 250)
          .stroke();
}

 

The%20Generated%20Header

The Generated Header

 

Function generateTable()

This function generates the table that contains a list of items.

generateTable(pdfkit)
{
    const _kTABLE_TOP_Y = 270;
    const _kITEM_CODE_X = 50;
    const _kDESCRIPTION_X = 100;
    const _kQUANTITY_X = 250;
    const _kPRICE_X = 300;
    const _kAMOUNT_X = 350;
    pdfkit.fontSize(10)
          .text("Item Code", _kITEM_CODE_X, _kTABLE_TOP_Y, {bold: true, underline: true})
          .text("Description", _kDESCRIPTION_X, _kTABLE_TOP_Y, {bold: true, underline: true})
          .text("Quantity", _kQUANTITY_X, _kTABLE_TOP_Y, {bold: true, underline: true})
          .text("Price", _kPRICE_X, _kTABLE_TOP_Y, {bold: true, underline: true})
          .text("Amount", _kAMOUNT_X, _kTABLE_TOP_Y, {bold: true, underline: true});
    let items = this.invoice.items;
    for (let idx = 0; idx < items.length; idx++)
    {
        let item = items[idx];
        let yCoord = _kTABLE_TOP_Y + 25 + (idx * 25);
        pdfkit.fontSize(10)
              .text(`${item.itemCode}`, _kITEM_CODE_X, yCoord)
              .text(`${item.itemDescription}`, _kDESCRIPTION_X, yCoord)
              .text(`${item.quantity}`, _kQUANTITY_X, yCoord)
              .text(`€${item.price}`, _kPRICE_X, yCoord)
              .text(`€${item.price * item.quantity}`, _kAMOUNT_X, yCoord);
    }
}

 

The%20Generated%20Table

The Generated Table

 

Function drawBoxes()

This function draws some boxes.

drawBoxes(pdfkit)
{
    const _kTOP_H_LINE_X = 100;
    const _kTOP_H_LINE_Y = 500;
    const _kBOTTOM_H_LINE_X = 100;
    const _kBOTTOM_H_LINE_Y = 525;
    const _kH_LINE_LENGTH = 400;
//  [COMMENT] Draw the 2 horizontal lines.
    pdfkit.moveTo(_kTOP_H_LINE_X, _kTOP_H_LINE_Y)
          .lineTo(_kH_LINE_LENGTH, _kTOP_H_LINE_Y)
          .stroke();
    pdfkit.moveTo(_kBOTTOM_H_LINE_X, _kBOTTOM_H_LINE_Y)
          .lineTo(_kH_LINE_LENGTH, _kBOTTOM_H_LINE_Y)
          .stroke();
    const _kV_LINE_1_X = 100;
    const _kV_LINE_2_X = 200;
    const _kV_LINE_3_X = 300;
    const _kV_LINE_4_X = 400;
    const _kV_LINE_LENGTH = 25;
//  [COMMENT] Draw the vertical lines.
    pdfkit.moveTo(_kV_LINE_1_X, _kTOP_H_LINE_Y)
          .lineTo(_kV_LINE_1_X, _kTOP_H_LINE_Y + _kV_LINE_LENGTH)
          .stroke();
    pdfkit.moveTo(_kV_LINE_2_X, _kTOP_H_LINE_Y)
          .lineTo(_kV_LINE_2_X, _kTOP_H_LINE_Y + _kV_LINE_LENGTH)
          .stroke();
    pdfkit.moveTo(_kV_LINE_3_X, _kTOP_H_LINE_Y)
          .lineTo(_kV_LINE_3_X, _kTOP_H_LINE_Y + _kV_LINE_LENGTH)
          .stroke();
    pdfkit.moveTo(_kV_LINE_4_X, _kTOP_H_LINE_Y)
          .lineTo(_kV_LINE_4_X, _kTOP_H_LINE_Y + _kV_LINE_LENGTH)
          .stroke();
//  [COMMENT] Write the text “Box 1”, “Box 2”, and “Box 3” in the boxes.
    pdfkit.fontSize(10)
          .text("Box 1", _kV_LINE_1_X + 5, _kTOP_H_LINE_Y + 5)
          .text("Box 2", _kV_LINE_2_X + 5, _kTOP_H_LINE_Y + 5)
          .text("Box 3", _kV_LINE_3_X + 5, _kTOP_H_LINE_Y + 5)
}

 

The%20Drawn%20Boxes

The Drawn Boxes

 

Function displayImage() – Using Request

This function gets an image from a web URL using the Request library and displays it in the PDF document.

async displayImage(pdfkit)
{
    let requestPayload =
    {
        url: "https://www.sap.com/dam/application/shared/photos/hero-product-category/sap-s4hana-custom-hero.jpg/_jcr_content/renditions/sap-s4hana-custom-hero_3198_1648.jpg.adapt.1599_824.false.false.false.false.jpg/1563795394722.jpg",
        method: "GET",
        encoding: null
    }
    let response = await _REQUEST_UTIL.doRequest(requestPayload);
    let statusCode = response.statusCode;
    let responseBody = response.body;
    let imgBinary = Buffer.from(responseBody, "binary");
//  [COMMENT] The image is encoded in base64 string.
    let imgBase64 = imgBinary.toString("base64");
    let img = Buffer.from(imgBase64, "base64");
    pdfkit.addPage().text("Image").image(img, 100, 100, {width: 400});
}

 

Function displayImage() – Using axios

This function gets an image from a web URL using the axios library and displays it in the PDF document.

async displayImage(pdfkit)
{
    let imageUrl = "https://www.sap.com/dam/application/shared/photos/hero-product-category/sap-s4hana-custom-hero.jpg/_jcr_content/renditions/sap-s4hana-custom-hero_3198_1648.jpg.adapt.1599_824.false.false.false.false.jpg/1563795394722.jpg";
    let response = await _AXIOS.request(
    {
        method: "GET",
        url: imageUrl,
        responseEncoding: "binary"
    });
    let responseData = response.data;
    let imgBinary = Buffer.from(responseData, "binary");
//  [COMMENT] The image is encoded in base64 string.
    let imgBase64 = imgBinary.toString("base64");
    let img = Buffer.from(imgBase64, "base64");
    pdfkit.addPage().text("Image").image(img, 100, 100, {width: 400});
}

 

Function displayImageBase64() – Image Encoded in Base64 String

This function displays an image encoded in base64 string in the PDF document. An image can be read from the file system, encoded in base64 and displayed in the PDF document.

displayImage(pdfkit)
{
//  [COMMENT] The image is encoded in base64 string.
    let imgBase64 = "/9j/4AAQSkZJRgABAgAA ... atrdbG1sbH0LY2Nn/9k=";
    let img = Buffer.from(imgBase64, "base64");
    pdfkit.addPage().text("Image").image(img, 100, 100, {width: 400});
}

 

The%20Displayed%20Image

The Displayed Image

 

Function generateBulletList()

This function generates a bullet list of items.

generateBulletList(pdfkit)
{
    let theList = ["Item A", "Item B", "Item C"];
    let xCoord = 100;
    let yCoord = 400;
    pdfkit.fontSize(10)
          .fillColor("green")
          .list(theList, xCoord, yCoord, {listType: "bullet"});
}

 

The%20Generated%20Bullet%20List

The Generated Bullet List

 

Function generateNumberedList()

This function generates a numbered list of items.

generateNumberedList(pdfkit)
{
    let theList = ["Item A", "Item B", "Item C"];
    let xCoord = 100;
    let yCoord = 450;
    pdfkit.fontSize(10)
          .fillColor("purple")
          .list(theList, xCoord, yCoord, {listType: "numbered"});
}

 

The%20Generated%20Numbered%20List

The Generated Numbered List

 

Function generateLetteredList()

This function generates a lettered list of items.

generateLetteredList(pdfkit)
{
    let theList = ["Item A", "Item B", "Item C"];
    let xCoord = 100;
    let yCoord = 500;
    pdfkit.fontSize(10)
          .fillColor("purple")
          .list(theList, xCoord, yCoord, {listType: "lettered"});
}

 

The%20Generated%20Lettered%20List

The Generated Lettered List

 

Function generateMultiLevelList()

This function generates a multi-level list of items.

generateMutliLevelList(pdfkit)
{
    let theList = ["Item A", ["Item A1", "Item A2"], "Item B", "Item C"];
    let xCoord = 100;
    let yCoord = 550;
    pdfkit.fontSize(10)
          .fillColor("purple")
          .list(theList, xCoord, yCoord, {listType: "bullet"});
}

 

The%20Generated%20Multi-Level%20List

The Generated Multi-Level List

 

Function generateHyperlink()

This function generates a clickable hyperlink.

generateHyperlink(pdfkit)
{
    let linkText = "SAP.com";
    let xCoord = 100;
    let yCoord = 650;
    pdfkit.fontSize(10)
          .fillColor("blue")
          .text(linkText, xCoord, yCoord);
    let width = pdfkit.widthOfString(linkText);
    let height = pdfkit.currentLineHeight();
    pdfkit.underline(100, 650, width, height, {color: "blue"})
          .link(100, 650, width, height, "https://www.sap.com");
}

The tooltip in the figure below shows the website URL of the hyperlink.

 

The%20Generated%20Clickable%20Hyperlink

The Generated Clickable Hyperlink

 

Function generateFooter()

This function generates the footer section of the PDF document.

generateFooter(pdfkit)
{
    pdfkit.fontSize(10)
          .fillColor("black")
          .text("Payment due upon receipt.", 50, 700, {align: "center"});
}

 

The%20Generated%20Footer

The Generated Footer

 

This is the sample of the generated PDF document.

The Generated PDF File

 

Summary

In this blog post, we introduced PDF generation using Node.js. There are other considerations in doing a project, such as template management, document repository management, etc. However, implementations of those properly depend on the business requirements. Whether there is a need to introduce an Enterprise Content Management (ECM) tool is beyond the scope of this blog post.

 

 

Your feedback and comments are most welcome. You may also want to check out the Q&A area of SAP BTP, Cloud Foundry Environment (https://answers.sap.com/tags/73555000100800000287).

 

Be the first to leave a comment
You must be Logged on to comment or reply to a post.