😄 19年之后由于某些原因断更了三年,23年重新扬帆起航,推出更多优质博文,希望大家多多支持~
🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志
🎐 个人CSND主页——Micro麦可乐的博客
🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战
🌺《RabbitMQ》专栏主要介绍使用JAVA开发RabbitMQ的系列教程,从基础知识到项目实战
🌸《设计模式》专栏以实际的生活场景为案例进行讲解,让大家对设计模式有一个更清晰的理解
💕《Jenkins实战》专栏主要介绍Jenkins+Docker的实战教程,让你快速掌握项目CI/CD,是2024年最新的实战教程
🌞《Spring Boot》专栏主要介绍我们日常工作项目中经常应用到的功能以及技巧,代码样例完整
如果文章能够给大家带来一定的帮助!欢迎关注、评论互动~
Spring Boot 集成 OpenPDF 和 Freemarker 实现 PDF 导出功能
前言
本文对应代码下载地址:https://download.csdn.net/download/lhmyy521125/89590079 无需积分!无需积分!
在我们日常开发中,生成 PDF
文件是一项常见的需求。无论是生成单据、报表、发票还是其他文档,PDF
格式因其便捷的打印和跨平台支持而被广泛使用。本文将介绍如何在 Spring Boot
项目中使用 flying-saucer-pdf
和 Freemarker
来实现 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 Saucere
在GitHub
上的介绍,你会发现 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>¥${p.price}</td>
<td>¥${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>¥${total}</td>
</tr>
<tr>
<td><strong>税:</strong>
</td>
<td>¥${tax}</td>
</tr>
<tr>
<td><strong>总计</strong>
</td>
<td>¥${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 Saucer
和 Freemarker
实现 PDF
导出功能,并附加水印,并也演示了直接在后端编码形式生成PDF
。
通过 Freemarker
模板引擎生成 HTML
,再使用 Flying Saucer
将 HTML
转换为 PDF
,此方法灵活且易于扩展,可以根据业务需求生成复杂的 PDF
文档
如果本文对您有所帮助,希望 一键三连 给博主一点点鼓励,如果您有任何疑问或建议,请随时留言讨论!