SpringBoot + FreeMarker + FlyingSaucer 动态生成PDF及在线预览、打印、下载

1. 关键技术点

(1)Freemarker模板引擎

  • 模板语法

(2)FlyingSaucer根据模板生成pdf

  • 兼容中文(及中文换行问题)
  • 兼容CSS(绝对、相对定位)
  • 兼容图片
  • 多页输出

(示例代码没有dao、service层,生产环境中自行添加,本示例完整,不坑人)

源代码地址: https://github.com/QuSongtao/demo-pdf

2. 实现步骤

2.1 SpringBoot项目搭建

spring-boot-starter-parent版本为1.5.10.RELEASE,想使用2.0的伙伴自行去升级,目前没有做过测试。

2.2 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.22</version>
</dependency>

2.3 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文件中,读者可根据自己需要改造

2.4 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: '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://全路径

大家研究一下有没有更好的办法。

2.5 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");
    }
}

2.6 配置application.yml

server:
  port: 8999

2.7 运行演示

运行项目,访问http://localhost:8999/

点击预览效果如下(有个小坑,就是input控件中的汉字有问题,反正我实际生产中pdf模板不用input控件),其实这个页面已集成了下载和打印功能,这是Chrome自带的pdf预览。

再点击下载,效果如下:

显示已下载,从pdf软件打开该pdf文件效果如下:

大功告成!

3. 坑点总结

1. 中文字体
2. Css路径
3. 图片路径
4. 页面尺寸(纸张大小)

4. 建议

该示例只是为了演示如何利用freemarker模板引擎生成pdf预览、下载,其中数据都为静态数据,在实际项目中调整数据来源可完美达到预期效果,目前支持比较好的是Chrome内核浏览器,为达到更好的浏览器支持,可以用PDF.js来完成兼容。

PdfUtils.java只是对模板操作做了简单封装,可以根据自己的需要进行二次封装,generateAll方法中已经实现了一个模板接收多个参数对象,输出多页到一个pdf文件中,读者可根据自己需要改造(比如把多个不同的模板输出到一个pdf文件中)。

源代码GITHUB地址: https://github.com/QuSongtao/demo-pdf

  • 13
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 34
    评论
Spring Boot是一个用于构建独立的、基于生产级别的Spring应用程序的框架。而Freemarker是一种模板引擎,用于生成动态内容,特别适合Web应用程序的开发。在Spring Boot中使用Freemarker可以通过引入相应的依赖和配置来实现。 首先,在Spring Boot工程中引入Freemarker依赖。可以通过在pom.xml文件中添加以下依赖来实现: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> ``` 这样,在工程中就可以使用Freemarker来处理模板了。如果想要修改模板文件的位置等配置,可以在application.properties文件中进行配置。比如可以通过以下配置来指定模板文件的位置: ``` spring.freemarker.allow-request-override=false spring.freemarker.allow-session-override=false spring.freemarker.cache=false spring.freemarker.charset=UTF-8 spring.freemarker.check-template-location=true spring.freemarker.content-type=text/html spring.freemarker.expose-request-attributes=false spring.freemarker.expose-session-attributes=false spring.freemarker.suffix=.ftl spring.freemarker.template-loader-path=classpath:/templates/ ``` 这样,Spring Boot就会根据这些配置来加载并解析模板文件,生成相应的动态内容。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Spring Boot 整合 Freemarker](https://blog.csdn.net/yaxuan88521/article/details/117173289)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [Springboot整合FreeMarker](https://blog.csdn.net/m0_67402096/article/details/126114796)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 34
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值