Home Posts

Pdf Generation Libraries Comparison

Intro

As a Developer, I sometimes need to render PDF documents. However, each library is different, and it's hard to decide which is best for you in terms of time and learning curve. I used to use Pupeteer and JSpdf for this purpose, but is there another way? Everything takes time to research. You are under the pressure of deadlines and tend to choose whatever is easiest or has good reviews. You solve problems on your way, but looking into all possible options consumes a lot of your time. In this article, I will provide information about PDF generation, different libraries, and recommendations on where to use it. I would concentrate on next libraries:

I created a GitHub repo with the Next.js project. For each library(almost), I added the following features:

But I will give you a brief information about alternative solutions as well.

Important disclaimer

As you may know, PDF libraries are quite heavy, so don't include them in your bundle. Always add lazy loading for them.

Example for next.js. For react, you can use React.lazy

const DynamicPDFComponent = dynamic(  
  () => import('./Big_Component_With_Pdf_Rendering'),  
  {  
    ssr: false,  
    loading: () => <span>...loading</span>,  
  }  
);

Also simpliest way to preview PDF in web application is IFrame so don't need to look for fancy library

const blob = new Blob([document.buffer], { type: 'application/pdf' });
const documentUrl = URL.createObjectURL(blob);
...
<iframe  
  src={pdfDocument}  
  width="100%"  
  height="500px"  
  ...
/>

Now, let's start with alternative solutions.

Alternative Solutions

html2pdf.js

It's a wrapper on top of html2canvas and JSPDF. You get an element from DOM, put it into the library, make a screenshot, and put it into PDF.

const element = document.getElementById('element-to-print');

html2pdf(element);

Easy right? On the other hand, it's the same as putting any image into a PDF. The image can be blurry, Some styles might be off(I remember this problem existed when I tried it last time), and texts in the PDF would not be searchable. It's a way, an easy one, but it is not that reliable for me

Playwrite, Pupeeter

You set up a browser on the backend. Yup, it's a browser. You feed it with URL or HTML content, wait for it to finish rendering, and print it using the browser.

Problems? If you need to generate a lot, you'll need to manage your setup wisely. Otherwise, Browser will take up a large portion of your memory or crash. It may be simple but costly if you need to generate many PDF documents. If you need to generate PDF documents quickly and automatically, I think it's better to use a PDF generation library instead of implementing a workaround in a browser. However, it definitely has its own use cases.

And now it's time to start for real

Libriries Review

React PDF

I was skeptical about this library at first. Like, JSX code in react application that would automagically convert into PDF document? It's too good to be true. And surprisingly, I was wrong. For me, it's the number one library now for PDF generation. The only downside is that it depends on React, and it's problematic to use with other frameworks if you want to render it on the client. But in all other cases, it's a breeze to work with it. In addition, you can use familiar ways of styling using CSS in JS like approach

Installation

yarn add @react-pdf/renderer

In order to make your first document, you need to import related components

import {  
  Page,  
  Text,  
  View,  
  Document,  
  StyleSheet,  
} from '@react-pdf/renderer';

It does not support HTML tags, so you need to use their components

let's create first document

import {  
  Page,  
  Text,  
  View,  
  Document,  
  StyleSheet,  
} from '@react-pdf/renderer';

const styles = StyleSheet.create({  
  page: {  
    flexDirection: 'row',  
    backgroundColor: '#E4E4E4',  
  },  
  section: {  
    margin: 10,  
    padding: 10,  
    flexGrow: 1,  
  },  
});

export const PdfDocument = () => {
	return (
		<Document>      
		    <Page size="A4" style={styles.page}>  
		        <View style={styles.section}>  
		          <Text>Section #1</Text>  
		        </View>        
		        <View style={styles.section}>  
		          <Text>Section #2</Text>  
		        </View>      
	        </Page>    
	    </Document>
	);
}

It's so readable and easy to work with. You can create reusable components, pass props, and do everything that you regularly do in JSX.

Want to use a custom font? Easy

import { StyleSheet, Font } from '@react-pdf/renderer' // Register font 

Font.register({ family: 'Roboto', src: source }); // Reference font 

const styles = StyleSheet.create({ title: { fontFamily: 'Roboto' } })

Want to insert an image or other elements? Check their documentation

In case of need, you can even render SVG

Or, if you're working with some Chart generation library, you can save the chart as an image and put it in the document using the Image tag

Once you prepared a document you can show it to a user using their PDFViewer component like

import {  
  PDFViewer
} from '@react-pdf/renderer';
import {  
  PdfDocument
} from './PdfDocument';

