Download file pdf in Rails with gem pdfkit

1. Giới thiệu

Download file pdf là 1 tính năng được nhiều người ưa chuộng, nó thuận tiện để in các biên lai, hay các thông tin về order,.. Hôm nay mình sẽ giới thiệu cách export ra file pdf bằng gem PDFKit trong rails. Đây là link github của của gem PDFKit

2. PDFKit

Add gem PDFKit in rails app

gem "pdfkit"

Và sử dụng gem wkhtmltopdf tren back-end để renders html using Webkit.

gem "wkhtmltopdf-binary"

Sau đó chạy lệnh bundle để cài đặt gem

Middleware Setup Rails apps

module DemoPdfkit
  class Application < Rails::Application
    config.middleware.use PDFKit::Middleware
    .....
      end
end

Usage

# PDFKit.new takes the HTML and any options for wkhtmltopdf
# run `wkhtmltopdf --extended-help` for a full list of options
kit = PDFKit.new(html, :page_size => 'Letter')
kit.stylesheets << '/path/to/css/file'

# Get an inline PDF
pdf = kit.to_pdf

# Save the PDF to a file
file = kit.to_file('/path/to/save/pdf')

# PDFKit.new can optionally accept a URL or a File.
# Stylesheets can not be added when source is provided as a URL of File.
kit = PDFKit.new('http://google.com')
kit = PDFKit.new(File.new('/path/to/html'))

# Add any kind of option through meta tags
PDFKit.new('<html><head><meta name="pdfkit-page_size" content="Letter"')
PDFKit.new('<html><head><meta name="pdfkit-cookie cookie_name1" content="cookie_value1"')
PDFKit.new('<html><head><meta name="pdfkit-cookie cookie_name2" content="cookie_value2"')

Bạn có thể xem thêm trong doc cùa gem. Đến đây ta bắt đầu có thể sử dụng gem pdfkit. Tạo Model, Data, HTML

rails generate model user name
rails generate model payment order_id amount:float user:references

rails db:migrate

User sẽ có nhiều payment

class User < ApplicationRecord
  has_many :payments
end
class Payment < ApplicationRecord
  belongs_to :user
end

Tạo data seed để demo db/seeds.rb

User.create!([{name: "name1"},{name: "names"}])
10.times do |n| 
  Payment.create!(
  { order_id: "#{Time.zone.now.strftime('%y%m%d')}-test#{n}", amount: rand(1..100), user_id: rand(1..2) })
end

Tạo User Controller

  rails generate controller Users index show

app/controllers/users_controller.rb

class UsersController < ApplicationController
  def index
    @users = User.all
  end

  def show
    @user = User.find_by id: params[:id]
  end
end

Cài đặt routes trong file config/routes

Rails.application.routes.draw do
  root "users#index"
  resources :users, only: [:index, :show] 
end

Bây giờ đến phần xử lý download file pdf payment của user Tách biết phần xử lý pdf này ra khỏi controller, mình tạo 1 services là generate_user_payment_as_pdf.rb

class GenerateUserPaymentAsPDF
  def initialize user
    @user = user
  end

  def to_pdf
    kit = PDFKit.new(as_html, page_size: 'A4')
    kit.to_file("#{Rails.root}/public/invoice.pdf")
  end

  def filename
    "User #{user.id}.pdf"
  end

  private
  attr_reader :user
  def as_html
    render template: "downloads/show", layout: "invoice_pdf", locals: { user: user }
  end
end

Phương thức as_html dùng để tạo ra file HTML Phương thức to_pdf sử dụng PDFKit để lưu file PDF trong thư mục public của rails

** Xây dựng phần layout của file PDF**

app/views/layouts/invoice_pdf.erb

<!DOCTYPE html>
<html>
<head>
  <title>Download</title>
  <style>
    <%= Rails.application.assets.find_asset('application.scss').to_s %>
  </style>
</head>
<body>
  <%= yield %>
</body>
</html>

app/views/users/_template.html.erb

