一、背景
最近需要生成pdf报告,为了页面效果和方便后期维护,需要制作HTML模板,并从HTML导出pdf。基于这个需求,研究了一下前端和后台导出pdf的类库,先简单介绍下。
PD4ML是一个比较强大的java类库,是商业版的,因为我的项目是maven工程,不方便使用,没有详细研究。
iText是比较著名的java类库,我这里用5.5.11版本简单试了试。iText有很丰富的API,我们可以将HTML代码转化成iText可识别的Document对象,从而导出PDF文档。但是使用iText直接导出HTML页面的效果很糟糕,它无法识别很多HTML的tag和attribute,也不支持CSS。虽然Itext提供了很多底层的API,但是很不灵活,布局渲染都要hard code进java类里面,当需求发生改变时,哪怕只需要更改一个属性名,都要重新修改那段代码,维护起来相当麻烦。
html2pdf是开源的js库,使用起来也很简单,原理是将HTML页面转成图片,并添加到pdf中。Htm2pdf能将页面样式原封不动的保留,还封装了图片质量的参数,缺点是图片质量设置的比较高时,客户端压力较大,导出的pdf还会出现文字截断的现象,也就是pdf分页的地方会有一半文字在上一页,一半在下一页。
Wkhtmltopdf是第三方工具,导出效果也不错,同样会出现文字截断的现象。
flying sauser 是开源的java类库,它是基于iText并做了封装,能解析HTML和CSS,能输出image,PDF格式也可以方便的设置,很完美,完全满足我的需求,flying sauser也是我最终采用的库。
二、实现方法
使用flying sauser生成PDF的整个流程是这样的:
(1) 编写Freemarker模板,制作HTML报告,CSS样式可以使用,但注意不要引用需要js的样式文件。
(2) 在业务逻辑层中获取数据,使用Freemarker引擎生成最终的内容
(3) 调用flying sauser,生成PDF
jar包选择
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.23</version>
</dependency>
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf-itext5</artifactId>
<version>9.1.5</version>
</dependency>
flying sauser生成pdf代码
package com.iflytek.itesttech.utils;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import org.springframework.stereotype.Component;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.pdf.BaseFont;
@Component
public class ExportPdf {
public void exportPdfFromUrl
(String url,OutputStream os)
throws IOException{
ITextRenderer renderer
= new ITextRenderer();
//
指定模板地址
renderer.setDocument(url);
//
解决中文支持
try {
ITextFontResolver fontResolver =
renderer.getFontResolver();
String FontPath =
new File(this.getClass().getClassLoader().getResource
("fonts/msyh.ttf").getPath()).getCanonicalPath();
fontResolver.addFont(
FontPath,BaseFont.IDENTITY_H,
BaseFont.NOT_EMBEDDED);
}
catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
catch (DocumentException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
renderer.layout();
try {
renderer.createPDF(os);
}
catch (DocumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
os.flush();
os.close();
}
}
Controller浏览器输出文件
@RequestMapping(value =
"/pdfGenerate", method = RequestMethod.GET)
@ResponseBody
public void pdfGenerate
(HttpServletRequest request,
HttpServletResponse response,String pdfName)
{
String basePath = request.getScheme()
+ "://" + request.getServerName() + ":"
+ request.getServerPort()+request.getContextPath() + "/";
String url=basePath+"/report";
try {
pdfName = URLEncoder.encode(pdfName, "UTF-8");//
不能超过
17
个字
pdfName = pdfName.replace("+", "%20");//
处理空格
}
catch (java.io.UnsupportedEncodingException e) {
e.printStackTrace();
}
response.setHeader
("Content-disposition", "attachment;filename="+pdfName);
response.setContentType("application/pdf");
OutputStream os = null;
try {
os=response.getOutputStream();
exportPdf.exportPdfFromUrl(url, os);
}
catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
try {
if(os!=null){
os.flush();
os.close();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
HTML模板
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>
报告
</title>
<style type="text/css">
.table >tbody > tr > td,.table > thead > tr > th {
padding: 8px;
text-align: left;
}
@page{size:297mm 420mm;}
</style>
</head>
<body style="font-family:'Microsoft YaHei',sans-serif;
color: #333;
font-size: 14px;
-webkit-font-smoothing: antialiased;">
<div style="margin: 0px;padding:0px;">
<div style="background-color: #ffffff;
border-top: 0;
padding: 0 10px 20px 10px;
margin: 0 0;">
<div style="padding: 15px 20px 20px 20px;
margin:0 50px 10px 50px;border:0;">
<div style="text-align: center;">
<img id="img" src="/hole/img/secReportHead.jpg"
width="100%"/>
<div id="imgTitle" style="vertical-align: middle;
margin-top:-180px;z-index:2;color:#fff;">
<div style="font-size:24px;
line-height: 1.1;
font-weight: 500;
margin-top: 5px;
margin-bottom: 5px;">
${taskBuildInfo.scanName?default("")}</div>
<small>
报告生成时间:
${currTime?string("yyyy-MM-dd HH:mm:ss")}</small>
</div>
<div id="imgClear" style="margin-top:180px;"></div>
</div>
<div style="padding-top:30px;
padding-bottom:20px;margin-bottom:30px;
border-bottom:1px solid #eeeeee;">
<div style="float:left;
color:#333;font-size: 24px;
font-weight:500;line-height: 1.1;
margin-top: 5px;margin-bottom: 5px;">详细报告
</div>
<small style="float:right;line-height:4em">
检测时间:
${taskBuildInfo.startTime?string("yyyy-MM-dd HH:mm:ss")}</small>
<div style="clear:both;"></div>
</div>
<!--
表格
-->
<div id="hole_content"
style="padding:20px 0px 0px 0px;">
<!--
数据表格
-->
<table id="table_hole"
class="table table-striped toggle-arrow-tiny table-hover">
<thead>
<tr style="background: #f3f3f3;">
<th style="border:0;vertical-align: middle;">
名称
</th>
<th style="border:0;
vertical-align: middle;
text-align: center;">等级
</th>
<th style="border:0;
vertical-align: middle;">描述
</th>
</tr>
</thead>
<tbody id="tbody_hole">
<#if holeList?size==0>
<tr id="clearRow">
<td colspan="3" style="color:#333;">
没有数据
</td>
</tr>
<#else>
<#list list as item>
<tr style="background-color: #f9f9f9;">
<td style="width:45%;color:#333;">
${item.name?default("")}</td>
<td style="width:10%;
text-align: center;vertical-align: middle;">
<#if hole.level??>
<#if hole.level gte 3>
<span style="color: #e46c6c;">
高
</span>
<#elseif hole.level == 2>
<span style="color: #d2904c;">
中
</span>
<#elseif hole.level lte 1>
<span style="color: #98984c;">
低
</span>
<#else>
<span style="color: #98984c;">
${item.level?default("")}</span>
</#if>
</#if>
</td>
<td style="width:45%;">
${item.desc?default("")}</td>
</tr>
</#list>
</#if>
</tbody>
</table>
</div>
</div>
</div>
</div>
</body>
</html>
三、使用中的一些问题
1、flyingsauser中文字体问题
将系统字体文件msyh.ttf放入工程,并显示添加。注意字体文件与HTML模板中字体设置要一致,font-family不要使用中文,大小写正确。
ITextFontResolver fontResolver = renderer.getFontResolver();
fontResolver.addFont(FontPath,BaseFont.IDENTITY_H,BaseFont.NOT_EMBEDDED);
<body style="font-family:'Microsoft YaHei',sans-serif;”>
2、图片路径问题
flying sauser版本9.1.5能自动识别图片路径,jar包引入正确就能正常显示图片,不需要做多余的处理。
3、中文换行问题
flying sauser 9.1.5支持中文换行,不能正常显示的话注意看看是不是引入了多余的jar包,之前因为多引入了flyingsauser的core包导致中文不能换行,删除引用就能正常显示了。
4、HTML模板规则
(1)所有标签都要关闭
(2)大写标签不识别,都要用小写,比如<DIV><TD>统统不识别
(3)所有属性必须加引号,比如<tdcolspan=3 >要写成<tdcolspan="3" >
四、进阶
flying-saucer做一下特殊的页面设置,只用修改HTML页面,加入一些标签即可,十分简单方便,下面举几个例子。
1、pdf纸张设置,分页
只需要在head中加入page样式即可,设置边距和纸张大写
@page {
size:297mm 420mm;
margin: 20mm 5mm 40mm 5mm;
}
2、pdf水印设置
只需要在body中加入背景图片即可
<body style="background-size: auto;
background:#ffffff url(/img/sy.png) repeat">
3、pdf标签设置
在页面合适位置增加锚点,并在head中加入<bookmarks>标签
<head>
<meta charset="UTF-8"/>
<bookmarks>
<bookmark name="summary" href="#summary" />
<bookmark name="table" href="#table" />
</bookmarks>
</head>
<!--
表格
-->
<a href="#table"name="table"></a>
4、pdf页眉页脚设置
在head中设置样式,在body中加入页面页脚内容。
<style>
#header {position: running(header);}
#footer {position: running(footer);}
@page{
@top-center{
content : element(header);
}
@bottom-center{
content : element(footer)
}
}
#pages:before{
content : counter(page);
font-size : 10px;
}
#pages:after{
content : counter(pages);
font-size : 10px;
}
</style>
<div id="header"
style="text-align: center;margin-top: 0px;
border-bottom:1px solid #ccc;">
<span>
科大讯飞测试技术部
</span>
</div>
<div id="footer" style="border-top:1px solid #ccc;">
<div style="text-align: center;">
第
<span id="pages">
页,共
</span>
页
</div>
</div>
2017.07.08测试技术嘉年华火热报名中
国际著名安全机构OWASP、前惠普高级性能工程师大牛就要来讯飞啦
还不快来报名!
报名地址:https://www.sojump.hk/jq/14382135.aspx
嘉年华官网:http://itest.iflytek.com/
爱测未来公众号:itest_forever
CSDN:http://blog.csdn.net/itest_2016
QQ群:274166295(爱测未来2群)、610934609(爱测未来3群)
会场地址:合肥市望江西路666号科大讯飞语音产业基地A1#201会场
时间:2017.07.08