Spring Boot集成OpenPDF和Freemarker实现PDF导出功能并附水印

在这里插入图片描述

😄 19年之后由于某些原因断更了三年,23年重新扬帆起航,推出更多优质博文,希望大家多多支持~
🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志
🎐 个人CSND主页——Micro麦可乐的博客
🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战
🌺《RabbitMQ》专栏主要介绍使用JAVA开发RabbitMQ的系列教程,从基础知识到项目实战
🌸《设计模式》专栏以实际的生活场景为案例进行讲解,让大家对设计模式有一个更清晰的理解
💕《Jenkins实战》专栏主要介绍Jenkins+Docker的实战教程,让你快速掌握项目CI/CD,是2024年最新的实战教程
🌞《Spring Boot》专栏主要介绍我们日常工作项目中经常应用到的功能以及技巧,代码样例完整
如果文章能够给大家带来一定的帮助!欢迎关注、评论互动~

前言

本文对应代码下载地址:https://download.csdn.net/download/lhmyy521125/89590079 无需积分!无需积分!

在我们日常开发中,生成 PDF 文件是一项常见的需求。无论是生成单据、报表、发票还是其他文档,PDF 格式因其便捷的打印和跨平台支持而被广泛使用。本文将介绍如何在 Spring Boot 项目中使用 flying-saucer-pdfFreemarker 来实现 HTML 模板到 PDF 的导出功能

flying-saucer-pdf + html输出的单据效果:

在这里插入图片描述
OpenPDF后端编码形式输出的单据效果:

在这里插入图片描述


概述

Flying Saucere介绍
项目地址:https://github.com/flyingsaucerproject/flyingsaucer

Flying Saucer是一个纯Java库,用于使用CSS 2.1 / CSS 3呈现任意格式良好的XML(或XHTML),用于布局和格式化,输出到Swing面板,PDF和图像

使用文档:https://flyingsaucerproject.github.io/flyingsaucer/r8/guide/users-guide-R8.html

OpenPDF介绍
项目地址:https://github.com/LibrePDF/OpenPDF

OpenPDF是一个用于创建和编辑PDF文件的Java库,具有LGPL和MPL开源许可证。OpenPDF是iText的LGPL/MPL开源继承者,基于iText 4 svn标签的一些分支

不同版本的OpenPDF,它们需要不同版本的Java

  • 2.0.x分支需要Java 17或更高版本。
  • 1.4.x分支需要Java 11或更高版本。
  • 1.3.x分支需要Java 8或更高版本。

为什么要把这两个放在一起说?

如果大家有看了Flying SaucereGitHub上的介绍,你会发现 flying-saucer-pdf 实际上是依赖于OpenPDF
在这里插入图片描述
也就是说无论我们是要基于HTML模版来生成,还是采用后端编码的形式生成,我们都只需要引入 flying-saucer-pdf 依赖即可,比如博主文章开始的效果截图


实战开始

❶ 项目初始化

首先,创建一个新的 Spring Boot 项目,在在 pom.xml 文件中添加相关依赖

<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Boot Starter Freemarker -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-freemarker</artifactId>
    </dependency>

    <!-- 实际上 flying-saucer-pdf 使用OpenPDF实现 -->
    <dependency>
        <groupId>org.xhtmlrenderer</groupId>
        <artifactId>flying-saucer-pdf</artifactId>
        <version>9.9.0</version>
    </dependency>
</dependencies>

❷ 配置 Freemarker

application.yml 文件中添加 Freemarker 的基本配置

# freemarker配置 实际上也可以直接默认Springboot装配配置
# 更多是只需要修改模版后缀 和 模版路径
spring:
    freemarker:
        suffix: .ftl
        charset: utf-8
        template-loader-path: classpath:/templates/
        expose-request-attributes: true
        expose-session-attributes: true
        expose-spring-macro-helpers: true

❸ 创建 HTML 模板

