17/08/2021

How to print a browser screen to PDF on a server...

If you have an XPages browser-based application, the user can simply print his screen's contents to a local PDF printer. It is possible to control the layout by using media-dependent CSS. Now, if you'd like to print the identical screen to a PDF file on the server, be prepared for some extra coding and the installation of some additional tools. The general idea is to tell a browser on the server to mimic the user, i.e. to load the same page and then print it to a file.

Requirements

Additional software

Some additional software has to be installed. First, we need a browser that allows headless operations (no user interface needed, it will be controlled by software in the background). At the moment, only chromium is suitable for handling requests from an API in a headless way.

	yum install chromium
I had some issues, some conflicts between the Domino and node environments when starting node directly from the Domino server. A simple and effective solution is to install some batch software to make sure that a node application runs in the default environment.
	yum install at
Finally, we need Puppeteer. It is a Node library which provides a high-level API to control Chromium (link).
	npm install puppeteer
When printing using Puppeteer, the stylesheet media-type can be set to print, so the browser can be fored to handle the screen exactly the same as in a normal user session.

Procedure

As a design choice, in order to desynchronise activities, our application handles many of the more complex activities in the background. These activities are triggered by means of an internal mail, sent to itself (as a mail-in database). An agent executes the code, either before or after the mail arrives.

In this specific case, an internal mail is sent to a (LotusScript) agent, with the following content: the note-id, the session-id and the authentication-id. The first one is required to open the correct document. The other two are special: they are essential to open the document in exactly the same way as the user, by hijacking the user's already authenticated session. That means: no login required, no password, because the server can use the real user's session.

For completeness' sake, the session-id can be obtained with the following code:

	facesContext.getCurrentInstance().getExternalContext().getRequest()
		.getSession().getId()
or in Java:
	(String) new BaseXPage().getRequestScope().get("com.ibm.xsp.SESSION_ID")
And the code for the authentication-id, in client-side JavaScript:
	dojo.cookie("DomAuthSessId")
but it can also be obtained on the server using:
	((Cookie) new BaseXPage().getCookie().get("DomAuthSessId")).getValue()

So, all the ingredients are there to start coding. In LotusScript:

	Dim fn As String
	Dim url As String
	Dim rtrnUrl As String
	Dim cmd As String
	Dim r As Integer
	Dim noteId as String
	
	fn= SystemData.GetSystemTempFolder() + SystemData.Unique() + ".pdf"
	url= SystemData.getHttpStart() + "doc.xsp?openDocument&id=" + noteId
	rtrnUrl= SystemData.getHttpStart() + "xagent.xsp?action=batch&file=" + fn
	cmd= "cd; node makepdf.js '" + url + "' " + fn + " '" + rtrnUrl + "' " _
	 + sessionId + " " + authId + "> /tmp/node.out 2>&1;"
	r= Shell("echo """ + cmd + """ | batch")
		
A brief explanation: a filename is created, the url to open the document and a return-url, and the command to be executed on Linux. The return-url is needed to inform the application that the job is done, and that the file can be attached somewhere. Since node.js is installed for the notes user, a cd is required to switch to the notes home folder, i.e. where the node files are. And finally, the batch command is used to execute the command, because I get all sorts of conflicts when I use Shell(cmd); do not hesitate to test that for yourself!

And what's that makepdf.js, you say? That's the node.js script that controls the browser. Here's a copy:

	// makepdf.js
	//
	// node makepdf.js url filename returnUrl sessionId authId
	//
	const url= process.argv[2];
	const filename= process.argv[3];
	const rurl= process.argv[4];
	const sessionId= process.argv[5];
	const authId= process.argv[6];

	const puppeteer = require('puppeteer');
	const assert = require('assert');

	async function run () {

		const browser = await puppeteer.launch({
			executablePath: '/usr/bin/chromium-browser',
			args: ["--no-sandbox"]
		});

		try {
			const page = await browser.newPage();
			if(!filename)
				filename= 'test.pdf';
			if(sessionId)
				await page.setCookie({url: url, name:'SessionID', value: sessionId});
			if(authId)
				await page.setCookie({url: url, name: 'DomAuthSessId', value: authId});
			await page.goto(url, {waitUntil: 'networkidle0'});
			const pdf = await page.pdf({ path: filename, printBackground: true });
			// report back to application
			const rpage = await browser.newPage();
			await rpage.goto(rurl, {waitUntil: 'networkidle0'});
		} catch(e) {
			console.log(e);
		} finally {
			await browser.close();
		}
	}

	if(!url) {
		console.log('use: node makepdf.js url filename retUrl [sessionId [authId]]');
		return;
	}
	
	run();
		

Is there room for improvement? Undoubtedly, so please let me know!

© 2020 Sjef Bosman · Consultant HCL Domino/Notes
SIRET 442 133 252 00019 · tél. +33 475 252 805
sjef@bosman.fr · sjef.bosman