SpringBoot + FreeMarker + FlyingSaucer 实现PDF在线预览、打印、下载
原文:《SpringBoot + FreeMarker + FlyingSaucer 实现PDF在线预览、打印、下载》
案例2:《vue +SpringBoot + FreeMarker + FlyingSaucer 实现PDF在线预览、打印、下载》
关键技术点:
1. Freemarker模板引擎
模板语法
2. FlyingSaucer根据模板生成pdf
兼容中文(及中文换行问题)
兼容CSS(绝对、相对定位)
兼容图片
多页输出
(示例代码没有dao、service层,生产环境中自行添加,本示例完整,不坑人)
实现步骤一:SpringBoot项目搭建
项目结构截图
Maven依赖配置
<!-- freemarker依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <!-- web基础依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- FlyingSaucer依赖 https://mvnrepository.com/artifact/org.xhtmlrenderer/flying-saucer-pdf --> <dependency> <groupId>org.xhtmlrenderer</groupId> <artifactId>flying-saucer-pdf</artifactId> <version>9.1.12</version> </dependency>
PDF工具类编写
PdfUtils.java,方法上有完整注释,思路是利用模板引擎动态处理模板参数,先生成html字符串放在StringWriter中,再用HTML字符串生成Document,再利用FlyingSaucer的ITextRenderer处理Document,最后输出pdf。
package com.suncd.demopdf.Utils; import com.lowagie.text.pdf.BaseFont; import freemarker.template.Template; import freemarker.template.TemplateException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.CollectionUtils; import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; import org.w3c.dom.Document; import org.xhtmlrenderer.pdf.ITextFontResolver; import org.xhtmlrenderer.pdf.ITextRenderer; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.io.*; import java.util.List; import java.util.Map; /** * 功能:pdf处理工具类 * * @author qust * @version 1.0 2018/2/23 17:21 */ public class PdfUtils { private PdfUtils() { } private static final Logger LOGGER = LoggerFactory.getLogger(PdfUtils.class); /** * 按模板和参数生成html字符串,再转换为flying-saucer识别的Document * * @param templateName freemarker模板名称 * @param variables freemarker模板参数 * @return Document */ private static Document generateDoc(FreeMarkerConfigurer configurer, String templateName, Map<String, Object> variables) { Template tp; try { tp = configurer.getConfiguration().getTemplate(templateName); } catch (IOException e) { LOGGER.error(e.getMessage(), e); return null; } StringWriter stringWriter = new StringWriter(); try(BufferedWriter writer = new BufferedWriter(stringWriter)) { try { tp.process(variables, writer); writer.flush(); } catch (TemplateException e) { LOGGER.error("模板不存在或者路径错误", e); } catch (IOException e) { LOGGER.error("IO异常", e); } DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); return builder.parse(new ByteArrayInputStream(stringWriter.toString().getBytes())); }catch (Exception e){ LOGGER.error(e.getMessage(), e); return null; } } /** * 核心: 根据freemarker模板生成pdf文档 * * @param configurer freemarker配置 * @param templateName freemarker模板名称 * @param out 输出流 * @param listVars freemarker模板参数 * @throws Exception 模板无法找到、模板语法错误、IO异常 */ private static void generateAll(FreeMarkerConfigurer configurer, String templateName, OutputStream out, List<Map<String, Object>> listVars) throws Exception { if (CollectionUtils.isEmpty(listVars)) { LOGGER.warn("警告:freemarker模板参数为空!"); return; } ITextRenderer renderer = new ITextRenderer(); Document doc = generateDoc(configurer, templateName, listVars.get(0)); renderer.setDocument(doc, null); //设置字符集(宋体),此处必须与模板中的<body style="font-family: SimSun">一致,区分大小写,不能写成汉字"宋体" ITextFontResolver fontResolver = renderer.getFontResolver(); fontResolver.addFont("simsun.ttc", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); //展现和输出pdf renderer.layout(); renderer.createPDF(out, false); //根据参数集个数循环调用模板,追加到同一个pdf文档中 //(注意:此处从1开始,因为第0是创建pdf,从1往后则向pdf中追加内容) for (int i = 1; i < listVars.size(); i++) { Document docAppend = generateDoc(configurer, templateName, listVars.get(i)); renderer.setDocument(docAppend, null); renderer.layout(); renderer.writeNextDocument(); //写下一个pdf页面 } renderer.finishPDF(); //完成pdf写入 } /** * pdf下载 * * @param configurer freemarker配置 * @param templateName freemarker模板名称(带后缀.ftl) * @param listVars 模板参数集 * @param response HttpServletResponse * @param fileName 下载文件名称(带文件扩展名后缀) */ public static void download(FreeMarkerConfigurer configurer, String templateName, List<Map<String, Object>> listVars, HttpServletResponse response, String fileName) { // 设置编码、文件ContentType类型、文件头、下载文件名 response.setCharacterEncoding("utf-8"); response.setContentType("multipart/form-data"); try { response.setHeader("Content-Disposition", "attachment;fileName=" + new String(fileName.getBytes("gb2312"), "ISO8859-1")); } catch (UnsupportedEncodingException e) { LOGGER.error(e.getMessage(), e); } try (ServletOutputStream out = response.getOutputStream()) { generateAll(configurer, templateName, out, listVars); out.flush(); } catch (Exception e) { LOGGER.error(e.getMessage(), e); } } /** * pdf预览 * * @param configurer freemarker配置 * @param templateName freemarker模板名称(带后缀.ftl) * @param listVars 模板参数集 * @param response HttpServletResponse */ public static void preview(FreeMarkerConfigurer configurer, String templateName, List<Map<String, Object>> listVars, HttpServletResponse response) { try (ServletOutputStream out = response.getOutputStream()) { generateAll(configurer, templateName, out, listVars); out.flush(); } catch (Exception e) { LOGGER.error(e.getMessage(), e); } } }
中文字符坑点:
填坑:
generateAll方法中
//设置字符集(宋体),此处必须与模板中的<body style="font-family: SimSun">一致,区分大小写,不能写成汉字"宋体" ITextFontResolver fontResolver = renderer.getFontResolver(); fontResolver.addFont("simsun.ttc", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
①需要拷贝宋体字体文件到resource目录下(字体位置在“c:/Windows/Fonts/simsun.ttc”),方便集成和迁移
②在页面中设置body的样式<body style="font-family: SimSun">,必须写成英文,同时大小写敏感,
另外:也有不少文章直接根据操作系统类型取宋体字体文件路径的全路径,如下,显得代码臃肿:
注意: generateAll方法中已经实现了一个模板接收多个参数对象,输出多页到一个pdf文件中,读者可根据自己需要改造
实现步骤二:FreeMarker模板编写
跟编写普通html页面一样,定义2个页面,一个主页面index.ftl,一个pdf模板页面pdfPage.ftl
文件结构:
配置index.ftl
index.ftl,很简单,一个标题,两个按钮,一个预览功能,一个下载功能,同时预接收一个${title}参数
注:freemarker的语法和原理,读者自行科普
<!DOCTYPE html> <html> <head lang="en"> <title>Demo Page PDF</title> </head> <body> <h2>Demo Page ${title}</h2> <div><a href="/pdf/preview" target="_blank"> 强大的预览 </a></div> <div><a href="/pdf/download"> 强大的下载 </a></div> </body> </html>
配置 pdfPage.ftl
<!DOCTYPE html> <html> <head lang="en"> <title>Spring Boot Demo - PDF</title> <link href="http://localhost:8999/css/index.css" rel="stylesheet" type="text/css"/> <style> @page { size: 210mm 297mm; /*设置纸张大小:A4(210mm 297mm)、A3(297mm 420mm) 横向则反过来*/ margin: 0.25in; padding: 1em; @bottom-center{ content:"成都太阳高科技 ? 版权所有"; font-family: SimSun; font-size: 12px; color:red; }; @top-center { content: element(header) }; @bottom-right{ content:"第" counter(page) "页 共 " counter(pages) "页"; font-family: SimSun; font-size: 12px; color:#000; }; } </style> </head> <body style="font-family: 'SimSun'"> <div>1.标题-中文</div> <h2>${title}</h2> <div>2.按钮:按钮的边框需要写css渲染</div> <button class="a" style="border: 1px solid #000000"> click me t-p</button> <div id="divsub"></div> <div>3.普通div</div> <div id="myheader">Alice's Adventures in Wonderland</div> <div>4.图片 绝对定位到左上角(注意:图片必须用全路径或者http://开头的路径,否则无法显示)</div> <div id="signImg"></div> <div>5.普通table表格</div> <div> <table> <tr> <td>1</td> <td>2</td> <td>2</td> <td>2</td> <td>2</td> </tr> <tr> <td>1</td> <td>2</td> <td>2</td> <td>2</td> <td>2</td> </tr> <tr> <td>1</td> <td>2</td> <td>2</td> <td>2</td> <td>2</td> </tr> </table> </div> <div>6.input控件,边框需要写css渲染 (在模板中一般不用input,因为不存在输入操作)</div> <div> <label>姓名:</label> <input id="input1" aria-label="dasdasd" type="text" value="123你是"/> </div> </body> </html>
坑点(用户经常有页面尺寸需求,比如纸张类型):
1. 页面尺寸(A3,A4)设置和脚标设置
页面尺寸填坑: 在<head>节点中加入CSS3页面page属性,以毫米为单位设置size,即最终输出pdf每页的大小
A3: 297mm * 420mm (纵向)
A4: 210mm * 297mm (纵向)
A3: 420mm * 297mm (横向)
A4: 297mm * 210mm (横向)
这些都可以写成${XXX}占位符形式,通过后端代码传入
脚标填坑: 见下图
2. CSS路径和图片路径
填坑css路径: 引用css文件必须用http://全路径,如上图,可以把css文件单独放到一台服务器上,通过域名或者ip+端口访问.
填坑图片路径: css中引用的图片一样要使用http://全路径,如下图:
实现步骤三:Controller代码编写
写两个Controller,PublicController.java 和 PdfController.java
PublicController.java用来访问主页面, PdfController.java用来接受预览和下载请求
PublicController.java
package com.suncd.demopdf.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; /** * 功能:公共 * * @author qust * @version 1.0 2018/2/23 11:56 */ @Controller public class PublicController { @RequestMapping(value = "/") public ModelAndView index(ModelAndView modelAndView) { modelAndView.setViewName("index"); modelAndView.addObject("title", "CGX"); return modelAndView; } }
PdfController.java
package com.suncd.demopdf.controller; import com.suncd.demopdf.Utils.PdfUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 功能:pdf预览、下载 * * @author qust * @version 1.0 2018/2/23 9:35 */ @Controller @RequestMapping(value = "/pdf") public class PdfController { @Autowired private FreeMarkerConfigurer configurer; /** * pdf预览 * * @param request HttpServletRequest * @param response HttpServletResponse */ @RequestMapping(value = "/preview", method = RequestMethod.GET) public void preview(HttpServletRequest request, HttpServletResponse response) { // 构造freemarker模板引擎参数,listVars.size()个数对应pdf页数 List<Map<String,Object>> listVars = new ArrayList<>(); Map<String,Object> variables = new HashMap<>(); variables.put("title","测试预览ASGX!"); listVars.add(variables); PdfUtils.preview(configurer,"pdfPage.ftl",listVars,response); } /** * pdf下载 * * @param request HttpServletRequest * @param response HttpServletResponse */ @RequestMapping(value = "/download", method = RequestMethod.GET) public void download(HttpServletRequest request, HttpServletResponse response) { List<Map<String,Object>> listVars = new ArrayList<>(); Map<String,Object> variables = new HashMap<>(); variables.put("title","测试下载ASGX!"); listVars.add(variables); PdfUtils.download(configurer,"pdfPage.ftl",listVars,response,"测试中文.pdf"); } }
实现步骤四:配置application.yml
server: port: 8999
实现步骤五:运行演示
运行项目,访问http://localhost:8999/
点击预览效果如下(有个小坑,就是input控件中的汉字有问题,反正我实际生产中pdf模板不用input控件),其实这个页面已集成了下载和打印功能,这是Chrome自带的pdf预览。
再点击下载,效果如下:
显示已下载,从pdf软件打开该pdf文件效果如下:
大功告成!
坑点总结
1. 中文字体
2. Css路径
3. 图片路径
4. 页面尺寸(纸张大小)
建议
该示例只是为了演示如何利用freemarker模板引擎生成pdf预览、下载,其中数据都为静态数据,在实际项目中调整数据来源可完美达到预期效果,目前支持比较好的是Chrome内核浏览器,为达到更好的浏览器支持,可以用PDF.js来完成兼容。
PdfUtils.java只是对模板操作做了简单封装,可以根据自己的需要进行二次封装,generateAll方法中已经实现了一个模板接收多个参数对象,输出多页到一个pdf文件中,读者可根据自己需要改造(比如把多个不同的模板输出到一个pdf文件中)。
源代码GITHUB地址: https://github.com/QuSongtao/demo-pdf
源代码gitee地址: https://gitee.com/Alan2022/dome-pdf.git
学问:纸上得来终觉浅,绝知此事要躬行
为事:工欲善其事,必先利其器。
态度:道阻且长,行则将至;行而不辍,未来可期
.....................................................................
------- 桃之夭夭,灼灼其华。之子于归,宜其室家。 ---------------
------- 桃之夭夭,有蕡其实。之子于归,宜其家室。 ---------------
------- 桃之夭夭,其叶蓁蓁。之子于归,宜其家人。 ---------------
=====================================================================
* 博客文章部分截图及内容来自于学习的书本及相应培训课程以及网络其他博客,仅做学习讨论之用,不做商业用途。
* 如有侵权,马上联系我,我立马删除对应链接。 * @author Alan -liu * @Email no008@foxmail.com
转载请标注出处! ✧*꧁一品堂.技术学习笔记꧂*✧. ---> https://www.cnblogs.com/ios9/