SpringBoot + FreeMarker + FlyingSaucer 實現PDF線上預覽、列印、下載
阿新 • • 發佈:2019-02-08
關鍵技術點:
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,很簡單,一個標題,兩個按鈕,一個預覽功能,一個下載功能,同時預接收一個${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: 宋體">
<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.javaPublicController.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.javapackage 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檔案中)。