😄 19年之后由于某些原因断更了三年,23年重新扬帆起航,推出更多优质博文,希望大家多多支持~
🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志
🎐 个人CSND主页——Micro麦可乐的博客
🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战
🌺《RabbitMQ》专栏主要介绍使用JAVA开发RabbitMQ的系列教程,从基础知识到项目实战
🌸《设计模式》专栏以实际的生活场景为案例进行讲解,让大家对设计模式有一个更清晰的理解
💕《Jenkins实战》专栏主要介绍Jenkins+Docker的实战教程,让你快速掌握项目CI/CD,是2024年最新的实战教程
🌞《Spring Boot》专栏主要介绍我们日常工作项目中经常应用到的功能以及技巧,代码样例完整
如果文章能够给大家带来一定的帮助!欢迎关注、评论互动~
Spring Boot 集成 OpenPDF 和 Freemarker 实现 PDF 导出功能
- 前言
- 概述
- 实战开始
- ❶ 项目初始化
- ❷ 配置 Freemarker
- ❸ 创建 HTML 模板
- ❹ 基于HTML模版 PDF 生成逻辑
- ❺ 基于后端编码形式生成
- ❻ 创建 Controller
- ❼ 测试和运行
- 一点点建议
- 总结
前言
本文对应代码下载地址: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: .ftlcharset: utf-8template-loader-path: classpath:/templates/expose-request-attributes: trueexpose-session-attributes: trueexpose-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 {@Autowiredprivate FreeMarkerConfigurer freeMarkerConfigurer;public byte[] generatePdf(Map<String, Object> data) throws Exception {// 生成HTMLString 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() {@Overridepublic 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.2fgs.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 {@Autowiredprivate 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() {@Overridepublic 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.2fgs.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 {@Autowiredprivate PdfService pdfService;@Autowiredprivate 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
文档
如果本文对您有所帮助,希望 一键三连 给博主一点点鼓励,如果您有任何疑问或建议,请随时留言讨论!