原文:《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