src/main/resources/templates 目录下创建一个 Freemarker 模板文件 template.ftl

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>测试导出单据模版</title>
  <link href="https://demo.ruoyi.vip/css/bootstrap.min.css?v=3.3.7" rel="stylesheet" type="text/css"/>
  <link href="https://demo.ruoyi.vip/css/style.min.css?v=20210831" rel="stylesheet"/>
  <link href="https://demo.ruoyi.vip/ruoyi/css/ry-ui.css?v=4.7.9" rel="stylesheet"/>
  <style>
    @page {
      size: 210mm 297mm; /*设置纸张大小:A4(210mm 297mm)、A3(297mm 420mm) 横向则反过来*/
      margin: 0.5in;
      @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;
      };
    }
    body{
      font-family: SimSun;
    }
    img {
      width: 50px;
    }
  </style>
</head>


<body class="gray-bg">
<div>
  <div class="row">
    <div class="col-sm-12">
      <div class="ibox-content">
        <div class="row">
          <div class="col-sm-6 text-right">
            <h4>单据编号:</h4>
            <h4 class="text-navy">H+-000567F7-00</h4>
            <address>
              <strong>${companyName}</strong><br/>
              ${address}<br/>
              <abbr title="Phone">总机:</abbr> ${tel}
            </address>
            <p>
              <span><strong>日期:</strong> 2014-11-11</span>
            </p>
          </div>
        </div>

        <div class="table-responsive m-t">
          <table class="invoice-table" style="width: 100%; line-height: 60px">
            <thead>
            <tr>
              <th>图片</th>
              <th>清单</th>
              <th>数量</th>
              <th>单价</th>
              <th>总价</th>
            </tr>
            </thead>
            <tbody>
            <#if products?? && (products?size> 0)>
              <#list products as p>
              <tr>
                <td><img src="${p.productImg}" /></td>
                <td><strong>${p.productName}</strong></td>
                <td>${p.quantity}</td>
                <td>&yen;${p.price}</td>
                <td>&yen;${p.total}</td>
              </tr>
              </#list>
            </#if>

            </tbody>
          </table>
        </div>
        <!-- /table-responsive -->

        <table class="invoice-total" style="width: 100%; line-height: 30px">
          <tbody>
          <tr>
            <td><strong>总价:</strong>
            </td>
            <td>&yen;${total}</td>
          </tr>
          <tr>
            <td><strong>税:</strong>
            </td>
            <td>&yen;${tax}</td>
          </tr>
          <tr>
            <td><strong>总计</strong>
            </td>
            <td>&yen;${aggregate}</td>
          </tr>
          </tbody>
        </table>
        <div class="well m-t"><strong>注意:</strong> 请保存好单据</div>
      </div>
    </div>
  </div>
</div>
</body>
</html>

❹ 基于HTML模版 PDF 生成逻辑

创建一个 PdfService 类,用于生成 PDF 文件

package com.toher.project.openpdf;

import com.lowagie.text.*;
import com.lowagie.text.Font;
import com.lowagie.text.pdf.*;
import freemarker.cache.ClassTemplateLoader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;

import java.awt.*;
import java.io.ByteArrayOutputStream;
import java.util.Map;

@Service
public class PdfService {

    @Autowired
    private FreeMarkerConfigurer freeMarkerConfigurer;

    public byte[] generatePdf(Map<String, Object> data) throws Exception {
        // 生成HTML
        String html = FreeMarkerTemplateUtils.processTemplateIntoString(
                freeMarkerConfigurer.getConfiguration().getTemplate("template.ftl"), data);


        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ITextRenderer renderer = new ITextRenderer();
        //加载/resource/static/font的字体
        ClassTemplateLoader classTemplateLoader = new ClassTemplateLoader(PdfService.class, "/static/font");
        ITextFontResolver fontResolver = (ITextFontResolver)renderer.getSharedContext().getFontResolver();
        String fontPath = classTemplateLoader.getBasePackagePath() + "simsun.ttc";
        fontResolver.addFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);

        renderer.setDocumentFromString(html);
        renderer.layout();
        renderer.createPDF(out,false);
        PdfWriter writer = renderer.getWriter();

        //设置水印
        BaseFont bfChinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
        Font docFont = new Font(bfChinese, 10, Font.UNDEFINED, Color.BLACK);
        writer.setPageEvent(new PdfPageEventHelper() {
            @Override
            public void onEndPage(PdfWriter writer, Document document) {
                PdfContentByte waterMar = writer.getDirectContentUnder();
                String text = "Micro麦可乐";
                addTextFullWaterMark(waterMar, text, bfChinese);
            }
        });
        renderer.finishPDF();

