Como criar PDFs incríves usando Ruby + Prawn + Gruff

ruby on rails pdf
Introdução 🙂

Criar PDFs para exportar dados, gerar boletos e etc é uma tarefa comum em vários sistemas e é claro que o Ruby On Rails possui várias maneiras de resolver este problema. A minha preferida é utilizando a gem Prawn porque ela te permite realizar customizações complexas de uma maneira bem simples e intuitiva.

O que vamos aprender?

Nesse tutorial eu vou abordar a criação de dois tipos de documentos em PDF, o primeiro é um contrato gerado através de alguns dados básicos como nome do cliente, descrição, valor e etc e o segundo documento vai ser um PDF com dois gráficos diferentes baseados nos dados de despesas de uma empresa.
Nós vamos aprender como usar a sintaxe básica do prawn e também como gerar gráficos incríveis usando o gruff.

INGREDIENTES
Objetivos

Criar um projeto que permita exportar os dados de duas tabelas de uma empresa em PDF.
A primeira gerando um contrato (em PDF) e a segunda um gráfico sobre as contas de uma empresa (em PDF).

Passo a Passo
  1. Criar a estrutura do Projeto
  2. Incluir os links e métodos de export
  3. Criar nossos métodos para gerar os PDFs
Mãos à Obra \o/
Parte 1 – Criando o Projeto

Primeiro vamos criar nosso projeto Rails, nossas tabelas e também dois scaffolds básicos para podermos gerenciar os dados que serão usados para gerar os PDFs.

1 – Para começar, rode no seu console o comando para gerar o projeto:

1
rails new generate_pdf

2 – Inclua no seu Gemfile o prawn e o gruff:

1
2
3
4
# Gem para gerar os pdfs
gem 'prawn-rails'
# Gem para gerar os gráficos
gem 'gruff'

3 – O gruff tem como dependências os seguintes softwares, para instalar no ubuntu rode:

1
sudo apt-get install imagemagick libmagickcore-dev libmagickwand-dev

*Caso você esteja em outro sistema, acesse este link para instalar.

4 – Vamos instalar nossas Gems, rode:

1
bundle install

5 – Vamos criar nossos scaffolds para poder gerenciar os dados das tabelas que usaremos pra gerar os PDFs, rode este comando para o primeiro scaffold:

1
rails g scaffold agreement client_name:string description:text price:decimal

E agora este para o segundo, rode:

1
rails g scaffold spending value:decimal section:integer

6 – Vamos rodar nossas migrations (como estamos usando o sqlite3 não precisamos rodar o rake db:create antes)

1
rake db:migrate

7 – Vamos rodar o servidor e criar um novo agreement, siga os passos:

  • Rode o servidor
1
rails s

prawn gruff

  • Clique em “New Agreement” e preencha o formulário.
  • Agora acessando http://localhost:3000/agreements seu resultado deve ser semelhante ao da imagem a baixo:
    captura-de-tela-de-2016-11-11-17-44-00

Pronto \o/, nosso projeto foi criado, agora vamos preparar ele para exportar nossos dados.

Parte 2 – Preparando os Exports

Nesta fase nós vamos incluir no nosso projeto os botões de export e também vamos preparar nossos controllers para chamarem nosso módulo gerador de PDF quando ele estiver pronto.

1 – Vamos acrescentar as rotas para exportarmos nossas tabelas. Vá até routes.rb e substitua o conteúdo por:

1
2
3
4
5
6
7
8
9
10
11
  Rails.application.routes.draw do
  resources :spendings
  # /spendings_export
  get '/spending_export' => 'spendings#export'
  resources :agreements do
    member do
      # /agreements/:id/export
      get 'export'
    end
  end
end

2 – Vá até o arquivo “app/views/agreements/index.html.erb”, e substitua o conteúdo por:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<p id="notice"><%= notice %></p>
 
<h1>Agreements</h1>
 
