使用pdfkit+wkhtmltopdf将html页面转换为pdf

ruby转换pdf相关的库有:pdfkitwicked_pdf,都是基于wkhtmltopdf开发的。我选用pdfkit,相对简洁一点。(核心功能都一样)


安装


wkhtmltopdf安装直接去官网下载安装包即可(https://wkhtmltopdf.org/downloads.html)

WX20190211-194636

gem install pdfkit wkhtmltopdf-binary 

Html页面准备


首先把controller、routes、template构建好。

#reports_controller
class ReportsController < ApplicationController
    def demo
      respond_to do |format|
        format.html
      end
    end
end
#routers
get 'reports/demo' => 'reports#demo'
#templates/reports/demo.html.erb
<h1>测试报表</h1>
<table class="table table-bordered table-striped">
	<thead>
		<th></th>
		<% (0..23).each do |num| %>
			<th><%= "#{num}点" %></th>
		<% end %>
	</thead>

	<tbody>
		<% (1..30).each do |num| %>
		<tr>
			<td><%= num %></td>
			<% (0..23).each do |num| %>
				<% operaty = rand(100) %>
				<td style="background: rgba(255, 255, 0, <%= operaty / 100.0 %>)"><%= operaty %>%</td>
			<% end %>
		</tr>
		<% end %>
	</tbody>
</table>

页面是随机生成1~30、0点~23点的黄色块随机透明度。

WX20190211-204307


调试


wkhtmltopdf

首先先试下wkhtmltopdf的命令行,搞明白这个库大概的功能。

wkhtmltopdf -V
#=> wkhtmltopdf 0.12.4 (with patched qt)

wkhtmltopdf http://localhost:3000/reports/demo demo.pdf

WX20190211-205052

导出成功

WX20190211-205145


pdfkit

现在调试pdfkit的用法。

kit = PDFKit.new('http://localhost:3000/reports/demo', page_size: 'A3', dpi: 300, orientation: 'landscape')
kit.to_file("/tmp/demo.pdf")

WX20190211-210233

导出成功

WX20190211-210315


导出(转换为pdf并下载)


新建一个导出的action

WX20190211-210820

WX20190211-211020

WX20190211-210800

到这一步都没有问题,但是下载的时候问题来了

WX20190211-211427

WX20190211-212216

WX20190211-211700

一直卡着不动,最后报错了,怀疑是开发环境workers和threads的问题。 另起一个服务验证下,发现确实是这个问题。(在5000端口的服务导出3000端口的html是成功的)

WX20190211-212931

增加了workers的数量后,就能成功下载pdf了。

WX20190211-214628


设置html页眉页脚


这几个属性只能填text,比较简单,一般还是html用的多一些,可以实现图片、复杂的文字排版等等。

# reports_controller
  def demo
    html = render :template => 'reports/demo', :layout => 'reports'
    if params[:export] == "true"
      kit = PDFKit.new(html, page_size: 'A3', dpi: 300, orientation: 'landscape', :header_html => render_header_footer("header_test", (@main.project.id rescue nil)), :footer_html => render_header_footer("footer_test"))
      kit.stylesheets << open(URI("https://cdn.bootcss.com/twitter-bootstrap/3.4.0/css/bootstrap.min.css"))
      kit.stylesheets << "#{Rails.root}/app/assets/stylesheets/report.css"
      pdf = kit.to_file("/tmp/demo.pdf")
      send_file pdf
    else
      html
    end
  end
  
  private
  def render_header_footer(type, project_id = nil)
    compiled = ERB.new(File.read("#{Rails.root}/app/views/reports/#{type}.html.erb")).result(binding)
    file = Tempfile.new(["#{type}",".html"])
    file.write(compiled)
    file.rewind
    file.path
  end

header_test.html.erb

<!DOCTYPE html>
<html>
<meta charset="utf-8">
<style type="text/css">
	body{
		font-family: '微软雅黑' !important;
		margin:0;
		padding:0;
		height:60px;
		overflow:hidden;
	}
</style>
<head>
	<title></title>
</head>
	<body>
	<div style="float: left;">
		<img width="60" height="60" src="https://ss0.baidu.com/6ONWsjip0QIZ8tyhnq/it/u=2088390759,1902560199&fm=58&bpow=446&bpoh=512">
	</div>
	<div style="float:right;text-align: right;">
		<%= Time.now.strftime '%Y-%m-%d' %>
		<br />
		<%= Time.now.strftime '%H:%M:%S' %>
	</div>
	</body>
</html>

footer_test.html.erb

<!DOCTYPE html>
<html>
<meta charset="utf-8">
<style type="text/css">
	body{
		font-family: '微软雅黑' !important;
		margin:0;
		padding:0;
		height:60px;
		overflow:hidden;
	}
</style>
<head>
	<title></title>
</head>
<body>
<div style="text-align: center;">
	-- 测试页脚 --
</div>
</body>
</html>

这样就能根据自己的需求进行排版了,html不要用url的方式读取(网络延迟经常抛异常),要用文件方式读取。

WX20190213-220125


优化


上面这种方法虽然能够实现pdf的转换和下载,但是有几个问题:

  1. 每次导出都是增加访问html的请求,导出所产生的请求数为 n*2。
  2. 需要传递html的url地址,这样也就需要配置各个环境的root_url(http://localhost、http://xxx.com),如果url本身包含各种参数就更麻烦了。
  3. html页面的权限控制:这点用之前的方案是无法实现的。但是在企业级应用中,报表的权限控制是非常复杂和必不可少的。

现在来试着优化一下:

用一个action实现html的预览和导出下载功能,区别就是将html对象直接生成好,传给pdfkit,不再用url方式。通过export参数的判断来控制预览和下载功能,这样就能在action上进行权限控制了!

  def demo
    html = render :template => 'reports/demo', :layout => 'reports'
    if params[:export] == "true"
      kit = PDFKit.new(html, page_size: 'A3', dpi: 300, orientation: 'landscape')
      # 最后总结会解释
      # kit.stylesheets << open(URI("https://cdn.bootcss.com/twitter-bootstrap/3.4.0/css/bootstrap.min.css"))
      # kit.stylesheets << "#{Rails.root}/app/assets/stylesheets/report.css"
      pdf = kit.to_file("/tmp/demo.pdf")
      send_file pdf
    else
      html
    end
  end


总结


在实际应用过程中还有一些细节问题:

1)当pdfkit直接接受html对象时,css需要重新载入。

# 用bootstrap做案例
kit.stylesheets << open(URI("https://cdn.bootcss.com/twitter-bootstrap/3.4.0/css/bootstrap.min.css"))
# 自己定义的样式
kit.stylesheets << "#{Rails.root}/app/assets/stylesheets/report.css"

2)pdf分页后表格被切断,需要设置table的css属性。

WX20190211-221050

table, tr, td, th, tbody, thead, tfoot {
    page-break-inside: avoid;
}

WX20190211-221315

3)分页时重复表头的显示,table中要规范使用thead标签。

WX20190211-221811

4)打印的pdf的dpi要设置为300。