        return out.toByteArray();
    }

    public static void addTextFullWaterMark(PdfContentByte waterMar, String text, BaseFont bfChinese) {
        waterMar.beginText();

        PdfGState gs = new PdfGState();
        // 设置填充字体不透明度为0.2f
        gs.setFillOpacity(0.1f);
        waterMar.setFontAndSize(bfChinese, 40);
        // 设置透明度
        waterMar.setGState(gs);
        // 设置水印对齐方式 水印内容 X坐标 Y坐标 旋转角度
        for (int x = 0; x <= 700; x += 200) {
            for (int y = 0; y <= 800; y += 200) {
                waterMar.showTextAligned(Element.ALIGN_RIGHT, text, x, y, 35);
            }
        }

        // 设置水印颜色
        waterMar.setColorFill(Color.GRAY);
        //结束设置
        waterMar.endText();
        waterMar.stroke();
    }
}

❺ 基于后端编码形式生成

有些项目不一定是采用html模版形式生成PDF,这里博主就简单演示一下,使用OpenPDF后端编码形式生成PDF

package com.toher.project.openpdf;

import com.lowagie.text.Font;
import com.lowagie.text.*;
import com.lowagie.text.Image;
import com.lowagie.text.alignment.HorizontalAlignment;
import com.lowagie.text.alignment.VerticalAlignment;
import com.lowagie.text.html.simpleparser.HTMLWorker;
import com.lowagie.text.pdf.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import org.xhtmlrenderer.pdf.ITextRenderer;

import java.awt.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.Map;

@Service
public class OpenPdfService {

    @Autowired
    private FreeMarkerConfigurer freeMarkerConfigurer;

    public byte[] generatePdf(Map<String, Object> data) throws Exception {
    
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        // 创建PDF文档
        Document document = new Document();
        PdfWriter writer = PdfWriter.getInstance(document, out);

        //如果需要定义字体,将自己的字体放在 resources/fonts目录下
        //BaseFont font = BaseFont.createFont("fonts/Viaoda_Libre/ViaodaLibre-Regular.ttf", BaseFont.IDENTITY_H, false);
        BaseFont bfChinese = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
        Font docFont = new Font(bfChinese, 10, Font.UNDEFINED, Color.BLACK);

        //设置水印
        writer.setPageEvent(new PdfPageEventHelper() {
            @Override
            public void onEndPage(PdfWriter writer, Document document) {
                PdfContentByte waterMar = writer.getDirectContentUnder();
                String text = "Micro麦可乐";
                addTextFullWaterMark(waterMar, text, bfChinese);
            }
        });
        // 设置边距
        document.setMargins(20, 20, 20, 20);
        // 打开文档
        document.open();

        /**
         * 01 表格演示
         */
        String[] tableTitle = new String[]{"清单", "数量", "单价", "总价"};
        Table table = new Table(tableTitle.length);
        table.setWidths(new float[]{70, 10, 10, 10});
        // 设置表格前的间距
        table.setSpacing(0);
        // 设置表格在页面中所占的宽度百分比
        table.setWidth(100);
        table.setBorder(0);
        //模拟5行表格数据
        for (int row = 0; row < 5; row++) {
            for (int i = 0; i < tableTitle.length; i++) {
                Chunk chunk;
                if (row == 0) {
                    chunk = new Chunk(tableTitle[i], docFont);
                } else {
                    chunk = new Chunk(row + "行 模拟数据" + i, docFont);
                }
                // 建立单元格
                Cell cell = new Cell(chunk);
                // 设置水平对齐
                cell.setHorizontalAlignment(HorizontalAlignment.CENTER);
                // 设置垂直对齐
                cell.setVerticalAlignment(VerticalAlignment.CENTER);
                table.addCell(cell);
            }
        }
        document.add(table);

        /**
         * 02 写入图片
         */
        byte[] byteArray = new byte[0];
        InputStream inputStream = this.getClass().getResourceAsStream("/static/img/test.png");
        if (inputStream != null) {
            byteArray = new byte[inputStream.available()];
            inputStream.read(byteArray);
        }

        Image image = Image.getInstance(byteArray);
        // 图片进行缩放
        image.scaleAbsolute(200, 200);
        document.add(image);

        /**
         * 03 写入html内容
         */
        HTMLWorker htmlWorker = new HTMLWorker(document);
        String html = "<p style='color: crimson'>Hello, micro</p>";
        htmlWorker.parse(new StringReader(html);
        // 关闭文档
        document.close();

        return out.toByteArray();
    }

    public static void addTextFullWaterMark(PdfContentByte waterMar, String text, BaseFont bfChinese) {
        waterMar.beginText();

        PdfGState gs = new PdfGState();
        // 设置填充字体不透明度为0.2f
        gs.setFillOpacity(0.2f);
        waterMar.setFontAndSize(bfChinese, 40);
        // 设置透明度
        waterMar.setGState(gs);
        // 设置水印对齐方式 水印内容 X坐标 Y坐标 旋转角度
        for (int x = 0; x <= 700; x += 200) {
            for (int y = 0; y <= 800; y += 200) {
                waterMar.showTextAligned(Element.ALIGN_RIGHT, text, x, y, 35);
            }
        }

        // 设置水印颜色
        waterMar.setColorFill(Color.GRAY);

        //结束设置
        waterMar.endText();
        waterMar.stroke();
    }
}

❻ 创建 Controller

创建一个 PdfController 类,用于处理生成 PDF 的请求

package com.toher.project.openpdf;

import com.lowagie.text.DocumentException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


@RestController
public class PdfController {

