使用pdfkit+wkhtmltopdf将html页面转换为pdf
ruby转换pdf相关的库有:pdfkit、wicked_pdf,都是基于wkhtmltopdf开发的。我选用pdfkit,相对简洁一点。(核心功能都一样)
安装
wkhtmltopdf安装直接去官网下载安装包即可(https://wkhtmltopdf.org/downloads.html)
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点的黄色块随机透明度。
调试
wkhtmltopdf
首先先试下wkhtmltopdf的命令行,搞明白这个库大概的功能。
wkhtmltopdf -V
#=> wkhtmltopdf 0.12.4 (with patched qt)
wkhtmltopdf http://localhost:3000/reports/demo demo.pdf
导出成功
pdfkit
现在调试pdfkit的用法。
kit = PDFKit.new('http://localhost:3000/reports/demo', page_size: 'A3', dpi: 300, orientation: 'landscape')
kit.to_file("/tmp/demo.pdf")
导出成功
导出(转换为pdf并下载)
新建一个导出的action
到这一步都没有问题,但是下载的时候问题来了
一直卡着不动,最后报错了,怀疑是开发环境workers和threads的问题。 另起一个服务验证下,发现确实是这个问题。(在5000端口的服务导出3000端口的html是成功的)
增加了workers的数量后,就能成功下载pdf了。
设置html页眉页脚
- header_left
- header_center
- header_right
- footer_left
- footer_center
- footer_right
这几个属性只能填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的方式读取(网络延迟经常抛异常),要用文件方式读取。
优化
上面这种方法虽然能够实现pdf的转换和下载,但是有几个问题:
- 每次导出都是增加访问html的请求,导出所产生的请求数为 n*2。
- 需要传递html的url地址,这样也就需要配置各个环境的root_url(http://localhost、http://xxx.com),如果url本身包含各种参数就更麻烦了。
- 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属性。
table, tr, td, th, tbody, thead, tfoot {
page-break-inside: avoid;
}
3)分页时重复表头的显示,table中要规范使用thead标签。
4)打印的pdf的dpi要设置为300。