最近接到一个需求:做一个简单的打印功能,总共就三个小点:
- HTML 要能显示在浏览器上
- 要能在电脑上打印
- 要生成 PDF 给小程序用
重点是,这三个地方的显示效果要求完全一致。
最开始的想法很简单:以浏览器显示的效果为标准调整 HTML,电脑打印直接用浏览器自带的打印功能。既然都是浏览器渲染,样式不会有多大出入,即使有细微差别也容易调整。
麻烦出在小程序的 PDF 上。小程序本身限制较多且面向用户,整体思路还是要先把 PDF 保存到服务器,小程序只负责下载。那能不能在 PC 浏览器打印时调用“另存为 PDF”,后台静默把文件上传到服务器?这样就不需要额外写生成 PDF 的接口了。
可是仔细调研后发现,浏览器为了用户隐私与安全,禁止后台静默调用打印接口——大概是防止某些网页未经允许擅自打印、浪费纸张吧。唯一可行的办法是让用户自己另存为 PDF,再自行上传,这种方案显然不可能通过。
于是,只能自己生成 PDF。
常规 PDF 生成方案
使用 java 生成 PDF 的常规方法有以下几种:
Apache PDFBox
需要完全用 Java 代码去构造样式,对于要求样式高度一致、且工作量较大的场景,并不适用。下面是一段使用 PDFBox 的示例代码:
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
contentStream.beginText();
contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12);
contentStream.newLineAtOffset(100, 700);
contentStream.showText("这是一个使用Apache PDFBox生成的PDF文件。");
contentStream.endText();
} catch (IOException e) {
e.printStackTrace();
}
iText (pdfHTML)
iText 支持从 HTML 直接转换为 PDF,看上去完美契合需求。于是立刻上手试用,代码很简洁,传入 HTML 就能输出 PDF 文件:
public void createPdf(String html, HttpServletResponse response) throws Exception {
// 转换 HTML to PDF
PdfWriter writer = new PdfWriter(response.getOutputStream());
PdfDocument pdfDocument = new PdfDocument(writer);
// 设置PDF大小
pdfDocument.setDefaultPageSize(PageSize.A4);
// 设置中文
ConverterProperties converterProperties = new ConverterProperties();
FontProvider fontProvider = new DefaultFontProvider(false, true);
FontProgram fontProgram = FontProgramFactory.createFont("font/msyh.ttf");
fontProvider.addFont(fontProgram);
converterProperties.setFontProvider(fontProvider);
// html转换PDF
HtmlConverter.convertToPdf(html, pdfDocument, converterProperties);
// 关闭
pdfDocument.close();
}
测试几个简单例子都正常,中文乱码也很快解决。可一旦换上正式数据,打印出来的 PDF 样式就乱得一塌糊涂。深入研究才发现,iText 只完整支持 CSS2.1,对 CSS3 的支持非常有限,Flexbox/Grid 这类现代布局完全不被识别。而项目的 HTML 早已写好并上线使用,现在再按照它的标准去改动根本不现实,强行改还会导致数据样式前后不一致的风险。
把浏览器塞进项目里
问题又绕回浏览器打印——要想样式和 HTML 显示一模一样,终究还得依赖浏览器渲染引擎。但浏览器只允许用户手动调用打印,怎么破?
这时发现了一个好东西:Playwright,微软出品的自动化测试框架。把它引入项目:
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.40.0</version>
</dependency>
它的原理就是直接把一个真实的浏览器装进了服务器:启动一个无头 Chromium 实例,这样就能在服务端调用打印功能,渲染出来的效果和用户浏览器看到的完全一致。示例代码如下:
public void generatePdfFromUrl() {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions());
Page page = browser.newPage();
// 直接加载 HTML 字符串
page.setContent("<div class='html' style='page-break-after: always;'>...</div>");
// 等待静态资源(如图片、字体)加载完成
page.waitForLoadState(LoadState.NETWORKIDLE);
// 生成 PDF 文件流或保存到本地
page.pdf(new Page.PdfOptions()
.setPath(Paths.get("output.pdf"))
.setFormat("A4")
.setPrintBackground(true) // 保留 CSS 背景颜色和图片
.setMargin(new Margin().setTop("10mm").setBottom("10mm")));
browser.close();
}
}
这种方式并非没有缺点,最大的问题就是“重”:首次运行时,Playwright 会自动下载 Chromium 内核到临时目录(也可以提前在打包时下载好);每次运行时都会占用相当多的 CPU 和内存,相当于在服务器上开了一个浏览器,启动速度也慢,达到秒级。在我电脑上实测,初始化大约 5 秒,实际打印 2 秒左右。因此,在生产环境中,这种方案只适用于低频、可接受一定延迟的场景,并且最好控制单线程运行,以免内存或 CPU 溢出。
但它重归重,最终打印效果无可挑剔,CSS 各种样式、图片、甚至 JavaScript 执行后的动态内容都能完美呈现——浏览器支持的,它都支持。刚好我的业务场景访问频率不高,简直完美契合。
两种技术对比
最后对比一下 iText 与浏览器方案(Playwright)的特性差异:
| 特性 |
iText (pdfHTML) |
Playwright (Browser-based) |
| 技术本质 |
纯 Java 库,解析 HTML 并映射到 PDF 坐标 |
驱动真实浏览器 (Chromium) 进行渲染 |
| CSS 支持 |
有限(CSS2.1,部分 CSS3,不支持 Flexbox/Grid) |
完美支持,浏览器能看到的就能渲染 |
| JS 执行 |
不支持(无法运行 Vue/React 或图表脚本) |
支持,可等待数据加载或动画完成后生成 |
| 生成速度 |
极快(毫秒级) |
较慢(需要启动浏览器进程,秒级) |
| 资源消耗 |
低(纯内存操作) |
高,需大量 CPU 和内存 |
| 部署成本 |
低,只需引入 Jar 包 |
高,需安装 Chromium 或依赖其驱动 |
两者各有优势,选择哪个方案,取决于实际场景对样式一致性、性能和部署复杂度的取舍。
如果你也在寻找 HTML 转 PDF 的解决方案,或者想探讨更多 Java 生成 PDF 的实战经验,欢迎到 云栈社区 与开发者们一起交流。
今日收获:HTML 转 PDF 解决方案再 +1。