<div class="invoice">
  <h1>User Payments</h1>
  <h3>User Name: <%= @user.name %></h3>
  <h3>Date: <%= DateTime.now.to_time %></h3>
  <div class="receipt__details">
    <h2 class="payment-title">Danh sach Payments</h2>
    <table class="base-table base-table--standard">
      <thead>
        <tr>
            <th class="base-table__col-name">STT</th>
            <th class="base-table__col-name">OrderID</th>
            <th class="base-table__col-name">Amount</th>
        </tr>
      </thead>
      <tbody>
        <% total = 0 %>
        <% @user.payments.each_with_index do |payment, index| %>
          <tr>
            <td class="base-table__content">
                <p><%= index + 1 %></p>
            </td>
            <td class="base-table__content">
                <p><%= payment.order_id %></p>
            </td>
            <td class="base-table__content">
                <p class="base-table__text--right"><%= number_to_currency payment.amount %></p>
            </td>
            <% total += payment.amount %>
          </tr>
        <% end %>
        <tr class="total">
          <td style="text-align: right">Total: </td>
          <td><%= number_to_currency total %></span></td>
        </tr>
      </tbody>
    </table>
  </div>

app/views/users/index.html.erb

<h2>Danh sach users </h2>
<ul>
  <% @users.each do |user| %>
  <li>
    <%= link_to "#{user.id} - #{user.name} >>", user_path(user) %>
  </li>
  <% end %>
</ul>

app/views/users/show.html.erb

<%= render "template", user: @user %>
<%= link_to "Download", user_downloads_path(@user, format: "pdf"), target: :_blank %>

app/views/downloads/user_payment.html.erb

<%= render "users/template", user: @user %>

app/assets/stylesheets/users.scss

.invoice {
  width: 700px;
  max-width: 700px;
  border: 1px solid black;
  margin: 50px;
  padding: 50px;

  h1 {
    text-align: center;
    margin-bottom: 100px;
  }
  .notes {
    margin-top: 100px;
  }
  
  .total td {
    padding-top: 25px;
    padding-left: 100px;
  }
  p {
    margin: 0;
   }
    
  .payment-title {
    margin-bottom: 10px;
    text-align: center;
    font-size: 2.2rem;
    padding: 5px 15px;
    font-weight: 400;
  }
  
  @media print {
    body * {
      visibility: hidden;
    }
  
    #receipt * {
      visibility: visible;
    }
  }
  .base-table {
    border-collapse: collapse;
    border-spacing: 0;
    width: 100%;
    background-color: #fff;
    border: 2px solid #dedede;
  }
  .base-table__col-name {
    padding: 15px;
    font-weight: 400;
    font-size: 1.6rem;
    text-align: left;
    border: 2px solid #dedede;
  }

  .base-table__content {
    padding: 10px;
    border: 2px solid #dedede;
    vertical-align: middle;
  }

  .base-table__text--right {
    text-align: right;
  }
 
  .base-table--standard .base-table__col-name {
    padding: 8px;
    font-size: 1.4rem;
    border: 1px solid #000;
    background: #eee;
  }
}

Tiếp theo, mình xây dựng downloads_controller.rb để có thể xuất ra file PDF

app/controllers/downloads_controller.rb

class DownloadsController < ApplicationController
  before_action :load_user, only: [:index, :show]

  def show
    respond_to do |format|
      format.pdf { send_user_pdf }
      format.html { render_sample_html } if Rails.env.development?
    end
  end

  private

  def load_user
    @user = User.find_by id: params[:user_id]
  end

  def create_user_pdf
    GenerateUserPaymentAsPDF.new user
  end

  def send_user_pdf
    send_file create_user_pdf.to_pdf,
      filename: user_pdf.filename,
      type: "application/pdf",
      disposition: "inline"
  end

  def render_sample_html
    render template: "downloads/user_payment", layout: "invoice_pdf",
      locals: { uesr: @user }
  end
end

Update lại app/config/routes.rb

Rails.application.routes.draw do
  root "users#index"
  resources :users, only: [:index, :show] do
    resource :downloads, only: :show
  end
end

Bây giờ chạy rails s để kiểm tra kết quả: Kết quả show 1 user http://localhost:3000/users/1

kết quả download pdf