DEV Community

Dmitrii Boikov
Dmitrii Boikov

Posted on • Edited on • Originally published at dmitriiboikov.com

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:

  • React PDF
  • PDF lib
  • PDF me
  • PDF make
  • JSPDF

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

  • Client-side generation
  • Server-side generation
  • Download buttons for Server/Client generated documents
  • Dynamic Table Generation
  • Template form to fill data

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>,  
  }  
);
Enter fullscreen mode Exit fullscreen mode

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"  
  ...
/>

Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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, the 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 using a PDF generation library is better than implementing a workaround in a browser. However, it definitely has its own use cases.

And now it's time to start for real

Libraries Review

React PDF

I was skeptical about this library at first. JSX code in a React application that would automagically convert into a PDF document? It's too good to be true.
Surprisingly, I was wrong. It's now my number one library 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
Enter fullscreen mode Exit fullscreen mode

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

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

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

  • Page - Unit that represents a page, you can separate content by pages by yourself or make everything in one page and let React PDF wrap the content for you
  • View - it's grouping element, aka div
  • Text - it's span or p, obviously
  • Document - it's a wrapper and top-level element.
  • StyleSheet - CSS in JS like component for reusable styles

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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' } })
Enter fullscreen mode Exit fullscreen mode

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 have 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>
    );
}
Enter fullscreen mode Exit fullscreen mode

Or you can render it on BE like

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

export const reactPdfRenderToStream = () => {  
  return renderToStream(<PdfDocument />);  
};
Enter fullscreen mode Exit fullscreen mode

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)  
      );  
    },  
  });  
}
Enter fullscreen mode Exit fullscreen mode

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',  
  }),  
})
Enter fullscreen mode Exit fullscreen mode

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 shines 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
Enter fullscreen mode Exit fullscreen mode

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' };
Enter fullscreen mode Exit fullscreen mode

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,  
    },  
  ];  
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 },  
})
Enter fullscreen mode Exit fullscreen mode

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',  
  }),  
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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',  
        },
    }
}
Enter fullscreen mode Exit fullscreen mode

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,  
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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));  
  });  
};
Enter fullscreen mode Exit fullscreen mode

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),
Enter fullscreen mode Exit fullscreen mode

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

Installation

yarn add pdf-lib
Enter fullscreen mode Exit fullscreen mode

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,  
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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); 
Enter fullscreen mode Exit fullscreen mode

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),  
  });  
});
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'));
Enter fullscreen mode Exit fullscreen mode

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*/
...
Enter fullscreen mode Exit fullscreen mode

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'));
Enter fullscreen mode Exit fullscreen mode

Conclusion

From this point, I have two favorite generation libraries: React PDF and PDF Make. 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)

Top comments (6)

Collapse
 
ansellmaximilian profile image
Ansell Maximilian

Thanks so much for this list! I used Puppeteer in the past in combination with handlebars to bulk print custom invoices, but I think I'll try that React one.

Collapse
 
dmitrii_boikov profile image
Dmitrii Boikov

Yup, it would be more performant. But if you have a designer and custom invoices that are more or less static(like no dynamic rows in the table), then I think PDFMe would be even easier to integrate. Purely because you would make a template in UI builder and use it in generation as it is

Collapse
 
kiiromame profile image
キイロマメ

I came to the same conclusion as you.
As a result, I ended up using PDFMake in a Vue 3 project to generate dynamic tables as PDF.
It worked well, but it was somewhat tedious to implement.

Collapse
 
dmitrii_boikov profile image
Dmitrii Boikov

I can relate. PDF generation always has trade-offs.
But using margins instead of absolute positioning is nice

Collapse
 
and_pav_b176d733b42cc429d profile image
and pav

Good job! A very informative read!

I have a question, did you ever try react-pdf-viewer ? It has plugin architecture (as well as PDF me) and looks simple for usage.

Collapse
 
dmitrii_boikov profile image
Dmitrii Boikov

I think the purpose of react-pdf-viewer is different than for PDF generation libraries.

I didn't use it before, but as I can see, it provides reach functionality to view and interact with PDF documents but doesn't allow you to generate one. Am I wrong?