Creating a certificate template with PDFKit in Node.js
By: Eduardo Quintino
Tags: Node, Certificate, Pdfkit, Certification, Template

I was learning about how to make pdf certificates and created this guide to help other developers with PDFKit functionalities.

PDF Certificate model

This article shows some key functionalities examples of how I managed to create the certificate above.

The complete code is available at the end.

Creating the server

Create a folder of your preference and start the project

yarn init -y

Install dependencies

yarn add pdfkit

yarn add nodemon -D

Add in package.json a dev script to help in the process

“license”: “MIT”,

“scripts”: {

“dev” : “nodemon index.js”

},

Create a file index.js with this initial setup

const PDFDocument = require('pdfkit');

const fs = require('fs'); const doc = new PDFDocument({

layout: 'landscape',

size: 'A4',

}); doc.pipe(fs.createWriteStream('output.pdf')); doc.rect(0, 0, doc.page.width, doc.page.height).fill('#fff'); doc.end();

Start the server

yarn dev

It should generate an empty file in output.pdf

Margins

For web developers, working with PDFkit to some objects is like using ‘position: absolute’ in CSS.

Below there is an example of how to create a margin in your document:

const distanceMargin = 18; doc

.fillAndStroke('#0e8cc3')

.lineWidth(20)

.lineJoin('round')

.rect(

distanceMargin,

distanceMargin,

doc.page.width - distanceMargin * 2,

doc.page.height - distanceMargin * 2,

)

.stroke();

.lineJoin(‘round’) is the shape of the margin, in PDFKit guide there is also: ‘butt’, ‘round’, ‘square’, ‘miter’, and ‘bevel’.

distanceMargin is a constant to help with the margin starting position

doc.page.width is the total pdf width

doc.page.height is the total pdf height

.rect sets where to insert the margin

PDF Margin

Image centralized

To centralize an image dynamically (if height and width might change) at some position use image fit:

const maxWidth = 140;

const maxHeight = 70; doc.image(

'assets/winners.png',

doc.page.width / 2 - maxWidth / 2,

60,

{

fit: [maxWidth, maxHeight],

align: 'center',

}

);

‘assets/winners.png’ is the path for your image

doc.page.width / 2 — maxWidth / 2 is a way to centralize

60 is the height you want for the image

fit: [maxWidth, maxHeight] is the ‘block’ your image will be inside

*Using this concept, I also added a QR code image at the bottom.

Image at center (horizontally)

Jumping Lines (with moveDown)

It is possible to move elements down with PDFKit:

doc.moveDown();

To help in the development it is also possible to use a helper function:

function jumpLine(doc, lines) {

for (let index = 0; index < lines; index++) {

doc.moveDown();

}

}

Then, for example, to moveDown 5 lines:

jumpLine(doc, 5);

Texts with fonts

To insert a text with some font in your document, I recommend downloading from Google Fonts. Then import it to the project, for example in a folder called assets.

doc

.font('fonts/NotoSansJP-Light.otf')

.fontSize(10)

.fill('#021c27')

.text('Super Course for Awesomes', {

align: 'center',

}

);

.font(‘fonts/NotoSansJP-Light.otf’) is where the font is located

.fontSize(10) is the size of the font (is not in pixels)

.fill(‘#021c27’) is the color you want

.text(‘Super Course for Awesomes’, { align: ‘center’, } is the text itself at center

Text with a font

Adding more texts and moving down lines, it will look like this:

More example texts in PDF

Signatures

To signatures, centralize texts (like text-align: center) was a little bit different, maybe it is not the best way to do it, but I have managed to create centralized texts setting one column for each text:

const lineSize = 174;

const signatureHeight = 390; doc.lineWidth(1);

doc.fillAndStroke('#021c27');

doc.strokeOpacity(0.2); const startLine1 = 128;

const endLine1 = 128 + lineSize; // Creates a line

doc

.moveTo(startLine1, signatureHeight)

.lineTo(endLine1, signatureHeight)

.stroke(); // Evaluator info

doc

.font('fonts/NotoSansJP-Bold.otf')

.fontSize(10)

.fill('#021c27')

.text(

'John Doe',

startLine1,

signatureHeight + 10,

{

columns: 1,

columnGap: 0,

height: 40,

width: lineSize,

align: 'center',

}

); doc

.font('fonts/NotoSansJP-Light.otf')

.fontSize(10)

.fill('#021c27')

.text(

'Associate Professor',

startLine1,

signatureHeight + 25,

{

columns: 1,

columnGap: 0,

height: 40,

width: lineSize,

align: 'center',

}

);

const lineSize = 174; is the width of each signature

const signatureHeight = 390; is the initial height on page

doc.lineWidth(1); ensures line width is 1.

doc.fillAndStroke(‘#021c27’); sets color

doc.strokeOpacity(0.2); changes the opacity

columns: 1, sets the number of columns the text will fill

sets the number of columns the text will fill width: lineSize, sets the width of each column

align: ‘center’, align each column text at the center

The first signature will look like this:

Signature at left

Adding more signatures fields:

Signature fields

Text Link

For the validation link, I have inserted an underline and a text in the document:

const link = 'http://www.validate-your-certificate.hello/validation-code-here'; doc.fontSize(10) const linkWidth = doc.widthOfString(link);

const linkHeight = doc.currentLineHeight(); doc

.underline(

doc.page.width / 2 - linkWidth / 2,

450,

linkWidth,

linkHeight,

)

.link(

doc.page.width / 2 - linkWidth / 2,

450,

linkWidth,

linkHeight,

link,

); doc

.fontSize(10)

.fill('#021c27')

.text(

link,

doc.page.width / 2 - linkWidth / 2,

448,

linkWidth,

linkHeight,

);

doc.fontSize(10) to ensure the font size to be measured

.underline( … ) positions a centralized underline at height 450

.link( …, link) includes the link itself

Final certificate in PDF

Conclusion

I found out there are many ways to achieve the same results with PDFKit, and they are a little bit unusual, in my opinion, for who is adapted to CSS flexbox. I found out a little bit hard to find templates for PDFKit for this context.

Give a star on GitHub complete code if it was helpful:

https://github.com/eduqg/pdfkit-node-certificate-template

And that’s all!

I write to learn more and help other people in the process.