0
点赞
收藏
分享

微信扫一扫

STL——常用排序算法

拾杨梅记 2024-07-28 阅读 28

在这里插入图片描述

Spring Boot 集成 OpenPDF 和 Freemarker 实现 PDF 导出功能

前言

本文对应代码下载地址: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标签的一些分支

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

如果大家有看了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 文档

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


在这里插入图片描述

举报

相关推荐

0 条评论