基于flying-saucer和freemark实现HTML导出pdf

一、背景


最近需要生成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


flying-saucer-pdf是一个Java库,用于将HTML文档转换为PDF格式。它提供了一种简单的方式来生成高质量的PDF文件,可以用于生成报告、电子书、发票等各种类型的文档。 使用flying-saucer-pdf可以通过以下步骤来下载PDF工具类: 1. 首先,你需要在你的项目中添加flying-saucer-pdf的依赖。你可以在Maven或Gradle中添加以下依赖: Maven: ```xml <dependency> <groupId>org.xhtmlrenderer</groupId> <artifactId>flying-saucer-pdf</artifactId> <version>9.1.22</version> </dependency> ``` Gradle: ```groovy implementation 'org.xhtmlrenderer:flying-saucer-pdf:9.1.22' ``` 2. 下载完成后,你可以使用flying-saucer-pdf提供的API来生成PDF文件。首先,你需要创建一个`ITextRenderer`对象,然后将HTML内容加载到该对象中,并使用`createPDF()`方法将其转换为PDF文件。以下是一个简单的示例代码: ```java import org.xhtmlrenderer.pdf.ITextRenderer; public class PdfGenerator { public static void main(String[] args) throws Exception { String htmlContent = "<html><body><h1>Hello, World!</h1></body></html>"; ITextRenderer renderer = new ITextRenderer(); renderer.setDocumentFromString(htmlContent); renderer.layout(); String outputFile = "output.pdf"; renderer.createPDF(new FileOutputStream(outputFile)); System.out.println("PDF generated successfully!"); } } ``` 以上代码将生成一个包含"Hello, World!"标题的PDF文件,并将其保存为output.pdf。 希望这个简单的介绍能帮助到你!如果你有任何进一步的问题,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值