在rails中从html生成pdf

3431 words, 10 mins

因为工作需要,要生成pdf文档,这个需求还挺有意思的,要注意的地方比如pdf分页的处理,还有不要用复杂的flex布局.

目前的解决方案有:

  • PDFKit
  • Wicked PDF(本文使用这个,文档够用)

这两个都是基于一个跨平台的免费开源工具wkhtmltopdf.这个工具能直接从html页面生成pdf.

安装wkhtmltopdf

下载对应平台的包

安装好后,可以直接在命令行测试一下

 ✗ wkhtmltopdf https://gnuser.github.io/ gnuser.pdf
Loading pages (1/6)
Counting pages (2/6)
Resolving links (4/6)
Loading headers and footers (5/6)
Printing pages (6/6)
Done

这条指令会在当前目录根据博客首页生成gnuser.pdf文件.

image-20200309222930139

还带目录,感觉很棒吧!

新建一个rails项目

rails new rails-generate-pdf

安装wicked_pdf

添加gem到Gemfile

gem 'wicked_pdf'
gem 'wkhtmltopdf-binary' # 如果上面一步已经安装过,这里可以注释掉.

执行

# command line
bundle install

生成初始化文件

# command line
✗ rails g wicked_pdf
Running via Spring preloader in process 72924
      create  config/initializers/wicked_pdf.rb

创建data models

  • 生成Invoice模型
rails generate model Invoice from_full_name from_address from_email from_phone to_full_name to_address to_email to_phone status discount:decimal vat:decimal

修改app/models/invoice.rb文件

# file: rails-generate-pdf/app/models/invoice.rb
class Invoice < ApplicationRecord
    has_many :invoice_items, dependent: :destroy

    STATUS_CLASS = {
        :draft => "badge badge-secondary",
        :sent => "badge badge-primary",
        :paid => "badge badge-success"
    }

    def subtotal
        self.invoice_items.map { |item| item.qty * item.price }.sum
    end

    def discount_calculated
        subtotal * (self.discount / 100.0)
    end

    def vat_calculated
        (subtotal - discount_calculated) * (self.vat / 100.0)
    end

    def total
        subtotal - discount_calculated + vat_calculated
    end

    def status_class
        STATUS_CLASS[self.status.to_sym]
    end

end
  • 生成InvoiceItem模型
rails generate model InvoiceItem name description price:decimal qty:integer invoice:references

创建数据库

rails db:create
ralis db:migrate

生成测试数据

修改db/seeds.rb,太长就不贴了.

rails db:seed

创建controller

rails generate controller Invoices index show

修改app/controllers/invoices_controller.rb

# file: rails-generate-pdf/app/controllers/invoices_controller.rb
class InvoicesController < ApplicationController
    def index
        @invoices = scope
    end

    def show
        @invoice = scope.find(params[:id])

        respond_to do |format|
            format.html
            format.pdf do
                render pdf: "Invoice No. #{@invoice.id}",
                page_size: 'A4',
                template: "invoices/show.html.erb",
                layout: "pdf.html",
                orientation: "Landscape",
                lowquality: true,
                zoom: 1,
                dpi: 75
            end
        end
    end

    private
        def scope
            ::Invoice.all.includes(:invoice_items)
        end
end

这里我们在show方法实现了两种渲染方式(html和pdf), 只要访问.pdf后缀,我们就可以渲染pdf了.

可以试试这两个链接:

  • http://rails-generate-pdf.herokuapp.com/invoices/1
  • http://rails-generate-pdf.herokuapp.com/invoices/1.pdf

配置参数

  • layout布局pdf.html: app/views/layouts/pdf.html.erb
  • template文件invoices/show.html.erb
  • page-size: A4纸
  • orientation: Landscape或者Portrait(默认)
  • lowquality: 降低pdf质量,压缩文件大小
  • dpi: 修改dpi系数
  • zoom: 缩放比例

添加路由

修改app/config/routes.rb

# file: rails-generate-pdf/app/config/routes.rb
Rails.application.routes.draw do
    root to: 'invoices#index'

    resources :invoices, only: [:index, :show]
end

修改layout文件

# file: rails-generate-pdf/views/layous/pdf.html.erb
<!DOCTYPE html>
<html>
<head>
<title>PDFs - Ruby on Rails</title>
    <%= wicked_pdf_stylesheet_link_tag "invoice" %>
</head>
<body>
    <%= yield %>
</body>
</html>

这里的helper函数wicked_pdf_stylesheet_link_tag会添加css文件 app/assets/stylesheets/invoice.scss

修改show.html.erb

参照这里

添加css文件

修改config/initializers/asset.rb

# file: config/initializers/assets.rb
Rails.application.config.assets.precompile += %w( invoice.scss )

你需要的css和js文件都需要在这里添加

添加下载pdf按钮

<%= link_to 'DOWNLOAD PDF', invoice_path(@invoice, format: :pdf) %>

如果没有问题,就可以看到生成的invoice的pdf文件了

image-20200309235314882

添加footer信息

比较麻烦的地方是footer信息也能使用html来定制样式,根据数据动态生成内容,并且能打印页数.

比较全的配置文件参考这里

推荐使用html方式

这样你就可以更加灵活的控制文字样式和位置了,包括参数传递和页码打印

参考这里

写好html后,就可以配置参数了,像这样:

footer: {
  content: render_to_string(
    'template/footer.html',
    layout: false
    )
  }

注意

不要用复杂的flex布局,可能会不支持!