springboot生成pdf方案之dot/html/图片转pdf三种方式

文章目录

  • pdf生成方案
    • dot转pdf
    • html转pdf
      • openhtmltopdf
      • aspose-pdf
        • 实践
      • playwright
        • 实践
    • 图片转pdf
      • Apache PDFBox实践
  • 框架场景匹配
  • 后记

    前言:随着客户对报告审美的提升,需求也越来越五彩斑斓~ 原有的dot模板已经满足不了他们了!这篇文章主打列出各种方案及适用场景,带部分demo。

pdf生成方案

dot转pdf

自定义.dot文档,模板中插入书签占位,使用aspose-words转换为pdf
ps:这个收费=_=|| 公司之前有项目用了,所以没探索其他实现方案

html转pdf

探索了三个框架,openhtmltopdfaspose-pdfflying-saucer-pdf-openpdf,下面分别对这些方案进行描述。

openhtmltopdf

源码地址

明晃晃的优点:

  1. 开源&&免费

但有两个不得不忽视的缺点:

  1. 中文乱码,官网issue中有人提单了含有中文字符的html输出pdf有乱码 #129
    ,按照解决方案并不能修复,所以block了
  2. 仅仅支持简单的CSS

aspose-pdf

官网:Creating a complex PDF,虽然和aspose-words都是aspose家的,但他们分开收费!!!
优点:

  1. 内置14种字体 - 中文支持度非常高
  2. 支持加密/解密、数字签名、权限控制
  3. 文档完善、社区活跃度高

缺点:

  1. 贵!!!经费不足不考虑
  2. 对CSS3中部分样式不支持,例如aspect-ratio,需要后端一点点排查再让前端调整。。。 (太难了)
实践

注意: 这个库不是在maven中央仓库中管理,需要加一个仓库配置https://releases.aspose.com/java/repo/

<dependency><groupId>com.aspose</groupId><artifactId>aspose-pdf</artifactId><version>23.6</version>
</dependency>
import com.aspose.pdf.Document;
import com.aspose.pdf.HtmlLoadOptions;
import com.aspose.pdf.SaveFormat;public void generatePdf(String name) {	// 1. 准备数据模型Map<String, Object> data = new HashMap<>();data.put("name", name);data.put("date", LocalDate.now().toString());String html = freeMarkerService.getTemplate2String("report.ftl", data);try (FileOutputStream fileOutputStream = new FileOutputStream("E:\\test\\report.pdf");InputStream stream = new ByteArrayInputStream(html.getBytes("UTF-8"));) {// 加载静态资源HtmlLoadOptions loadOptions = new HtmlLoadOptions("src/main/resources/static/report");loadOptions.setEmbedFonts(true);Document document = new Document(stream, loadOptions);document.save(fileOutputStream, SaveFormat.Pdf);} catch (Exception e) {log.error("[generatePdf] html转pdf失败", e);}
}

playwright

由前端提供的html文件,里面包含的CSS样式太复杂了,没办法只能用webkit这种方式渲染样式才不会有大的偏差~
优点:

  1. 渲染质量ok,基本上和html展示一致
  2. 开源免费
  3. 跨平台支持,docker中也可运行

缺点:

  1. 初次运行要下载浏览器
  2. 资源消耗大,每个转换需要100-300M内存
  3. Java版是对Node.js版的封装
实践

maven

<dependency><groupId>com.microsoft.playwright</groupId><artifactId>playwright</artifactId><version>1.52.0</version>
</dependency>

业务代码(强制将输出A4纸张大小):