const styles = StyleSheet.create({  
  page: {  
    flexDirection: 'row',  
    backgroundColor: '#E4E4E4',  
  },  
  section: {  
    margin: 10,  
    padding: 10,  
    flexGrow: 1,  
  },  
});

export const PreviewDocument = () => {
	return (
		<PDFViewer>
			<PdfDocument />
		</PDFViewer>
	);
}

Or you can render it on BE like

import { renderToStream } from '@react-pdf/renderer';
import {  
  PdfDocument
} from './PdfDocument';

export const reactPdfRenderToStream = () => {  
  return renderToStream(<PdfDocument />);  
};

In order to make a route and send it to the client by API, you need to convert the result from renderToStream(): NodeJS.ReadableStream to ReadableStream<Uint8Array>.

You can do it this way:

export function streamConverter(  
  stream: NodeJS.ReadableStream  
): ReadableStream<Uint8Array> {  
  return new ReadableStream({  
    start(controller) {  
      stream.on('data', (chunk: Buffer) =>  
        controller.enqueue(new Uint8Array(chunk))  
      );  
      stream.on('end', () => controller.close());  
      stream.on('error', (error: NodeJS.ErrnoException) =>  
        controller.error(error)  
      );  
    },  
  });  
}

and send your content to the Client

...
return new NextResponse(doc, {  
  status: 200,  
  headers: new Headers({  
    'content-disposition': `attachment; filename=${document name}.pdf`,  
    'content-type': 'application/zip',  
  }),  
})

As you can see, it's a 100% straightforward library. I would use it as my main one from this point.

PDF me

I have mixed feelings about this one. It has some incredible features but fails to provide basic convenience.

The incredible feature of this library is the UI template builder of PDF documents: https://pdfme.com/template-design. You can literally drag and drop elements where you see fit, play with UI, download template, and... just use it. This feature is shining when you need to make Documents with a limited dynamic compound. If you have a more or less static position of elements, don't have elements with unpredictable sizes(like a table that can have from 2 to infinite rows), or need to provide the possibility for a user to fill fields of the document by himself. it's brilliant.

Curious why I wrote that it fails in basic convenience. Things are different if you need to generate dynamic content and write a template on your own. More about it down below.

Installation

npm i @pdfme/generator @pdfme/common @pdfme/schemas

The whole document should be written in template file like

import { BLANK_PDF, Template } from '@pdfme/common';

const template: Template = {
	basePdf: BLANK_PDF,
	schemas: [
		[
			{
				{  
					name: 'a',  
					type: 'text',  
					position: { x: 0, y: 0 },  
					width: 10,  
					height: 10,  
				},
			}
		]
	]
}

const inputs = { a: 'some text' };

You need to define the base PDF. It can be a blank PDF, as in the example, or you can add content to an existing PDF. During document generation, the name of any item in such a template can be replaced by an input object. Alternatively, you can add a readOnly attribute to it and make it static. This is an interesting feature because you can separate the template itself from the values you want to pass there.

NOTE: If you try to pass a number in inputs, you would get hardly trackable error, always use strings. At the same time, I'm using typescript, so it's quite frustrating that I don't get a type error for it

A full list of possible properties can be found if you play with their UI editor and save a template. I didn't find documentation that covers it

The template itself tends to grow in size rapidly. To make it at least readable I tried to make it into separate functions with reusable styling and I feel like developers didn't expect anyone to write such templates by hands

type Props = {  
  name: string;  
  y: number;  
  position: 'right' | 'left';  
};  
  
export const infoTitle = ({ name, y, position }: Props) => {  
  const x = position === 'left' ? 10.29 : 107.9;  
  return [  
    {  
      name: `${name}Label`,  
      type: 'text',  
      content: 'Invoice From:',  
      position: {  
        x: x,  
        y: y,  
      },  
      width: 45,  
      height: 10,  
      rotate: 0,  
      alignment: 'left',  
      verticalAlignment: 'top',  
      fontSize: 15,  
      lineHeight: 1,  
      characterSpacing: 0,  
      fontColor: '#686868',  
      backgroundColor: '',  
      opacity: 1,  
      strikethrough: false,  
      underline: false,  
      required: false,  
      readOnly: true,  
      fontName: 'NotoSerifJP-Regular',  
    },  
    {  
      name: name,  
      type: 'text',  
      content: 'Type Something...',  
      position: {  
        x: x + 38,  
        y: y,  
      },  
      width: 45,  
      height: 10,  
      rotate: 0,  
      alignment: position,  
      verticalAlignment: 'top',  
      fontSize: 15,  
      lineHeight: 1,  
      characterSpacing: 0,  
      fontColor: '#000000',  
      backgroundColor: '',  
      opacity: 1,  
      strikethrough: false,  
      underline: false,  
      required: true,  
      readOnly: false,  
    },  
  ];  
};

