Generating the PDF
Goals:
- Generate a pdf server-side
- return the PDF file to the caller
Step 1
Lets create some helpers
We need a helper to render a full html document to register fonts and interpolate the body of the pdf
// file: apps/pdf-server/src/pdf/helpers/render-pdf-document.ts
import { Font, PDFData } from '@pdf-generation/constants';
const renderFontFaces = (font: Font) => {
return (
font?.fontFaces?.map((fontFace) => {
const family = font.family ? `font-family: ${font.family};` : '';
const style = fontFace.style ? `font-style: ${fontFace.style};` : '';
const weight = fontFace.weight ? `font-weight: ${fontFace.weight};` : '';
const format = fontFace.format ? `format(${fontFace.format})` : '';
return `
@font-face {
${family}
${style}
${weight}
src: url(${fontFace.src}) ${format};
}
`;
}) || ''
);
};
export const renderPdfDocument = (data: PDFData, body: string) => {
const font = renderFontFaces(data.font);
return `
<html>
<head>
<meta charset="utf-8" />
<title>${data.metadata.title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
${font}
body {
-webkit-print-color-adjust: exact !important;
color-adjust: exact !important;
print-color-adjust: exact !important;
}
</style>
</head>
<body>
${body}
</body>
</html>
`;
};
Now we need a helper to set the metadata of the PDF file. Puppeteer, unfortunately, has no way to specify the metadata one may need. Therefore, we use pdf-lib
below to set this metadata and return a Buffer
for further use
import { PDFDocument } from 'pdf-lib';
const metadataSetters = {
title: 'setTitle',
author: 'setAuthor',
creator: 'setCreator',
producer: 'setProducer',
subject: 'setSubject',
};
export async function setPdfMetadata(pdf: Buffer, metadata) {
const setterEntries = Object.entries(metadataSetters);
if (!setterEntries.some(([key]) => !!metadata[key])) return pdf;
const document = await PDFDocument.load(pdf);
setterEntries.forEach(([key, setter]) => {
const value = metadata[key];
if (value) document[setter](value);
});
const pdfBytes = await document.save();
return Buffer.from(pdfBytes);
}
Lets also define a font we want to use
export const DEFAULT_PDF_FONT = {
family: 'Open Sans',
familyDisplay: "'Open Sans', sans-serif",
fontFaces: [
{
src: 'https://fonts.gstatic.com/s/opensans/v18/mem5YaGs126MiZpBA-UN_r8-VeJoCqeDjg.ttf',
weight: 300,
},
{
src: 'https://fonts.gstatic.com/s/opensans/v18/mem8YaGs126MiZpBA-U1UpcaXcl0Aw.ttf',
},
{
src: 'https://fonts.gstatic.com/s/opensans/v18/mem6YaGs126MiZpBA-UFUJ0ef8xkA76a.ttf',
style: 'italic',
},
{
src: 'https://fonts.gstatic.com/s/opensans/v18/mem5YaGs126MiZpBA-UNirk-VeJoCqeDjg.ttf',
weight: 600,
},
{
src: 'https://fonts.gstatic.com/s/opensans/v18/memnYaGs126MiZpBA-UFUKXGUehsKKKTjrPW.ttf',
weight: 600,
style: 'italic',
},
],
};
Step 2
Lets create the service that will do the work.
// file: apps/pdf-server/src/pdf/pdf.service.ts
import { Injectable } from '@nestjs/common';
import * as puppeteer from 'puppeteer';
import { writeFile } from 'fs/promises';
import { PDFDocumentData } from '@pdf-generation/constants';
import { PdfDocProps, renderPdfDoc } from '@pdf-generation/pdf-doc';
import { environment } from '../environments/environment';
import { renderPdfDocument, setPdfMetadata, DEFAULT_PDF_FONT } from './helpers';
@Injectable()
export class PdfService {
async generatePdf(data: PDFDocumentData<PdfDocProps>) {
const browser = await puppeteer.launch({
/**
* Adding `--no-sandbox` here is for easier deployment. Please see puppeteer docs about
* this argument and why you may not want to use it.
*/
args: [`--no-sandbox`],
});
const pdfData = {
...data,
font: data.font ?? DEFAULT_PDF_FONT,
};
const documentBody = renderPdfDoc(pdfData.document, pdfData.font);
const content = renderPdfDocument(pdfData, documentBody);
const pdfOptions = pdfData?.options || {};
const page = await browser.newPage();
await page.setContent(content, {
waitUntil: 'networkidle2',
});
const pdf = await page.pdf({
format: 'a4',
printBackground: true,
preferCSSPageSize: true,
...pdfOptions,
margin: {
left: '40px',
right: '40px',
top: '40px',
bottom: '40px',
...(pdfOptions?.margin || {}),
},
});
// Give the buffer to pdf-lib
const result = await setPdfMetadata(pdf, pdfData?.metadata);
// write to disk for debugging
if (!environment.production) {
const now = Date.now();
const filePath = `${environment.tmpFolder}/${now}-tmp-file.pdf`;
await page.screenshot({
fullPage: true,
path: `${environment.tmpFolder}/${now}-tmp-file.png`,
});
await writeFile(filePath, result);
}
await browser.close();
return result;
}
}
Now whats happening here?
- We are opening a headless chromium browser
- We render the content of the pdf we generate with
renderPdfDoc
- We render the full html document with
renderPdfDocument
- We set the metadata with
setPdfMetadata
if available - We close the browser then return the
Buffer
Step 3
Now we need to wire up the service so its accessible by updating the Controller
// file: apps/pdf-server/src/pdf/pdf.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { PdfService } from './pdf.service';
@Controller('pdf')
export class PdfController {
constructor(private service: PdfService) {}
@Post('/generate')
async generatePdf(@Body() data) {
return this.service.generatePdf(data);
}
}
Step 4
Now finally, wire up the PDF module to the app.
in apps/pdf-server/src/app/app.module.ts
Add PdfModule
to the imports
array
@Module({
imports: [PdfModule], // <== here
controllers: [AppController],
providers: [AppService],
})
Thats it!
Now you can test the api by starting it
pnpm nx run pdf-server:serve
curl --location --request POST 'localhost:3333/api/pdf/generate' \
--header 'Content-Type: application/json' \
--data-raw '{
"id": "sample",
"metadata": {
"title": "Some random pdf",
"subject": "Some radom pdf",
"author": "Statale",
"producer": "pdf-generation-server",
"creator": "My awesome creator"
},
"document": {
"images": [
{
"id": 0,
"url": "https://source.unsplash.com/category/technology/1600x900"
},
{
"id": 1,
"url": "https://source.unsplash.com/category/current-events/1600x900"
}
],
"title": "Some random pdf",
"description": "<p>Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat,...</p>"
}
}'
Top comments (4)
How to generate PDF from HTML in React / Node.js app? wiccan spell to break up a relationship
at the core of this article this is achieved by using puppeteer in a Nodejs app
Im not sure i understood the question fully but let me know if this answers your question.
Please note that there are many alternatives to creating pdf documents and puppeteer is only one that this article focuses on
Thanks for your article, I agree and think that Puppeteer is the best way to create HTML to PDF. Unfortunately it is sometimes a little complicated to achieve on a large scale... In the end I used a micro-saas: doppio.sh
Thanks for the input. I didn't know about this service. I'll check it out