<table>
  <thead>
    <tr>
      <th>Client name</th>
      <th>Description</th>
      <th>Price</th>
      <th colspan="3"></th>
    </tr>
  </thead>
 
  <tbody>
    <% @agreements.each do |agreement| %>
      <tr>
        <td><%= agreement.client_name %></td>
        <td><%= agreement.description %></td>
        <td><%= agreement.price %></td>
        <td><%= link_to 'Show', agreement %></td>
        <td><%= link_to 'Edit', edit_agreement_path(agreement) %></td>
        <td><%= link_to 'Destroy', agreement, method: :delete, data: { confirm: 'Are you sure?' } %></td>
        <td><%= link_to 'Export', export_agreement_path(agreement), target: "_blank" %></td>
      </tr>
    <% end %>
  </tbody>
</table>
 
<br>
 
<%= link_to 'New Agreement', new_agreement_path %>

*Note que incluímos o link para dar o export de cada agreement.

3 – Agora vá até o arquivo “app/views/spendings/index.html.erb”, e substitua o conteúdo por:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<p id="notice"><%= notice %></p>
 
<h1>Spendings</h1>
 
<table>
  <thead>
    <tr>
      <th>Value</th>
      <th>Section</th>
      <th colspan="3"></th>
    </tr>
  </thead>
 
  <tbody>
    <% @spendings.each do |spending| %>
      <tr>
        <td><%= spending.value %></td>
        <td><%= spending.section %></td>
        <td><%= link_to 'Show', spending %></td>
        <td><%= link_to 'Edit', edit_spending_path(spending) %></td>
        <td><%= link_to 'Destroy', spending, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>
 
<br>
 
<%= link_to 'New Spending', new_spending_path %>
<%= link_to 'Export To Graph', spending_export_path, target: "_blank"%>

*Note que incluímos o link para dar o export das spendings.

4 – Agora vá até o controller “app/controllers/agreement_controller.rb” e substitua o conteúdo dele por:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class AgreementsController < ApplicationController
  # No Before Action nós adicionamos também o export para que ele consiga pegar o agreement correto
  before_action :set_agreement, only: [:show, :edit, :update, :destroy, :export]
  # Nós incluimos aqui a lib que vamos criar chamada generate_pdf.rb
  require './lib/generate_pdf'
 
  # GET /agreements
  # GET /agreements.json
  def index
    @agreements = Agreement.all
  end
 
  # GET /agreements/1
  # GET /agreements/1.json
  def show
  end
 
  # GET /agreements/new
  def new
    @agreement = Agreement.new
  end
 
  # GET /agreements/1/edit
  def edit
  end
 
  # POST /agreements
  # POST /agreements.json
  def create
    @agreement = Agreement.new(agreement_params)
 
    respond_to do |format|
      if @agreement.save
        format.html { redirect_to @agreement, notice: 'Agreement was successfully created.' }
        format.json { render :show, status: :created, location: @agreement }
      else
        format.html { render :new }
        format.json { render json: @agreement.errors, status: :unprocessable_entity }
      end
    end
  end
 
  # PATCH/PUT /agreements/1
  # PATCH/PUT /agreements/1.json
  def update
    respond_to do |format|
      if @agreement.update(agreement_params)
        format.html { redirect_to @agreement, notice: 'Agreement was successfully updated.' }
        format.json { render :show, status: :ok, location: @agreement }
      else
        format.html { render :edit }
        format.json { render json: @agreement.errors, status: :unprocessable_entity }
      end
    end
  end
 
  # DELETE /agreements/1
  # DELETE /agreements/1.json
  def destroy
    @agreement.destroy
    respond_to do |format|
      format.html { redirect_to agreements_url, notice: 'Agreement was successfully destroyed.' }
      format.json { head :no_content }
    end
  end
 
  # Criamos este método que vai chamar nossa lib para gerar o PDF e depois redirecionar o user para o arquivo PDF
  def export
    GeneratePdf::agreement(@agreement.client_name, @agreement.description, @agreement.price)
    redirect_to '/agreement.pdf'
  end
 
  private
    def set_agreement
      @agreement = Agreement.find(params['id'])
    end
 
    def agreement_params
      params.require(:agreement).permit(:client_name, :description, :price)
    end
end

* Se você chamar o controller no browser vai quebrar porque ainda não criamos nossa módulo de export, vamos fazer isso na próxima parte.

* Note que o que incluímos foi o método export, o carregamento da lib que vamos criar e o método export no set_agreement para carregar corretamente o record. (Veja com calma os comentários no código)