package com.lizzy.mp.service;import java.io.FileOutputStream;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;import org.springframework.stereotype.Service;import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserType;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.options.Margin;import lombok.extern.slf4j.Slf4j;@Service
@Slf4j
public class PlaywrightPdfService {@Resourceprivate FreeMarkerService freeMarkerService;private Playwright playwright;private Browser browser;@PostConstructpublic void init() {playwright = Playwright.create();browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(true).setArgs(Stream.of("--disable-dev-shm-usage").collect(Collectors.toList())));}public void generatePdf(String name) {// 1. 准备数据模型Map<String, Object> data = new HashMap<>();data.put("name", name);data.put("date", LocalDate.now().toString());String htmlContent = freeMarkerService.getTemplate2String("report2.ftl", data);try (Page page = browser.newPage();FileOutputStream fileOutputStream = new FileOutputStream("E:\\test\\report.pdf");) {page.setContent(htmlContent);byte[] bytes = page.pdf(new Page.PdfOptions().setMargin(new Margin().setTop("0cm").setBottom("0cm").setLeft("0cm").setRight("0cm")).setPrintBackground(true).setFormat("A4"));fileOutputStream.write(bytes);} catch (Exception e) {log.error("[generatePdf] HTML转PDF失败", e);}log.info("[generatePdf] HTML转PDF成功");}@PreDestroypublic void cleanup() {if (browser != null) {browser.close();}if (playwright != null) {playwright.close();}}
}

图片转pdf

      项目中前后端共用一个html模板,前端会有预览功能,于是乎讨论出一个方案:前端直接将html生成图片,后端将图片转成pdf,这样后端就不用care样式问题了!
      网上解决方案很多,作者只调研了Apache PDFBox。

Apache PDFBox实践

maven:

<dependency><groupId>org.apache.pdfbox</groupId><artifactId>pdfbox</artifactId><version>3.0.3</version>
</dependency>

业务代码,说明:

  • 方法convert中生成的pdf打开后50%展示都很大
  • 方法convertForA4中进行了限制,打开后100%还原
    ps:最根本的解决方法还是控制css样式为A4
package com.lizzy.mp.service;import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;import javax.imageio.ImageIO;import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.springframework.stereotype.Service;import lombok.extern.slf4j.Slf4j;@Service
@Slf4j
public class Image2PdfService {public void convert() {// 创建PDF文档try (PDDocument document = new PDDocument()) {// 加载图片PDImageXObject pdImage = PDImageXObject.createFromFile("E:\\report_page-0001.jpg", document);// 创建页面,大小与图片相同PDPage page = new PDPage(new PDRectangle(pdImage.getWidth(), pdImage.getHeight()));document.addPage(page);// 将图片写入PDFtry (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {contentStream.drawImage(pdImage, 0, 0);}// 保存PDFdocument.save("E:\\test\\report0.pdf");} catch (IOException e) {log.error("[convert] 图片转pdf失败,错误原因:{}", e.getMessage(), e);}}public void convertForA4() {String imagePath = "E:\\report_page-0001.jpg";String outputPdfPath = "E:\\test\\report0.pdf";try (PDDocument document = new PDDocument()) {// 读取图片BufferedImage image = ImageIO.read(new File(imagePath));if (image == null) throw new IOException("无法读取图片");PDImageXObject pdImage = PDImageXObject.createFromFile(imagePath, document);// 创建A4页面PDRectangle a4 = PDRectangle.A4;PDPage page = new PDPage(a4);document.addPage(page);// 原始图片尺寸float imageWidth = image.getWidth();float imageHeight = image.getHeight();// A4尺寸float pageWidth = a4.getWidth();float pageHeight = a4.getHeight();// 缩放比例(等比缩放)float scale = Math.min(pageWidth / imageWidth, pageHeight / imageHeight);float drawWidth = imageWidth * scale;float drawHeight = imageHeight * scale;// 居中坐标float x = (pageWidth - drawWidth) / 2;float y = (pageHeight - drawHeight) / 2;// 写入图像try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {contentStream.drawImage(pdImage, x, y, drawWidth, drawHeight);}// 保存PDFdocument.save(outputPdfPath);} catch (IOException e) {System.err.println("图片转PDF失败: " + e.getMessage());e.printStackTrace();}}
}

框架场景匹配

框架名称CSS样式支持度是否开源中文支持使用难易体积大小说明
Aspose.word中等❌,收费高
试用版有水印
简单50+MB
Aspose.pdfCSS3(动画等不支持)❌,收费高
试用版有水印
中等
openhtmltopdfCSS2.1(基本支持)需显示引入字体简单小,3~5MB
playwright非常高浏览器原生支持需运行浏览器依赖大,依赖Chromium
apache pdfbox----只使用图像转pdf,无需控制样式

后记

因项目背景,对报告pdf的生成要求蛮高,所以得不停尝试各种解决方案,推荐获取思路的网址~

