Generate a PDF from HTML on Servicenow

The second post in the series about the Servicenow x NodeJS Bridge. A how-to for generating a PDF from arbitrary HTML.

Motivation

I am sure you know the feeling, when you want to generate a custom PDF on Servicenow. You search for a way, but the platform just simply does not offer enough. In this little how-to I would like to show you the process of creating a custom PDF while using Servicenow. We will use the very familiar and popular chrome browser for this. If I have your attention now, keep reading!

Preparations

For this tutorial to work, you will have to install and configure the Servicenow x nodeJS Bridge project on your instance. This allows you access and resources to the tech stack we will be using. You can refer to the Get Started article on the Wiki. That article contains generic setup instructions.

Get started

For generating a PDF, we will need a bit of customizations to the core repo. I wanted to make this as easy as possible for you. Read on for more details.

Steps

1. Import the scoped application to your instance

First, you need to import the Scoped App to your servicenow instance. If in doubt, follow the instructions on the Servicenow Docs.

You can find the Scoped App here: https://github.com/servicenow-node-bridge/scoped-app

2. Clone the node vm

Next you will need to clone the repository for the pre-configured node vm. I’ve prepared this for you, to ease the process.

$ git clone https://github.com/servicenow-node-bridge/pdfgen-nodevm.git

The vm has been pre-installed with the request package and puppeteer. Puppeteer is needed to control the chrome browser as a ‘puppet’, that is where the name comes from. We will leverage this to run and execute commands in the chrome headless enviroment.


Puppeteer is a powerful tool. I will only show you one of the many things it can do. With puppeteer, you can open web pages and render them to a PDF. You are able to execute arbitrary javascript on webpages.These webpages could be then saved to a screenshot or pdf and many more.

What is Chrome headless?

Chrome headless is basically a browser, but without the graphical user interface. We do not really need that in our case. This allows us to run chrome on the server.

You can use any enviroment which is able to run docker.

I will only show you a very simplistic part of it, generating a PDF from a html string.

After cloning the repo, you will have to install docker-compose. You can find more info in the get started Wiki.

Configure the docker containers

Put docker-compose.yaml into your folder where you cloned the repo to. You can also rename the docker-compose.sample.yaml to docker-compose.yaml and configure it.

version: "2"
services:
  node:
    image: "node"
    user: "node"
    restart: always
    working_dir: /home/snc-node-bridge
    environment:
      - NODE_ENV=production
    networks:
      static-network:
        ipv4_address: 172.20.128.2
    links:
      - "chrome"
    volumes:
      - ./snc-node-bridge:/home/snc-node-bridge
    command: "yarn start"
  rest-web:
    image: nginx
    restart: always
    volumes:
      - ./nginx/conf.d/:/etc/nginx/conf.d/
    networks:
      static-network:
        ipv4_address: 172.20.128.3
    links:
      - "chrome"
    ports:
      - "8080:8080"
  chrome:
    image: zenika/alpine-chrome:latest
    command: --no-sandbox --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222
    ports:
      - "9222:9222"
    networks:
      static-network:
        ipv4_address: 172.20.128.4
networks:
  static-network:
    ipam:
      config:
        - subnet: 172.20.0.0/16

This file is an extension of the original file for the core node-vm. I have created a static network and assigned static IP addresses to each container. This is only for ease of use, you are free to alter it.

In this case, we will have 3 containers:

  • nodeJS VM [ip:172.20.128.2]
  • nginx webserver [ip:172.20.128.3]
  • chrome headless on alpine linux [ip:172.20.128.4]

Now run the following command to start the containers:

$ docker-compose up

Now we can go back to the Bridge Scripts on our instance.

Create one with the following content:

// require puppeteer, fs and request packages, these are preinstalled in your node vm already if you've used the repo
const puppeteer = require('puppeteer');
const fs = require('fs');
const request = require('request');

// async function for generating the pdf
async function printPDF() {
    // let's make a connection to the chrome headless browser, notice the IP, this is the address of the container running the browser instance
    const browser = await puppeteer.connect({ browserURL: "http://172.20.128.4:9222", headless: true, slowMo: 250 });
    // open a new tab
    const page = await browser.newPage();
    await page.setViewport({ width: 1920, height: 1080 });
    // get the number of the incident from our bootstrap script
    let incNum = _getData()['result'][0]['number'];
    // html contents
    const htmlContent = '<html>'+
'<head>'+
'<title>Some nice title</title>'+
'</head>'+
'<body>'+
'<h3>This is a h3 heading for '+ incNum + '</h3>'+
'<img src="https://images.pexels.com/photos/1130980/pexels-photo-1130980.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500" alt="picture here" />'+
'</body>'+
'</html>';
    // set the page's content to the html string
    await page.setContent(htmlContent);
    await page.emulateMedia("screen");
    // save the pdf locally to a temporary folder, you need to make sure that the node vm has access to the folder
    await page.pdf({ path: "./tmp/rendered_from_html.pdf", format: 'A4' });
    // after render and save, close the page
    await page.close();
    // disconnect from the browser
    await browser.disconnect();
    // read the contents of the file into the variable
    const pdfFileData = base64_encode("./tmp/rendered_from_html.pdf");
    // use the postback function to send back the contents to the log entry table
    doPostBack(_getLogID(), {pdf: pdfFileData});
}


// function to encode file data to base64 encoded string
function base64_encode(file) {
    // read binary data, in sync mode, not ideal, but for the demo purpose it is enough
    var bitmap = fs.readFileSync(file);
    // convert binary data to base64 encoded string
    return new Buffer(bitmap).toString('base64');
}

const doPostBack = (logID, data) => {
    // use basic auth for api authentication user=rest.default & password=rest.default
    const baseUrl = "https://rest.default:rest.default@instancename.service-now.com";
    const endpoint = baseUrl + "/api/x_321937_snc_node/postback";
    request.post(endpoint, {
        json: {
            logID: logID,
            result: data
        }
    }, (error, res, body) => {
        if (error) {
            console.error(error);
            return
        }
        console.log(`statusCode: ${res.statusCode}`);
        console.log(body)
    })
};
// call the async function
printPDF();

Also as a demo, you can set a Data bootstrap script.

This could look something like this:

data = (function() {
	var gr = new GlideRecord("incident");
	// this is an incident from the demo data
	if (gr.get("57af7aec73d423002728660c4cf6a71c")) {
		return {"result": [
			{
				"number": gr.number + ""
			}
		]};
	} else {
		return {"result": []};
	}
})();

Save the Bridge script and then click the Run Script UI Action.

Now you should refer to the Execution log and wait for your script to be executed.

The result of the bootstrap script, should be an object or an array. This will be sent along the script. You can access the data with the _getData() internal function in your Bridge Script.


This is only for inspiration. There are many possibilities how this could be used. You could open a form on a Servicenow instance and render it to a PDF or a screenshot.

The postback script is also just an inspiration, you can leverage the REST API of Servicenow, to create/update or delete data based on the outcomes of the script.

If you have any tips or any requests, that would interest you, let me know. I can create how-to’s for other interesting ideas. You are free to create your own of course. Keep using the tool, give feedback and new improvements.

Disclaimer: The statements and opinions written in this post, may not reflect the view of the company I am currently employed at. These are my personal statements and observations only. Please consider them as such. Thanks!

Richard Szolár
Richard Szolár
Software Developer

Software developer with many years of experience. Tech enthusiasts, recently working with Go and React. Also reading a lot of 📚 and I ❤️ art 🍿🎬