    @Autowired
    private PdfService pdfService;

    @Autowired
    private OpenPdfService openPdfService;

    /**
     * 采用flying-saucer-pdf html转pdf
     * @return
     */
    @GetMapping("/generate-pdf")
    public ResponseEntity<byte[]> generatePdf() {

        // 模拟数据库查询结果
        Map<String, Object> data = new HashMap<>();
        data.put("img", "https://demo.ruoyi.vip/img/profile.jpg");

        data.put("companyName", "阿里巴巴集团");
        data.put("address", "中国杭州市华星路99号东部软件园创业大厦6层(310099)");
        data.put("tel", "(+86) 571-8502-2088");
        data.put("creatTime", "2024-07-27");

        data.put("total", 1026.00);
        data.put("tax", 235.98);
        data.put("aggregate", 1261.98);

        List<ProductVo> products = new ArrayList<>();
        ProductVo productVo = new ProductVo();
        productVo.setProductImg("https://demo.ruoyi.vip/img/profile.jpg");
        productVo.setProductName("尚都比拉2013冬装新款女装 韩版修身呢子大衣 秋冬气质羊毛呢外套");
        productVo.setQuantity(1);
        productVo.setPrice(new BigDecimal("26"));
        productVo.setTotal(productVo.getPrice().multiply(productVo.getPrice()));
        products.add(productVo);

        ProductVo productVo1 = new ProductVo();
        productVo1.setProductImg("https://demo.ruoyi.vip/img/profile.jpg");
        productVo1.setProductName("11*11夏娜 新款斗篷毛呢外套 女秋冬呢子大衣 韩版大码宽松呢大衣");
        productVo1.setQuantity(2);
        productVo1.setPrice(new BigDecimal("80"));
        productVo1.setTotal(productVo.getPrice().multiply(productVo.getPrice()));
        products.add(productVo1);

        ProductVo productVo2 = new ProductVo();
        productVo2.setProductImg("https://demo.ruoyi.vip/img/profile.jpg");
        productVo2.setProductName("2013秋装 新款女装韩版学生秋冬加厚加绒保暖开衫卫衣 百搭女外套");
        productVo2.setQuantity(3);
        productVo2.setPrice(new BigDecimal("280"));
        productVo2.setTotal(productVo.getPrice().multiply(productVo.getPrice()));
        products.add(productVo2);

        data.put("products", products);

        byte[] pdfBytes = null;
        try {
            pdfBytes = pdfService.generatePdf(data);
        } catch (Exception e) {
            e.printStackTrace();
        }

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_PDF);
        headers.setContentDispositionFormData("attachment", "example.pdf");

        return ResponseEntity.ok().headers(headers).body(pdfBytes);
    }


