1. 程式人生 > >SpringBoot + FreeMarker + FlyingSaucer 實現PDF線上預覽、列印、下載

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,很簡單,一個標題,兩個按鈕,一個預覽功能,一個下載功能,同時預接收一個${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.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檔案中)。