5 – Agora vá até o controller “app/controllers/spending_controller.rb” e substitua o conteúdo dele por:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
class SpendingsController < ApplicationController
  before_action :set_spending, only: [:show, :edit, :update, :destroy]
  # Incluimos a Lib que vamos criar para podermos chama-la no nosso método
  require './lib/generate_pdf'
 
  # GET /spendings
  # GET /spendings.json
  def index
    @spendings = Spending.all
  end
 
  # GET /spendings/1
  # GET /spendings/1.json
  def show
  end
 
  # GET /spendings/new
  def new
    @spending = Spending.new
  end
 
  # GET /spendings/1/edit
  def edit
  end
 
  # POST /spendings
  # POST /spendings.json
  def create
    @spending = Spending.new(spending_params)
 
    respond_to do |format|
      if @spending.save
        format.html { redirect_to @spending, notice: 'Spending was successfully created.' }
        format.json { render :show, status: :created, location: @spending }
      else
        format.html { render :new }
        format.json { render json: @spending.errors, status: :unprocessable_entity }
      end
    end
  end
 
  # PATCH/PUT /spendings/1
  # PATCH/PUT /spendings/1.json
  def update
    respond_to do |format|
      if @spending.update(spending_params)
        format.html { redirect_to @spending, notice: 'Spending was successfully updated.' }
        format.json { render :show, status: :ok, location: @spending }
      else
        format.html { render :edit }
        format.json { render json: @spending.errors, status: :unprocessable_entity }
      end
    end
  end
 
  # DELETE /spendings/1
  # DELETE /spendings/1.json
  def destroy
    @spending.destroy
    respond_to do |format|
      format.html { redirect_to spendings_url, notice: 'Spending was successfully destroyed.' }
      format.json { head :no_content }
    end
  end
 
  # Criamos o método export para chamar a lib que gera o PDF e depois redirecionar o usuário para baixo o PDF
  def export
    GeneratePdf::spending(Spending.all.map {|s| [s.section, s.value.to_f]})
    redirect_to '/spending.pdf'
  end
 
  private
    # Use callbacks to share common setup or constraints between actions.
    def set_spending
      @spending = Spending.find(params['id'])
    end
 
    # Never trust parameters from the scary internet, only allow the white list through.
    def spending_params
      params.require(:spending).permit(:value, :section)
    end
end

* Se você chamar o controller no browser vai quebrar porque ainda não criamos nossa módulo de export, vamos fazer isso na próxima parte.

* Note que o que incluímos foi o método export e o carregamento da lib que vamos criar. (Veja com calma os comentários no código)

Pronto Agora vamos para a parte divertida \o/

Parte 3 – Criando os métodos de Export

