Gerando PDFs utilizando React e Puppeteer

A criação de arquivos PDF de páginas web customizadas é uma exigência comum, mas nem semper 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 .

인디스


  • O que é o Puppeteer
  • Criando o projeto
  • Criando o script do Puppeteer
  • Gerando o PDF
  • Pontos importantes e dicas
  • Código

  • 1. 꼭두각시 인형을 조종해야 하나요?

    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. 크리안도 오 프로젝트

    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 eo 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. 게란도 또는 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 prop waiUntil 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.
  • 30초 동안 PDF를 종료할 때 제한 시간 초과가 있으므로 setDefaultNavigationTimeout을 사용하여 다른 값을 변경해야 하는 경우가 있습니다. Para mais informações verifique a documentação oficial .

  •     await page.setDefaultNavigationTimeout(0);
    

    6. 코디고

    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?

    길헤르메데카스트롤라이트 / PDF 인형극


    Puppeteer를 사용하여 PDF를 생성하기 위한 가이드의 샘플 컴패니언 리포지토리

    좋은 웹페이지 즐겨찾기