Gerando PDFs utilizando React e Puppeteer
Guilherme Leite
Posted on December 7, 2020
A criação de arquivos PDF de páginas web customizadas é uma exigência comum, mas nem sempre as soluções padrão dos navegadores são suficientes para se conseguir criar um PDF com paginação e tamanhos corretos.
Nesse artigo passaremos por todas as etapas necessárias para se criar um PDF de uma página estática utilizando React e Puppeteer.
Todo o código que utilizaremos nesse projeto está disponível no Github.
Índice
- O que é o Puppeteer
- Criando o projeto
- Criando o script do Puppeteer
- Gerando o PDF
- Pontos importantes e dicas
- Código
1. O que é o Puppeteer?
O Puppeteer é uma biblioteca do Node que provê uma API de controle do Chrome de maneira headless, ou seja, apenas em memória e sem a necessidade de visualmente existir um browser na tela. Esse tipo de abordagem permite utilizar o navegador em um script como vamos fazer mais à frente. Muitas vezes também é utilizado em testes end-to-end e em scrapers.
Uma das vantagens de se utilizar o Puppeteer para geração dos PDFs é que, como resultado, teremos um PDF real, vetorizado e com alta qualidade de impressão. Algo que não conseguimos com outros métodos que utilizam screenshots para geração dos documentos.
Para mais informações acesse a documentação oficial: https://pptr.dev/
2. Criando o Projeto
Como primeiro passo criaremos nosso projeto React que servirá de base para a criação dos PDFs.
Obs: Caso você já tenha um projeto criado fique à vontade para pular essa etapa, mas lembre-se, as medidas de sua página React usada na impressão devem ser as mesmas do script do Puppeteer que será configurado mais à frente.
Como exemplo, criaremos uma página contendo gráficos e conteúdos de texto. Podemos iniciar nosso projeto de maneira rápida criando um setup inicial com o create-react-app.
npx create-react-app pdf-puppeteer
cd pdf-puppeteer
yarn start
Remova os arquivos logo.svg
, App.css
e App.test.js
. Troque o código em src/App.js
pelo código abaixo. É extremamente importante que a tela que será acessada para criação do PDF tenha medidas iguais as que forem configuradas no script do Puppeteer.
Repare que no código utilizamos as medidas em milímetros de uma página A4. Fique à vontade para alterar o tamanho da maneira que achar necessário, sempre garantindo que as medidas estejam iguais às usadas no Puppeteer e sigam a proporção do tamanho de página escolhido.
import logo from './logo.svg';
import Chart from './components/Chart';
const chartData = [
{
name: 'Item 1',
value: 51.1,
},
{
name: 'Item 2',
value: 28.9,
},
{
name: 'Item 3',
value: 20,
},
{
name: 'Item 4',
value: 70.1,
},
{
name: 'Item 5',
value: 34.7,
},
]
function App() {
return (
<div
style={{
width: '209.55mm',
height: '298.45mm',
padding:'12mm',
backgroundColor: '#FFF',
}}
>
{/** Header */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between'}}>
<div>
<h1>Data Report</h1>
<h2>{new Date().getYear() + 1900}</h2>
</div>
<img src={logo} className="App-logo" alt="logo" style={{ width: '50mm', height: '50mm'}}/>
</div>
{/** Introduction text */}
<h3>Introduction</h3>
<h5 style={{ fontWeight: 'normal' }}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Dolor sit amet consectetur adipiscing elit duis tristique sollicitudin. Commodo viverra maecenas accumsan lacus vel facilisis volutpat est velit. Ut eu sem integer vitae. Bibendum neque egestas congue quisque egestas diam in. Quis lectus nulla at volutpat diam. Cursus euismod quis viverra nibh. Amet consectetur adipiscing elit duis tristique sollicitudin nibh sit amet. Nibh sed pulvinar proin gravida hendrerit lectus a. Purus in massa tempor nec feugiat nisl pretium. Velit dignissim sodales ut eu sem integer vitae justo eget. Augue ut lectus arcu bibendum at varius. Interdum varius sit amet mattis vulputate enim. In hendrerit gravida rutrum quisque non tellus orci. Lectus nulla at volutpat diam ut venenatis. Massa tempor nec feugiat nisl pretium fusce id velit ut. Aliquet sagittis id consectetur purus ut faucibus. Eget mi proin sed libero enim.
</h5>
{/** Chart with title */}
<h3>Chart 01</h3>
<Chart
data={chartData}
barProps={{
isAnimationActive: false,
}}
/>
{/** Info text */}
<h5 style={{ fontWeight: 'normal' }}>
Pulvinar pellentesque habitant morbi tristique senectus et netus. Nunc eget lorem dolor sed viverra ipsum nunc aliquet bibendum. Enim ut tellus elementum sagittis vitae et leo duis ut. Adipiscing vitae proin sagittis nisl. Orci phasellus egestas tellus rutrum tellus pellentesque eu tincidunt tortor. Id nibh tortor id aliquet lectus proin nibh nisl condimentum. Platea dictumst vestibulum rhoncus est pellentesque. Dictum sit amet justo donec enim diam vulputate. Libero volutpat sed cras ornare arcu dui. Magna fermentum iaculis eu non diam.
</h5>
{/** Chart with title */}
<h3>Chart 02</h3>
<Chart
data={chartData}
barProps={{
isAnimationActive: false,
}}
/>
</div>
);
}
export default App;
Será necessário também criar o arquivo src/components/Chart.jsx
contendo o componente de gráfico que utilizaremos no exemplo. Precisamos também instalar a biblioteca Recharts, uma excelente opção para criação de gráficos em SVG para React.
yarn add recharts
import React from 'react';
import PropTypes from 'prop-types';
import {
BarChart,
Bar,
XAxis,
YAxis,
ReferenceLine,
ResponsiveContainer,
} from 'recharts';
const CustomBarLabel = ({ isPercentage, labelProps, ...props }) => {
const {
x, y, value, width, height,
} = props;
const xPosition = width >= 0 ? x + width + 4 : x + width - ((value === -100 || value % 1 !== 0) ? 27 : 20);
const yPosition = y + height / 2 + 6;
return (
<text
dy={-4}
x={xPosition}
y={yPosition}
textAnchor='right'
fontSize={12}
fontWeight='600'
fill='#4D5365'
fontFamily='Helvetica'
{...labelProps}
>
{isPercentage ? `${value.toFixed(1).replace(/\.0$/, '')}%` : value.toFixed(1).replace(/\.0$/, '')}
</text>
);
};
CustomBarLabel.propTypes = {
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
value: PropTypes.number.isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
isPercentage: PropTypes.bool,
labelProps: PropTypes.object,
};
CustomBarLabel.defaultProps = {
isPercentage: false,
labelProps: {},
};
const Chart = ({
data,
range,
width,
height,
barSize,
barProps,
xAxisProps,
yAxisProps,
barChartProps,
labelProps,
isPercentage,
legend,
children,
}) => {
const { min, max, step } = range;
const ticks = (max - min) / step + 2;
const addLines = (start, end, arrayStep = 1) => {
const len = Math.floor((end - start) / arrayStep) + 1;
return Array(len).fill().map((_, idx) => start + (idx * arrayStep));
};
return (
<ResponsiveContainer width={width} height={height}>
<BarChart
data={data}
margin={{
top: 0, right: 0, left: 10, bottom: 0,
}}
layout='vertical'
barSize={barSize}
{...barChartProps}
>
<XAxis
type='number'
tickCount={ticks}
orientation='top'
domain={[min, max]}
axisLine={false}
tickLine={false}
tick={{
fill: '#6F798B',
fontSize: 14,
fontFamily: 'Helvetica',
}}
{...xAxisProps}
/>
<YAxis
dx={-16}
type='category'
dataKey='name'
axisLine={false}
tickLine={false}
tick={{
fill: '#4D5365',
fontSize: 16,
lineHeight: 22,
fontFamily: 'Helvetica',
}}
interval={0}
{...yAxisProps}
/>
{addLines(min, max, step).map((item) => (
<ReferenceLine key={item} x={item} style={{ fill: '#CDD2DB' }} />
))}
<Bar
dataKey='value'
fill='#A35ADA'
label={(props) => <CustomBarLabel isPercentage={isPercentage} labelProps={labelProps} {...props} />}
{...barProps}
/>
{children}
</BarChart>
</ResponsiveContainer>
);
};
Chart.propTypes = {
data: PropTypes.array,
range: PropTypes.shape({
min: PropTypes.number,
max: PropTypes.number,
step: PropTypes.number,
}),
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
barSize: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
barProps: PropTypes.object,
xAxisProps: PropTypes.object,
yAxisProps: PropTypes.object,
barChartProps: PropTypes.object,
labelProps: PropTypes.object,
isPercentage: PropTypes.bool,
legend: PropTypes.bool,
children: PropTypes.any,
};
Chart.defaultProps = {
data: [{ name: null, value: null }],
range: {
min: 0,
max: 100,
step: 20,
},
width: '100%',
height: 254,
barSize: 22,
barProps: {},
xAxisProps: {},
yAxisProps: {},
barChartProps: {},
labelProps: {},
isPercentage: false,
legend: false,
children: null,
};
export default Chart;
Com nossa página inicial e componentes criados podemos visualizar no browser como ficará nosso arquivo em http://localhost/3000
3. Criando o script do Puppeteer
Para utilizar o Puppeteer precisaremos instalar três pacotes:
- Puppeteer: pacote com o Chrome headless que será utilizado para geração dos PDFs
- Babel-Core: Usado para converter versões recentes do Javascript para ambientes e navegadores mais antigos.
- Babel-Node: CLI que funciona da mesma forma que o Node.js, com a vantagem de compilar códigos ES6 usando o Babel.
Rode o comando no terminal dentro da pasta do projeto para instalar os pacotes necessários:
yarn add -D @babel/core @babel/node puppeteer
Com os pacotes adicionados podemos criar nosso script no arquivo src/generate.js
com o código abaixo.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://localhost:3000');
await page.pdf({
path: 'src/assets/puppeteer-test.pdf',
printBackground: true,
width: '209.55mm',
height: '298.45mm',
});
await browser.close();
})();
O script executa as seguintes etapas:
- Cria uma instância do Puppeteer
- Abre uma nova “página”
- Navega até a página escolhida. Nesse caso nossa página de exemplo: http://localhost:3000
- Define o caminho e o nome do arquivo que será gerado, além das medidas usadas para construção da página. A opção
printBackground
é importante para que as cores originais da página sejam preservadas - Espera a geração ser finalizada
4. Gerando o PDF
Agora que já temos nosso código funcionando e nosso script configurado conseguimos terminar nossas alterações para que o PDF possa ser gerado.
Como primeira etapa precisamos adicionar um novo parâmetro chamado generate
nos scripts do arquivo package.json
como no código abaixo.
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"generate": "npx babel-node src/generate.js"
},
Essa linha é necessária para que possamos usar o babel-node instalado para transpilar nosso código Javascript e executá-lo no Node.
Para gerar o PDF basta rodar o comando abaixo enquanto o servidor React estiver rodando:
yarn generate
O resultado da execução do script é a criação do arquivo PDF com o nome definido no script dentro da pasta assets. Perceba que o arquivo tem as mesmas características da página original e pode ser ampliado sem perder a qualidade, uma das vantagens de se utilizar esse método.
Parabéns! Agora você tem um PDF que representa perfeitamente sua página 😄
5. Pontos importantes e dicas:
- Como já dito diversas vezes é essencial que sua página tenha as mesmas dimensões que as definidas no script do Puppeteer. Isso vai garantir que o conteúdo seja representado de maneira fiel e com paginação correta.
- Cada parte do código com as medidas definidas serão uma página no PDF. Uma dica para múltiplas páginas é criar um componente base de página com todas as características necessárias e envolver seus componentes.
- Para mudar a proporção de reatrato para paisagem basta alterar as dimensões de largura e altura entre si.
- Pode ser necessário customizar o script do Puppeteer com itens adicionais de acordo com sua página web. Em casos de páginas com chamadas api a função
page.goto
pode necessitar da propwaiUntil
como no código abaixo. Para mais informações verifique a documentação oficial.
await page.goto('http://localhost:3000/report-cba-full/12', { waitUntil: 'networkidle0' });
- Desabilite animações e transições na geração do PDF para que a página não seja gerada de maneira incompleta.
- Existe um timeout padrão de 30s para finalização do PDF, caso seja necessário há uma opção de se alterar esse valor com a opção setDefaultNavigationTimeout. Para mais informações verifique a documentação oficial.
await page.setDefaultNavigationTimeout(0);
6. Código
O código utilizado nesse projeto está disponível no Github no repositório abaixo. Fique à vontade para experimentar variações e configurações. Porque não adicionar uma nova página ao seu PDF?
guilhermedecastroleite / pdf-puppeteer
Sample companion repository of a guide for generating PDFs using Puppeteer
Posted on December 7, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.