  • 想找个生成PDF的库或者解决方案,不要Aspose的(from www.reddit.com),虽是C#解决方案,但道理相通
  • Pdf generation(from www.reddit.com)

ps:思考要记录,不然会忘记~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/88464.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/88464.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

前端开发—全栈开发

全栈开发者在面试前端或全栈岗位时&#xff0c;自我介绍需要巧妙融合“技术广度”与“岗位针对性”&#xff0c;避免成为泛泛而谈的“样样通样样松”。以下是结合面试官关注点和全栈特性的专业介绍策略&#xff1a;&#x1f9e0; 一、自我介绍的核心理念 突出全栈优势&#xff…

Redis生产环境过期策略配置指南:务实落地,避免踩坑

在生产环境中合理配置Redis过期策略是保障系统稳定性和内存效率的关键。以下配置建议基于实战经验&#xff0c;避免理论堆砌&#xff0c;直击核心要点&#xff1a;一、核心策略配置&#xff1a;惰性删除 定期删除&#xff08;默认已启用&#xff09;无需额外配置&#xff1a;R…

Ubuntu 20.04 安装 Node.js 20.x、npm、cnpm 和 pnpm 完整指南

&#x1f310; Ubuntu 20.04 安装 Node.js 20.x、npm、cnpm 和 pnpm 完整指南 &#x1f680; 在本文中&#xff0c;我们将介绍如何在 Ubuntu 20.04 上安装 Node.js 20.x&#xff0c;以及如何安装 npm、cnpm 和 pnpm 来提高开发效率 ⚡。1️⃣ 安装 Node.js 20.x 为了确保使用最…

【时时三省】(C语言基础)通过指针引用数组元素

山不在高&#xff0c;有仙则名。水不在深&#xff0c;有龙则灵。 ----CSDN 时时三省引用一个数组元素&#xff0c;可以用下面两种方法&#xff1a;( 1 )下标法&#xff0c;如a[i]形式&#xff1b;( 2 )指针法&#xff0c;如* ( a i )或* ( p i )。其中a是数组名&#xff0c;p…

Guava LoadingCache

LoadingCache 是 Google Guava 库提供的一个高级缓存实现&#xff0c;它通过自动加载机制简化了缓存使用模式。核心特性自动加载机制当缓存未命中时&#xff0c;自动调用指定的 CacheLoader 加载数据线程安全&#xff1a;并发请求下&#xff0c;相同key只会加载一次灵活的过期策…

基于LSTM-GRU模型的黄金价格动态监测:关税政策与美指的量化关联研究

摘要&#xff1a;本文通过BERT-Large模型对关税政策进行语义解析&#xff0c;结合LSTM-GRU混合模型、DCC-GARCH动态相关性模型及蒙特卡洛情景分析&#xff0c;量化解析7月11日黄金价格异动背后的三大驱动因子——政策冲击、美元指数压制与美联储政策不确定性&#xff0c;提供AI…

V少JS基础班之第七弹

文章目录一、 前言二、本节涉及知识点三、重点内容1、prototype2、constructor3、中场回顾&总结4、__ proto__5、第二次中场回顾&总结6、原型链6、第三次中场回顾&总结7、原型链中的奇点一、 前言 第七弹内容是原型链。网络上原型链的资料很多。但是我看了很多篇&…

Nuxt3自动打包及自动修改端口号脚本

Nuxt3自动打包及自动修改端口号脚本技术文章大纲 背景与需求 Nuxt3作为现代Vue框架&#xff0c;开发中常需处理打包部署和端口配置问题。自动化脚本可提升效率&#xff0c;减少手动操作错误。 实现自动打包 利用Nuxt3内置命令结合Node.js脚本实现自动化构建。通过npm run build…

红海云国资案例之多层级工贸集团的一体化HR平台建设实战

在中国经济迈向高质量发展的进程中&#xff0c;国有企业作为重要的经济支柱和行业引领者&#xff0c;正面临着数字化转型的深刻变革。F集团作为G市首家实现工贸一体化运营的大型企业&#xff0c;位列中国轻工业百强&#xff0c;其在人力资源数字化转型中的探索和实践&#xff0…

TCP详解——流量控制、滑动窗口

目录 流量控制 滑动窗口 丢包重传 情况一&#xff1a;数据到达&#xff0c;应答丢失 情况二&#xff1a;数据包丢失 流量控制 TCP协议会根据接收端的缓冲区大小来调整发送速度&#xff0c;剩余空间多则发送速度快&#xff0c;否则降低发送速度 接收端将⾃⼰可以接收的缓…

C#高级特性面试问题的详细分析,涵盖核心概念、应用场景和最佳实践

序列化与反序列化 1. 什么是序列化和反序列化&#xff1f;用途是什么&#xff1f; // 序列化示例 Person person new Person { Name "Alice", Age 30 }; string json JsonSerializer.Serialize(person); // 序列化为JSON// 反序列化示例 Person deserialized Js…

【电脑】内存的基础知识

内存&#xff08;Memory&#xff09;是计算机中用于临时存储数据和程序的地方&#xff0c;它直接影响到系统的运行速度和性能。以下是关于内存的详细知识&#xff1a;1. 内存类型常见的内存类型包括以下几个主要种类&#xff1a;SDRAM (Synchronous Dynamic Random Access Memo…

Java---IDEA

IDEA概述 IDEA&#xff1a;全称Intellij IDEA&#xff0c;是用于Java语言开发的集成开发环境 集成环境&#xff1a;把代码编写&#xff0c;编译&#xff0c;运行&#xff0c;调试等多种功能综合到一起的开发工具 下载与安装 下载&#xff1a;IntelliJ IDEA – the IDE for …

【每日刷题】x 的平方根

69. x 的平方根 - 力扣&#xff08;LeetCode&#xff09; 方法一&#xff1a;暴力 从0开始遍历&#xff0c;直到 ans*ans > x 为止&#xff0c;这时ans-1就是答案。需要注意可能会爆int&#xff0c;所以ans要开为long&#xff0c;最后再转换为int。 class Solution {publ…

C#元组:从基础到实战的全方位解析

C#元组&#xff1a;从基础到实战的全方位解析 在 C# 编程中&#xff0c;元组&#xff08;Tuple&#xff09;是一种轻量级的数据结构&#xff0c;用于临时存储多个不同类型的元素。无论是方法返回多个值、LINQ 查询中的临时投影&#xff0c;还是简化数据传递&#xff0c;元组都以…

Django母婴商城项目实践(二)

2、母婴商城项目环境配置 环境配置: Python3.12 解释器Pycharm Professional 2025.1 编辑器Django 4.2(或 Django 5.x)MySQL 8.0.28 数据库 1、Django框架 介绍 Django是一个高级的Python Web应用框架,可以快速开发安全和可维护的网站。由经验丰富的开发者构建,Django负责…

Go语言的Channel通道的含义。区分缓冲通道和非缓冲通道,并讨论通道的发送、接收、关闭以及如何安全地从已关闭的通道读取数据。

非缓冲通道&#xff1a;非缓冲通道在确定时没有声明容量大小&#xff0c;发送和接收操作会同步阻塞&#xff0c;直到另一端准备好。发送方和接收方必须同时就绪才能完成数据交换&#xff0c;否则会阻塞。常用于goroutine之间的同步通信。缓冲通道&#xff1a;缓冲通道在确定时就…

tensor

&#x1f609;如果您想用jupyter notebook跑我的笔记&#xff0c;可以在下面获取ipynb版本 &#x1f60a;麻烦给个免费的star&#x1f618; ❤️主包也更建议这种形式&#xff0c;上面的笔记也更加全面&#xff0c;每一步都有直观的输出 文章目录&#x1f4da; PyTorch张量操作…

STM32-DAC数模转换

DAC数模转换&#xff1a;将数字信号转换成模拟信号特性&#xff1a;2个DAC转换器每个都拥有一个转换通道8位或12位单调输出&#xff08;8位右对齐&#xff1b;12位左对齐右对齐&#xff09;双ADC通道同时或者分别转换外部触发中断电压源控制部分&#xff08;外部触发3个APB1&am…

前后端集合如何传递

前端vue后端rest风格&#xff1a;1.路径传参&#xff08;参数必传&#xff09;&#xff0c;通过pathvarible注解后端&#xff1a;DeleteMapping("/{YYIDs}")public R<Void> remove(NotEmpty(message "主键不能为空")PathVariable String[] YYIDs) {…