Setting up Prince on AWS Lambda and API Gateway

Bruce Lawson
7 min readJun 8, 2020

--

Part 1: assembling a Lambda deployment package

AWS Lambda is “serverless”; a way of uploading code you want to run to the cloud without having to worry about providing a server to run it on. Lambda itself takes care of that; if nothing’s using your code, it isn’t running (and isn’t costing you anything) and when you do need it to run, Lambda finds somewhere to run it, does so, and then closes that back down. It’s quite a convenient way of running some code without needing to set up servers to do it; for example, using Prince to convert an HTML document to PDF. Prince is free for non-commercial use.

Once it’s set up, it pretty much Just Works™. Setting it up need not take a long time, but it can be a frustrating experience if you haven’t done it before because the learning curve can be steep, especially for something you probably only want to do once. With this in mind, here’s a step-by-step guide for the AWS newbie.

To add your code to Lambda, you need to create a “deployment package”; this is your code, plus everything else it needs — other files, other libraries, and so on — all in one zip file. That zip file is then uploaded to Lambda. The deployment package for this task will include Prince, as well as a small amount of “glue” code designed to take an uploaded HTML file, pass it to Prince, and pass the resulting PDF back again.

The Prince team have created the Prince 13.5 Lambda deployment package (the latest stable official release). Download and unzip it. It contains Prince’s executables, an XML file called fonts.conf to tell Lambda where fonts can be found, libgomp for multithreaded processing (we use it to speed up our implementation of SVG/CSS image filters), some font utility files Prince requires, and some core fonts called Liberation fonts that will be used as the fallback serif, sans-serif and monospace fonts. They are metrically compatible with the most popular fonts on the Microsoft Windows operating system and the Microsoft Office software package (Arial, Times New Roman and Courier New), and are licensed for redistribution (the Microsoft fonts are not).

If your HTML files and PDFs require any other specific fonts, add the font files to the fonts directory now.

Create a file called index.js with the following content:

var execFile = require('child_process').execFile;function tinyMultipartParser(data) {
// assume first line is boundary
const lines = data.split("\r\n");
const boundary = lines[0];
const endboundary = boundary + "--";
const boundaries = lines.filter(l => l == boundary).length;
if (boundaries != 1) { throw new Error(`Unexpected boundaries ${boundaries}`); }
const endboundaries = lines.filter(l => l == endboundary).length;
if (endboundaries != 1) { throw new Error(`Unexpected end boundaries ${boundaries}`); }
const output = [];
let in_body = false;
lines.forEach(line => {
if (line.trim() == "" && !in_body) { in_body = true; return; }
if (!in_body && line.match(/^content-type: /i) && !line.match(/text\/html/)) { throw new Error("not html"); }
if (line.indexOf(boundary) > -1) return;
if (in_body) output.push(line);
})
return output.join("\n");
}
exports.handler = function(event, context, done) {

if (!event || !event.body) { return done(new Error("No data.")); }
let body = event.body;
if (event.isBase64Encoded) {
body = Buffer.from(body, "base64").toString("ascii");
}
let html = tinyMultipartParser(body);
let opts = {timeout: 10*1000, maxbuffer: 10*1024*1024, encoding: "buffer"};
let child = execFile("./prince", ["-", "-o", "-"], opts, function(err, stdout, stderr) {
if (err) { return done(err); }
if (err === null && (m = stderr.toString().match(/prince:\s+error:\s+([^\n]+)/))) {
return done(new Error(m[1]));
}
done(null, {
"isBase64Encoded": true,
"statusCode": 200,
"headers": { "Content-Type": "application/pdf" },
"body": stdout.toString("base64")
});
});
child.stdin.write(html);
child.stdin.end();
};

This Node.js JavaScript file receives a sent HTML file, and executes prince on it, sending the resulting PDF back to the requester.

The line sets up an array of Prince’s command line options, so add any that you need here (for example, to tell Prince to produce an accessible, tagged PDF):

