Что нового
  • Что бы вступить в ряды "Принятый кодер" Вам нужно:
    Написать 10 полезных сообщений или тем и Получить 10 симпатий.
    Для того кто не хочет терять время,может пожертвовать средства для поддержки сервеса, и вступить в ряды VIP на месяц, дополнительная информация в лс.

  • Пользаватели которые будут спамить, уходят в бан без предупреждения. Спам сообщения определяется администрацией и модератором.

  • Гость, Что бы Вы хотели увидеть на нашем Форуме? Изложить свои идеи и пожелания по улучшению форума Вы можете поделиться с нами здесь. ----> Перейдите сюда
  • Все пользователи не прошедшие проверку электронной почты будут заблокированы. Все вопросы с разблокировкой обращайтесь по адресу электронной почте : info@guardianelinks.com . Не пришло сообщение о проверке или о сбросе также сообщите нам.

Pdf Generation Libraries Comparison

Lomanu4

Команда форума
Администратор
Регистрация
1 Мар 2015
Сообщения
15,175
Баллы
155
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

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

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>,
}
);

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


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.




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


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

,

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.




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


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.




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

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

  • 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>
);
}

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

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.



In case of need, you can even render

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.



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>
);
}

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.


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.




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:

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

.
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

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


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.



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


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.




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


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.




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();

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.




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)


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

 
Вверх