    /**
     * 采用openpdf 生成pdf
     * @return
     */
    @GetMapping("/generate-openpdf")
    public ResponseEntity<byte[]> generateOpenPdf() {
        byte[] pdfBytes = null;
        try {
            pdfBytes = openPdfService.generatePdf();
        } catch (Exception e) {
            e.printStackTrace();
        }

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_PDF);
        headers.setContentDispositionFormData("attachment", "example.pdf");
        return ResponseEntity.ok().headers(headers).body(pdfBytes);
    }
}

❼ 测试和运行

启动 Spring Boot 应用程序,然后在浏览器中访问以下 URL:

#Html模版形式生成
http://localhost:8080/generate-pdf 

#后端编码形式生成
http://localhost:8080/generate-openpdf

浏览器将会下载生成的 PDF 文件 example.pdf,其中包含动态生成的内容,并且附加了水印

一点点建议

博主的代码中仅仅是为了让大家能快速熟悉,一些细节问题还需要大家在实际项目中进行优化调整

  • 模板设计:在设计 Freemarker 模板时,可以使用 CSS 来控制 PDF 的样式,使生成的 PDF 更加美观。
  • 水印设置:通过 CSS 设置水印样式,可以根据需求调整水印的位置、透明度、大小等属性。
  • 错误处理:在实际项目中,需增加错误处理和日志记录,确保在生成 PDF 过程中出现问题时能够及时发现并处理。
  • 性能优化:对于大批量生成 PDF 的场景,可以考虑使用异步处理或批处理机制,提高系统的处理能力。

总结

本文介绍了如何在 Spring Boot 项目中使用 Flying SaucerFreemarker 实现 PDF 导出功能,并附加水印,并也演示了直接在后端编码形式生成PDF
通过 Freemarker 模板引擎生成 HTML,再使用 Flying SaucerHTML 转换为 PDF,此方法灵活且易于扩展,可以根据业务需求生成复杂的 PDF 文档

如果本文对您有所帮助,希望 一键三连 给博主一点点鼓励,如果您有任何疑问或建议,请随时留言讨论!


在这里插入图片描述

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Spring Boot集成Freemarker导出Excel可以通过以下几个步骤来实现: 1. 添加依赖 在pom.xml文件中添加以下依赖: ```xml <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.31</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>4.1.2</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>4.1.2</version> </dependency> ``` 2. 创建Freemarker模板 创建一个freemarker模板,用于指定导出的excel文件的格式和样式。例如,可以创建一个名为`template.ftl`的模板文件,其中包含以下内容: ```html <table> <thead> <tr> <th>姓名</th> <th>年龄</th> <th>性别</th> <th>地址</th> </tr> </thead> <tbody> <#list users as user> <tr> <td>${user.name}</td> <td>${user.age}</td> <td>${user.gender}</td> <td>${user.address}</td> </tr> </#list> </tbody> </table> ``` 3. 创建Controller 创建一个Controller,用于接收请求并生成excel文件。例如,可以创建一个名为`ExportController`的Controller,其中包含以下代码: ```java @Controller public class ExportController { @Autowired private Configuration freemarkerConfig; @RequestMapping("/export") public void exportExcel(HttpServletRequest request, HttpServletResponse response) throws Exception { // 设置响应头 response.setContentType("application/vnd.ms-excel"); response.setHeader("Content-Disposition", "attachment;filename=users.xls"); // 获取模板 Template template = freemarkerConfig.getTemplate("template.ftl"); // 创建数据模型 Map<String, Object> model = new HashMap<String, Object>(); List<User> users = getUserList(); model.put("users", users); // 生成excel文件 Writer out = response.getWriter(); template.process(model, out); out.flush(); out.close(); } private List<User> getUserList() { List<User> users = new ArrayList<User>(); // 添加测试数据 users.add(new User("张三", 25, "男", "北京市海淀区")); users.add(new User("李四", 30, "女", "上海市浦东新区")); users.add(new User("王五", 35, "男", "广州市天河区")); users.add(new User("赵六", 40, "女", "深圳市福田区")); return users; } } ``` 4. 运行程序 启动Spring Boot应用程序,并访问`/export`路径即可下载生成的excel文件。 以上就是通过Spring Boot集成Freemarker导出Excel的步骤。需要注意的是,本例中使用的是xls格式的excel文件,如果需要使用xlsx格式,需要将poi-ooxml依赖修改为poi-ooxml-schemas。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Micro麦可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值