文章目录
前言:随着客户对报告审美的提升,需求也越来越五彩斑斓~ 原有的dot模板已经满足不了他们了!这篇文章主打列出各种方案及适用场景,带部分demo。
pdf生成方案
dot转pdf
自定义.dot
文档,模板中插入书签占位,使用aspose-words
转换为pdf
ps:这个收费=_=|| 公司之前有项目用了,所以没探索其他实现方案
html转pdf
探索了三个框架,openhtmltopdf
、aspose-pdf
、flying-saucer-pdf-openpdf
,下面分别对这些方案进行描述。
openhtmltopdf
明晃晃的优点:
- 开源&&免费
但有两个不得不忽视的缺点:
- 中文乱码,官网issue中有人提单了含有中文字符的html输出pdf有乱码 #129
,按照解决方案并不能修复,所以block了 - 仅仅支持简单的CSS
aspose-pdf
官网:Creating a complex PDF,虽然和aspose-words
都是aspose家的,但他们分开收费!!!
优点:
- 内置14种字体 - 中文支持度非常高
- 支持加密/解密、数字签名、权限控制
- 文档完善、社区活跃度高
缺点:
- 贵!!!经费不足不考虑
- 对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这种方式渲染样式才不会有大的偏差~
优点:
- 渲染质量ok,基本上和html展示一致
- 开源免费
- 跨平台支持,docker中也可运行
缺点:
- 初次运行要下载浏览器
- 资源消耗大,每个转换需要100-300M内存
- 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 {
@Resource
private FreeMarkerService freeMarkerService;
private Playwright playwright;
private Browser browser;
@PostConstruct
public 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成功");
}
@PreDestroy
public 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);
// 将图片写入PDF
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
contentStream.drawImage(pdImage, 0, 0);
}
// 保存PDF
document.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);
}
// 保存PDF
document.save(outputPdfPath);
} catch (IOException e) {
System.err.println("图片转PDF失败: " + e.getMessage());
e.printStackTrace();
}
}
}
框架场景匹配
框架名称 | CSS样式支持度 | 是否开源 | 中文支持 | 使用难易 | 体积大小 | 说明 |
---|---|---|---|---|---|---|
Aspose.word | 中等 | ❌,收费高 试用版有水印 |
高 | 简单 | 50+MB | |
Aspose.pdf | CSS3(动画等不支持) | ❌,收费高 试用版有水印 |
高 | 中等 | 大 | |
openhtmltopdf | CSS2.1(基本支持) | ✅ | 需显示引入字体 | 简单 | 小,3~5MB | |
playwright | 非常高 | ✅ | 浏览器原生支持 | 需运行浏览器依赖 | 大,依赖Chromium | |
apache pdfbox | - | ✅ | - | - | - | 只使用图像转pdf,无需控制样式 |
后记
因项目背景,对报告pdf的生成要求蛮高,所以得不停尝试各种解决方案,推荐获取思路的网址~
ps:思考要记录,不然会忘记~