Nesta parte nós vamos criar dois métodos dentro de um módulo para gerar nossos PDFs, para fazer isso vamos criar um arquivo dentro do diretório lib e incluir nossas dependências (prawn para o PDF e gruff para gerar as imagens dos gráficos).
Eu comentei linha a linha do código para você entender o funcionamento do Prawn e Guff então, acompanhe com calma o código 🙂

  1. Crie um arquivo chamado “generate_pdf.rb” no diretório “lib” da sua aplicação.
    1
    
    touch lib/generate_pdf.rb
  2. Agora copie dentro desse arquivo o seguinte código:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    
    require 'prawn'
    require 'gruff'
     
    module GeneratePdf
      PDF_OPTIONS = {
        # Escolhe o Page size como uma folha A4
        :page_size   => "A4",
        # Define o formato do layout como portrait (poderia ser landscape)
        :page_layout => :portrait,
        # Define a margem do documento
        :margin      => [40, 75]
      }
     
      def self.agreement name, details, price
        # Apenas uma string aleatório para termos um corpo de texto pro contrato
        lorem_ipsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec elementum nulla id dignissim iaculis. Vestibulum a egestas elit, vitae feugiat velit. Vestibulum consectetur non neque sit amet tristique. Maecenas sollicitudin enim elit, in auctor ligula facilisis sit amet. Fusce imperdiet risus sed bibendum hendrerit. Sed vitae ante sit amet sapien aliquam consequat. Duis sed magna dignissim, lobortis tortor nec, suscipit velit. Nulla sit amet fringilla nisl. Integer tempor mauris vitae augue lobortis posuere. Ut quis tellus purus. Nullam dolor mauris, egestas varius ligula non, cursus faucibus orci sectetur non neque sit amet tristique. Maecenas sollicitudin enim elit, in auctor ligula facilisis sit amet. Fusce imperdiet risus sed bibendum hendrerit. Sed vitae ante sit amet sapien aliquam consequat."
     
        Prawn::Document.new(PDF_OPTIONS) do |pdf|
          # Define a cor do traçado
          pdf.fill_color "666666"
          # Cria um texto com tamanho 30 PDF Points, bold alinha no centro
          pdf.text "Agreement", :size => 32, :style => :bold, :align => :center
          # Move 80 PDF points para baixo o cursor
          pdf.move_down 80
          # Escreve o texto do contrato com o tamanho de 14 PDF points, com o alinhamento justify
          pdf.text "#{lorem_ipsum}", :size => 12, :align => :justify, :inline_format => true
          # Move mais 30 PDF points para baixo o cursor
          pdf.move_down 30
          # Escreve o texto com os detalhes que o usuário entrou
          pdf.text "#{details}", :size => 12, :align => :justify, :inline_format => true
          # Move mais 30 PDF points para baixo o cursor
          pdf.move_down 10
          # Adiciona o nome com 12 PDF points, justify e com o formato inline (Observe que o <b></b> funciona para deixar em negrito)
          pdf.text "Com o cliente: <b>#{name}</b> por R$#{price}", :size => 12, :align => :justify, :inline_format => true
          # Muda de font para Helvetica
          pdf.font "Helvetica"
          # Inclui um texto com um link clicável (usando a tag link) no bottom da folha do lado esquerdo e coloca uma cor especifica nessa parte (usando a tag color)
          pdf.text "Link Para o Manul do Prawn<link href='http://prawnpdf.org/manual.pdf'> <color rgb='5ca3e6'>clicável</color></link>", :size => 10, :inline_format => true, :valign => :bottom, :align => :left
          # Inclui em baixo da folha do lado direito a data e o némero da página usando a tag page
          pdf.number_pages "Gerado: #{(Time.now).strftime("%d/%m/%y as %H:%M")} - Página <page>", :start_count_at => 0, :page_filter => :all, :at => [pdf.bounds.right - 140, 7], :align => :right, :size => 8
          # Gera no nosso PDF e coloca na pasta public com o nome agreement.pdf
          pdf.render_file('public/agreement.pdf')
        end
      end
     
      def self.spending spendings
        ## Gráfico 1 ##
     
        # Formata os dados para gerar o gráfico (Não se preocupe com isso, apenas saiba que nesse gráfico os dados de label precisa entrar como um hash)
        spending_labels = {}
        spendings.each_with_index {|s,i| spending_labels[i] = s[0].to_s}
     
        # Cria um objeto Gruff (gerador de gráfico)
        g = Gruff::AccumulatorBar.new 1000
        # Esconde a legenda
        g.hide_legend = true
        # Escolhe o tamanho da Font
        g.marker_font_size = 16
        # Escolhe as cores que serão usadas
        g.theme = {
         :colors => ['#aedaa9', '#12a702'],
         :marker_color => '#dddddd',
         :font_color => 'black',
         :background_colors => 'white'
        }
        # Aqui nós colocamos os dados y da tabela
        g.data 'Savings', spendings.map {|s| s[1]}
        # Aqui colocamos os dados que formatamos antes da coluna x
        g.labels = spending_labels
        # Gera a imagem no diretório público (você pode escolher onde gerar)
        g.write('public/graph.jpg')
     
     
        ## Gráfico 2 ##
        # Estamos colocando nossos dados direto em @datasets para preencher o gráfico 2
        @datasets = spendings
        # Cria o objeto Gruff
        g = Gruff::Pie.new 900
        g.theme = Gruff::Themes::PASTEL
     
        # Aqui ele formata nossos dados
        @datasets.each do |data|
          g.data(data[0], data[1])
        end
     
        # Aqui ele gera a imagem do gráfico
        g.write("public/graph_pie.jpg")
     
     
        # Inicia nosso PDF
        Prawn::Document.new(PDF_OPTIONS) do |pdf|
          # Define a cor do traçado
          pdf.fill_color "666666"
          # Cria um título com tamanho 28 PDF Points, bold alinha no centro
          pdf.text "Gráficos de gastos", :size => 28, :style => :bold, :align => :center
          # Move 60 PDF points para baixo o cursor
          pdf.move_down 60
          # Escreve mais um texto sobre o gráfico
          pdf.text "Gráfico em R$ de gastos por setor", size: 14, color: 'AAAAAA', align: :center
          # Inclui a imagem com o gráfico na escala 0.50 para diminuir pela metade a imagem
          pdf.image "public/graph.jpg", :scale => 0.50
          # Inclui em baixo da folha do lado direito a data e o numero da página usando a tag page
          pdf.number_pages "Gerado: #{(Time.now).strftime("%d/%m/%y as %H:%M")} - Página <page>", :start_count_at => 0, :page_filter => :all, :at => [pdf.bounds.right - 140, 7], :align => :right, :size => 8
          # Muda de página para incluir o outro gráfico
          pdf.start_new_page
          # Move 60 PDF points para baixo o cursor
          pdf.move_down 20
          # Escreve mais um texto sobre o gráfico
          pdf.text "Gráfico em % de gastos por setor", size: 14, color: 'AAAAAA', align: :center
          # Incluir o gráfico numero 2
          pdf.image "public/graph_pie.jpg", :scale => 0.50
          # Inclui em baixo da folha do lado direito a data e o numero da página usando a tag page
          pdf.number_pages "Gerado: #{(Time.now).strftime("%d/%m/%y as %H:%M")} - Página <page>", :start_count_at => 0, :page_filter => :all, :at => [pdf.bounds.right - 140, 7], :align => :right, :size => 8
          # Gera nosso pdf no diretório public
          pdf.render_file('public/spending.pdf')
        end
      end
     
    end

    Observe que temos 2 métodos dentro do módulo, o primeiro método (agreement) vai gerar nosso contrato usando os parâmetros que criarmos no scaffold de agreement usando o prawn e vai colocar o pdf criado em “public/agreement.pdf” o segundo (spending) cria o nosso PDF com dois gráficos baseados nos dados que entrarmos no scaffold de spendings usando o prawn para gerar o PDF e o gruff para gerar os gráficos (imagens) que serão incluídos dentro do PDF em “public/spending.pdf”. (Leia com calma os comentários do código acima)

  3. Agora finalmente vamos testar nossa pequena aplicação, primeiro rode o seu servidor:
    1
    
    rails s
  4. No browser visite http://localhost:3000/agreements
  5. Agora ao lado de um Agreement já criado via form você dever ver um link escrito “export” como na imagem a baixo:
    captura-de-tela-de-2016-11-11-21-02-29
  6. Clique nele, o PDF vai abrir em uma nova aba e o resultado deve ser semelhante a este:
    captura-de-tela-de-2016-11-11-20-44-50
  7. Agora visite http://localhost:3000/spendings
  8. Crie alguns spendings para gerar dados para o gráfico como na imagem a baixo.
    captura-de-tela-de-2016-11-11-20-48-27
  9. Agora na mesma tela (listagem de spendings) clique no link “Export to Graph”, quando você fizer isso o PDF vai se abrir na aba ao lado e a primeira página deve ser semelhante a esta:
    captura-de-tela-de-2016-11-11-20-45-54E a segunda página semelhante a está:
    captura-de-tela-de-2016-11-11-20-46-12
  10. Parabéns \o/ nós conseguimos criar um pequeno sistema que gera contratos e gráficos baseados nos dados das nossas tabelas.

CONCLUSÃO

Gerar PDF usando o ruby + prawn + gruff é uma tarefa muito simples e traz infinitas possibilidades, para você se aprofundar e aprender a fazer mais coisas com essas ferramentas incríveis acesse o manual do Prawn clicando aqui e o repositório do Gruff clicando aqui.

Como de costume o Código completo da aplicação está no Github, caso você queria clonar o código, clique aqui. Aproveita e me segue lá \o/

Se você ficou com dúvidas ou tem sugestões de posts para o Blog comenta aí em baixo ou me adiciona no Facebook clicando aqui.

Muito Obrigado,
Sua presença aqui é uma honra para mim,
Leonardo Scorza 🙂

Deixe seu Feedback!

Comentários