😄 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-8template-loader-path: classpath:/templates/
expose-request-attributes:trueexpose-session-attributes:trueexpose-spring-macro-helpers:true
❸ 创建 HTML 模板
在
src/main/resources/templates
目录下创建一个
Freemarker
模板文件
template.ftl
<!DOCTYPEhtml><html><head><metacharset="utf-8"/><title>测试导出单据模版</title><linkhref="https://demo.ruoyi.vip/css/bootstrap.min.css?v=3.3.7"rel="stylesheet"type="text/css"/><linkhref="https://demo.ruoyi.vip/css/style.min.css?v=20210831"rel="stylesheet"/><linkhref="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><bodyclass="gray-bg"><div><divclass="row"><divclass="col-sm-12"><divclass="ibox-content"><divclass="row"><divclass="col-sm-6 text-right"><h4>单据编号:</h4><h4class="text-navy">H+-000567F7-00</h4><address><strong>${companyName}</strong><br/>
${address}<br/><abbrtitle="Phone">总机:</abbr> ${tel}
</address><p><span><strong>日期:</strong> 2014-11-11</span></p></div></div><divclass="table-responsive m-t"><tableclass="invoice-table"style="width: 100%;line-height: 60px"><thead><tr><th>图片</th><th>清单</th><th>数量</th><th>单价</th><th>总价</th></tr></thead><tbody><#ifproducts??&&(products?size> 0)>
<#listproductsasp><tr><td><imgsrc="${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 --><tableclass="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><divclass="well m-t"><strong>注意:</strong> 请保存好单据</div></div></div></div></div></body></html>
❹ 基于HTML模版 PDF 生成逻辑
创建一个
PdfService
类,用于生成 PDF 文件
packagecom.toher.project.openpdf;importcom.lowagie.text.*;importcom.lowagie.text.Font;importcom.lowagie.text.pdf.*;importfreemarker.cache.ClassTemplateLoader;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importorg.springframework.ui.freemarker.FreeMarkerTemplateUtils;importorg.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;importorg.xhtmlrenderer.pdf.ITextFontResolver;importorg.xhtmlrenderer.pdf.ITextRenderer;importjava.awt.*;importjava.io.ByteArrayOutputStream;importjava.util.Map;@ServicepublicclassPdfService{@AutowiredprivateFreeMarkerConfigurer freeMarkerConfigurer;publicbyte[]generatePdf(Map<String,Object> data)throwsException{// 生成HTMLString html =FreeMarkerTemplateUtils.processTemplateIntoString(
freeMarkerConfigurer.getConfiguration().getTemplate("template.ftl"), data);ByteArrayOutputStream out =newByteArrayOutputStream();ITextRenderer renderer =newITextRenderer();//加载/resource/static/font的字体ClassTemplateLoader classTemplateLoader =newClassTemplateLoader(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 =newFont(bfChinese,10,Font.UNDEFINED,Color.BLACK);
writer.setPageEvent(newPdfPageEventHelper(){@OverridepublicvoidonEndPage(PdfWriter writer,Document document){PdfContentByte waterMar = writer.getDirectContentUnder();String text ="Micro麦可乐";addTextFullWaterMark(waterMar, text, bfChinese);}});
renderer.finishPDF();return out.toByteArray();}publicstaticvoidaddTextFullWaterMark(PdfContentByte waterMar,String text,BaseFont bfChinese){
waterMar.beginText();PdfGState gs =newPdfGState();// 设置填充字体不透明度为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
packagecom.toher.project.openpdf;importcom.lowagie.text.Font;importcom.lowagie.text.*;importcom.lowagie.text.Image;importcom.lowagie.text.alignment.HorizontalAlignment;importcom.lowagie.text.alignment.VerticalAlignment;importcom.lowagie.text.html.simpleparser.HTMLWorker;importcom.lowagie.text.pdf.*;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importorg.springframework.ui.freemarker.FreeMarkerTemplateUtils;importorg.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;importorg.xhtmlrenderer.pdf.ITextRenderer;importjava.awt.*;importjava.io.ByteArrayOutputStream;importjava.io.IOException;importjava.io.InputStream;importjava.io.StringReader;importjava.util.Map;@ServicepublicclassOpenPdfService{@AutowiredprivateFreeMarkerConfigurer freeMarkerConfigurer;publicbyte[]generatePdf(Map<String,Object> data)throwsException{ByteArrayOutputStream out =newByteArrayOutputStream();// 创建PDF文档Document document =newDocument();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 =newFont(bfChinese,10,Font.UNDEFINED,Color.BLACK);//设置水印
writer.setPageEvent(newPdfPageEventHelper(){@OverridepublicvoidonEndPage(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 =newString[]{"清单","数量","单价","总价"};Table table =newTable(tableTitle.length);
table.setWidths(newfloat[]{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 =newChunk(tableTitle[i], docFont);}else{
chunk =newChunk(row +"行 模拟数据"+ i, docFont);}// 建立单元格Cell cell =newCell(chunk);// 设置水平对齐
cell.setHorizontalAlignment(HorizontalAlignment.CENTER);// 设置垂直对齐
cell.setVerticalAlignment(VerticalAlignment.CENTER);
table.addCell(cell);}}
document.add(table);/**
* 02 写入图片
*/byte[] byteArray =newbyte[0];InputStream inputStream =this.getClass().getResourceAsStream("/static/img/test.png");if(inputStream !=null){
byteArray =newbyte[inputStream.available()];
inputStream.read(byteArray);}Image image =Image.getInstance(byteArray);// 图片进行缩放
image.scaleAbsolute(200,200);
document.add(image);/**
* 03 写入html内容
*/HTMLWorker htmlWorker =newHTMLWorker(document);String html ="<p style='color: crimson'>Hello, micro</p>";
htmlWorker.parse(newStringReader(html);// 关闭文档
document.close();return out.toByteArray();}publicstaticvoidaddTextFullWaterMark(PdfContentByte waterMar,String text,BaseFont bfChinese){
waterMar.beginText();PdfGState gs =newPdfGState();// 设置填充字体不透明度为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
的请求
packagecom.toher.project.openpdf;importcom.lowagie.text.DocumentException;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.http.HttpHeaders;importorg.springframework.http.MediaType;importorg.springframework.http.ResponseEntity;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestParam;importorg.springframework.web.bind.annotation.RestController;importjava.io.IOException;importjava.math.BigDecimal;importjava.util.ArrayList;importjava.util.HashMap;importjava.util.List;importjava.util.Map;@RestControllerpublicclassPdfController{@AutowiredprivatePdfService pdfService;@AutowiredprivateOpenPdfService openPdfService;/**
* 采用flying-saucer-pdf html转pdf
* @return
*/@GetMapping("/generate-pdf")publicResponseEntity<byte[]>generatePdf(){// 模拟数据库查询结果Map<String,Object> data =newHashMap<>();
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 =newArrayList<>();ProductVo productVo =newProductVo();
productVo.setProductImg("https://demo.ruoyi.vip/img/profile.jpg");
productVo.setProductName("尚都比拉2013冬装新款女装 韩版修身呢子大衣 秋冬气质羊毛呢外套");
productVo.setQuantity(1);
productVo.setPrice(newBigDecimal("26"));
productVo.setTotal(productVo.getPrice().multiply(productVo.getPrice()));
products.add(productVo);ProductVo productVo1 =newProductVo();
productVo1.setProductImg("https://demo.ruoyi.vip/img/profile.jpg");
productVo1.setProductName("11*11夏娜 新款斗篷毛呢外套 女秋冬呢子大衣 韩版大码宽松呢大衣");
productVo1.setQuantity(2);
productVo1.setPrice(newBigDecimal("80"));
productVo1.setTotal(productVo.getPrice().multiply(productVo.getPrice()));
products.add(productVo1);ProductVo productVo2 =newProductVo();
productVo2.setProductImg("https://demo.ruoyi.vip/img/profile.jpg");
productVo2.setProductName("2013秋装 新款女装韩版学生秋冬加厚加绒保暖开衫卫衣 百搭女外套");
productVo2.setQuantity(3);
productVo2.setPrice(newBigDecimal("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 =newHttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDispositionFormData("attachment","example.pdf");returnResponseEntity.ok().headers(headers).body(pdfBytes);}/**
* 采用openpdf 生成pdf
* @return
*/@GetMapping("/generate-openpdf")publicResponseEntity<byte[]>generateOpenPdf(){byte[] pdfBytes =null;try{
pdfBytes = openPdfService.generatePdf();}catch(Exception e){
e.printStackTrace();}HttpHeaders headers =newHttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDispositionFormData("attachment","example.pdf");returnResponseEntity.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
文档
如果本文对您有所帮助,希望 一键三连 给博主一点点鼓励,如果您有任何疑问或建议,请随时留言讨论!
版权归原作者 Micro麦可乐 所有, 如有侵权,请联系我们删除。