Also, it's frustrating that I need to pass the element's position, height, and width each time. And since you need to provide that information BEFORE rendering something it makes dynamic rendering a bit more complicated. Imagine you want to generate a table. And this table can take any size. So you need to calculate its height, how to do it? you need to calculate it on top of the function because we're making an object here

const tableLastY = 89 + (items + 1) * baseTableItemHeight;

Problems? If your text element is bigger than the item itself it will grow in size, but since you didn't know about it the text will overlap

Image of overflowing text

I know that it's relevant to most libraries. But in those libraries, I can at least calculate the size of the text before rendering it. How to do it here? I don't know, it's a mystery. But maybe there's a way

The basic generation of the document looks like

import { text } from '@pdfme/schemas';

...

generate({  
  template,  
  inputs,  
  plugins: { Text: text },  
})

As a result of generate you would get Promise<UInt8>, and you also need to list all used plugins. Sometimes it's unclear what plugins they have. I didn't find good documentation that fully covers it. But if you try to use something that's not listed in plugins, then you would get an error. and you can send it from BE like

return new NextResponse(doc, {  
  status: 200,  
  headers: new Headers({  
    'content-disposition': `attachment; filename=${data.title}${data.invoiceId}.pdf`,  
    'content-type': 'application/zip',  
  }),  
});

In conclusion: If you have the means to prepare a Template that covers all your needs, then everything is simple. If you need to generate a template by code and support dynamic content, then it's better to look for other options

PDF Make

This library is my second favorite now. The only reason it's second is that I could not make it work on the server side. I had a limited amount of time, so maybe later, I would fill this gap. Other than that, its Framework agnostic provides a convenient way of styling, doesn't rely on the absolute positioning of elements, and provides everything that React PDF delivers but with a bit less convenient way of declaring PDF template

It wraps around PDFKit. As a result, I decided to now include PDF kit library in this article

Installation

npm install pdfmake

Template structure: You have 2 fields in the documents styles - for reusable styles and content for content

{
	content: [
		{ text: 'Invoice', style: 'header' },  
		{ text: data.invoiceId, alignment: 'right' },
	],
	styles: {
		header: {  
		  fontSize: 22,  
		  bold: true,  
		  alignment: 'right',  
		},
	}
}

It also supports columns to render tables or multi-column texts

{  
  margin: [0, 40, 100, 0],  
  align: 'right',  
  columns: [  
    {  
      // star-sized columns fill the remaining space  
      // if there's more than one star-column, available width is divided equally      
      width: '*',  
      text: '',  
    },  
    {  
      // auto-sized columns have their widths based on their content  
      width: 'auto',  
      text: 'Total',  
    },  
    {  
	    width: 'auto',  
	    text: calculateTotalOfInvoice(data.items),  
    },  
  ],  
  // optional space between columns  
  columnGap: 10,  
}

I like that everything can be settled by margins between elements, and you can use columnGap instead of trying to calculate everything by yourself.

And generation on the client side looks like

import * as pdfMake from "pdfmake/build/pdfmake";
import * as pdfFonts from 'pdfmake/build/vfs_fonts';

(<any>pdfMake).addVirtualFileSystem(pdfFonts);

pdfMake.createPdf(template)
const blob = pdfDocGenerator.getBlob();

You need to provide fonts for the library to work.

Things are a bit different on the BE side. In short, there're 3 differences

  1. You get PDFkit as a result of generation
  2. Hardly trackable errors
  3. You need to pass fonts during creation.

The code looks like

import PdfPrinter from 'pdfmake';  
import path from 'path';  
import { IInvoice } from '../../types/invoice';  
import { templateBuilder } from './templateBuilder';  
  
const fonts = {  
  Roboto: {  
    normal: path.resolve('./fonts/Roboto-Regular.ttf'),  
    bold: path.resolve('./fonts/Roboto-Medium.ttf'),  
    italics: path.resolve('./fonts/Roboto-Italic.ttf'),  
    bolditalics: path.resolve('./fonts/Roboto-MediumItalic.ttf'),  
  },  
};  
  
export const pdfMakeServerGeneration = (  
  data: IInvoice  
): Promise<NodeJS.ReadableStream> => {  
  return new Promise((resolve) => {  
    const printer = new PdfPrinter(fonts);  
    const docDefinition = templateBuilder(data);  
    resolve(printer.createPdfKitDocument(docDefinition));  
  });  
};