let child = execFile("./prince", ["-", "--output=-", "--pdf-profile=PDF/UA-1"], opts, function....

Create a zip file (its name does not matter). In it, add the above createdindex.js, and the contents of the unpacked Prince zip file.

This is the deployment package: everything that is required to run Prince in the Lambda system. Next, set up Lambda itself.

Part 2: Setting up AWS Lambda

First sign in to the AWS console.

Under All services > Compute, choose Lambda.

Now it is time to create a function. Click the “Create Function” button to start. On the Create Function screen, ensure that “Author from scratch” is selected. Under “Function name”, write “prince”, and under “Runtime”, ensure that “Node.js 12.x” is selected. Click “Create function”.

This will take a few seconds to create your empty Lambda function, and then take you to a screen where it can be configured. In the “Function code” area, Lambda has provided you with some template code, but you will supersede that by uploading your deployment package.

In the “Function code” section, ensure that “Node.js 12.x” is selected under “Runtime”, and that “Handler” reads index.handler. Under "Code entry type", select "Upload a zip file". Click the new "Upload" button and browse to the zip file you created in Part 1. Then at the top of the page, choose the "Save" button. It may take a little time to upload.

Finally in configuring the Lambda function, scroll down to “Basic settings” and click “Edit”. In this edit screen, change the Timeout to something larger than 3 seconds; starting up Prince and rendering a PDF often takes longer than that. 10 seconds should be enough for testing; if jobs frequently time out, then increase this limit.

Part 3: Setting up API Gateway

You now have a Lambda function which runs Prince on an HTML file and returns a PDF. However, this function is not yet available to be called; making it so is the purpose of the AWS API Gateway. Browse to the AWS console and under “Networking & Content Delivery” choose “API Gateway”. This should then offer you the chance to create a new API; under “Choose an API type”, find “HTTP API” and click “Build”.

On the “Create an API” page, click “Add integration”, and in the resulting dropdown, choose “Lambda”. Click in the resulting “Lambda function” box and you should see “prince” appear, which is your Lambda function from part 2. Click this to select it. In the “API name” box, enter “Prince API” (this name does not matter), and click Next.

In the “Configure routes” screen, under “Method” choose “POST”, set “Resource path” to /prince (if it isn't already), and ensure "Integration target" is set to "prince". This defines your HTTP endpoint (it will be at /prince). Click Next. In "Configure stages", leave "Stage name" at "$default" and click Next (stages are for complicated deployment setups and aren't needed here).

On the “Review and Create” screen, click “Create”. This should create your API Gateway. Now, return to the AWS console and navigate back to Lambda. In the Functions screen in AWS Lambda, choose your “prince” function. The Designer should now show an “API Gateway” trigger joined to the “prince” function.

Clicking on that API Gateway trigger will reveal an API Gateway area below the designer, in which will be listed the “API endpoint”. It will look something like https://abcdefghijk.execute-api.us-east-1.amazonaws.com/prince. This is your HTTP API, to which you can POST an HTML document from an HTML form and get back a PDF.

To test this, create a file form.html with the following content, and open it in a web browser. Be sure to edit the form action to be the API endpoint from above.

<!DOCTYPE html>
<html><head><meta charset="utf-8"></head>
<body>
<form method="POST" action="https://abcdefghijk.execute-api.us-east-1.amazonaws.com/prince" enctype="multipart/form-data">
<input type="file" name="html">
<input type="submit">
</form>
</body></html>

Now in that form in a browser, click Browse and choose an HTML document to be converted to PDF, and click Submit Query. If everything worked, you should now see a PDF version of that HTML document, dynamically created by Prince in your AWS Lambda.

(If it did not work, there may be some error messages in the AWS Cloudwatch logs for your function.)

And there you are — you now have Prince running ‘serverlessly’, for a fraction of the costs of maintaining your own server. If you would prefer to run Prince serverlessly on PHP, read Setting up Prince on EC2 with PHP.

The Prince team are grateful to Martin Beeby of AWS, and Stuart Langridge for their assistance with this article.

--

--

Bruce Lawson

Web geek. Open Standards/ Open Source consultant. Groovy cat.