Problems? Yup, when I try to return the result in next.js, it makes my route not exist and doesn't provide any debugging information. If I published the article and didn't remove this place, then it means that I didn't find how to fix the problem. Feel free to write me if you know how to solve it)

Other than that, it's on par with React PDF, and once I fix the Backend problem, it would be superior just because it's framework-agnostic

PDF Lib

This one I didn't like that much. For one simple reason (0, 0) point of the document is in the bottom left corner. So you need to make around your whole thinking and reinvent the wheel to make it point to the top left corner. A bit of inconvenience yes but together with a weird color function

import { rgb } from 'pdf-lib';

rgb(101 / 255, 123 / 255, 131 / 255),

and better alternatives it may be something that you want to think twice before using.

Installation

yarn add pdf-lib

Basic usage

import { Color, PDFDocument, PDFFont, PDFPage, StandardFonts } from 'pdf-lib';

const fontSize = 18;
const doc = await PDFDocument.create();  
const font = await doc.embedFont(StandardFonts.TimesRoman);  

const page = doc.addPage();  
page.setFont(font);
const { height } = page.getSize();

const elementHeight = font.sizeAtHeight(fontSize); 

page.drawText('Some text', {  
  x: 10,  
  y: height - (elementHeight + 10),  
  size: fontSize,  
  color,  
});

Since you're rendering from the bottom to the top, you need to deduct the height of the element like height - (elementHeight + spacing on top) to make sure that your text is not cropped

Also as you can see, in order to use the library, you need to register the default font first, like

const font = await doc.embedFont(StandardFonts.TimesRoman);  
page.setFont(font);

and if you want to learn the size of the text element you need to do it by font object as well

const elementHeight = font.sizeAtHeight(fontSize); 

Such little inconvenience makes me think that JSPdf is a better option than this one. It provides the same way of layout declaration, but you need to pass fewer variables in each function.

Also, since you're pointing to the exact location, you need to save and update the pointer to the last rendered item in case you want to render something dynamically

let lastRenderedItem = 180;  
  
data.items.forEach((x) => {  
  wrapper.drawText({  
    text: x.title,  
    x: 30,  
    y: lastRenderedItem,  
    size: regularFont,  
  });  
  wrapper.drawText({  
    text: `${x.quantity}`,  
    x: 240,  
    y: lastRenderedItem,  
    size: regularFont,  
  });  
  wrapper.drawText({  
    text: `$${x.rate}`,  
    x: 360,  
    y: lastRenderedItem,  
    size: regularFont,  
  });  
  wrapper.drawText({  
    text: `$${(x.quantity ?? 0) * (x.rate ?? 0)}`,  
    x: 490,  
    y: lastRenderedItem,  
    size: regularFont,  
  });  
  lastRenderedItem += 22;  
  page.drawLine({  
    start: { x: 10, y: height - lastRenderedItem - 5 },  
    end: { x: width - 10, y: height - lastRenderedItem - 5 },  
    color: rgb(101 / 255, 123 / 255, 131 / 255),  
  });  
});

on the bright side, you can use the same code for both the client and backend and save your document like

const pdf: UInt8Array = await doc.save();

JSPdf

This is the library I'm most familiar with. And for me it seems quite straightforward so not much details about this one. I like it, but managing big documents is a hassle. But I would prefer it other than PDF lib

Installation

yarn add jspdf

Basic usage

import { jsPDF as JsPDF } from 'jspdf';

const doc = new JsPDF();  

doc.setFontSize(16);
doc.setTextColor('black');
doc.text('Some text', padding, 45);

const result = new Uint8Array(doc.output('arraybuffer'));

You need to be careful because the library is synchronous. So wrap it in a promise to not block your main thread for too long.

With this library, you can do almost anything: add images, draw, generate content(but keep the position pointer the same as in the PDF lib), and so on. 0,0 points to the top left corner by default and all styles that you define have block scope.

Example

doc.setFontSize(16);
doc.setTextColor('black');
/* block starts, everything would use this font size and color*/
...
doc.setFontSize(17);
doc.setTextColor('gray');
/* new block with scoped usage*/
...

This way, you can group together content that requires similar styles or write reusable functions to toggle styles on and off on demand.

You can render it in an array buffer or save it to a local machine right away

const result = new Uint8Array(doc.output('arraybuffer'));

Conclusion

From this point, I have two favorite generation libraries: React PDF and PDF Me. I am looking forward to using them more and experiencing any hidden problems they may have. It's a review article for libraries that I used for the first time. If you see any problems, feel free to write a comment or message me.

Hope it was of help to you)