目前最佳的HTML转PDF的方案,websocket调用chrome浏览器开发者工具使用chrome的headless模式实现,效果卓越,简单有效。使用JAVA搭配MAVEN实现项目示例。
前言
在Web开发和企业应用中,将HTML页面转换为PDF是一个常见需求。
市面上有很多工具可以实现这个功能,但多数要么依赖外部服务,要么需要额外安装复杂组件。
本文将分享一个纯Java实现的HTML转PDF解决方案,它通过WebSocket协议直接与Chrome浏览器通信,实现高质量的HTML到PDF转换。
语言、版本和工具依赖
语言:java代码
版本:jdk17(可下载源码之后进行降级)
依赖管理:maven
开源地址:chrome-html-to-pdf
特点
-
纯Java实现,无需额外依赖
-
高质量的渲染效果,支持现代化CSS特性
-
可以在任何安装了Chrome浏览器的环境中运行
-
提供简洁易用的API
技术原理
利用了Chrome提供的远程调试协议(Chrome DevTools Protocol,CDP),通过WebSocket与Chrome浏览器建立连接并进行通信。
当Chrome以特定参数启动时,会暴露一个WebSocket端点,允许外部程序像DevTools一样控制浏览器。
整个转换流程如下:
- 启动Chrome浏览器(无头模式)并获取WebSocket URL
- 通过WebSocket连接到Chrome DevTools Protocol
- 导航到指定的HTML文件
- 等待页面完全加载
- 使用Page.printToPDF命令生成PDF
- 保存PDF文件
这种方法的优势在于完全利用了Chrome的渲染引擎,确保转换后的PDF与浏览器中看到的内容保持一致,并支持所有现代CSS功能。
核心设计
该项目采用了模块化设计,主要分为以下几个组件:
1. HtmlToPdfConverter
public class HtmlToPdfConverter implements AutoCloseable {
*// 使用构建器模式创建实例*
public static Builder builder() {
return new Builder();
}
*// 核心转换方法*
public void convert(String *htmlFilePath*, String *outputPdfPath*, PdfOptions *options*) {
*// 转换实现*
}
*// 内部PdfOptions静态类...*
}
2. ChromeFinder
负责在系统中查找Chrome浏览器的可执行路径:
public class ChromeFinder {
public static String findChrome(String *userSpecifiedPath*) {
*// 寻找Chrome浏览器实现*
}
}
3. ChromeLauncher
负责启动Chrome浏览器,获取WebSocket URL:
public class ChromeLauncher {
public void launch(String *chromePath*, int *remoteDebuggingPort*) {
*// 启动Chrome实现*
}
public String getWebSocketDebuggerUrl() {
*// 获取WebSocket URL*
}
}
4. ChromeDevToolsClient
WebSocket客户端,负责与Chrome进行通信:
public class ChromeDevToolsClient extends WebSocketClient {
*// 发送命令实现*
public void navigateToUrl(String *url*) {
*// 导航到URL*
}
public void generatePdf(String *outputPath*, PdfOptions *options*) {
*// 生成PDF*
}
}
5. 异常处理设计
异常层次结构设计,便于调用者精确处理不同类型的错误:
public class HtmlToPdfException extends RuntimeException {
public static class ChromeNotFoundException extends HtmlToPdfException { ... }
public static class ConnectionException extends HtmlToPdfException { ... }
public static class PageNavigationException extends HtmlToPdfException { ... }
public static class PdfGenerationException extends HtmlToPdfException { ... }
}
实现过程
1. 查找Chrome浏览器
项目首先需要找到Chrome浏览器的可执行文件路径。这一步通过以下策略实现:
-
如果用户指定了路径,则使用用户指定的路径
-
否则,根据操作系统检查常见安装位置
-
如果仍未找到,尝试通过系统命令(where chrome.exe或which google-chrome)查找
2. 启动Chrome并获取WebSocket URL
这是整个项目中最关键的一环。Chrome必须以特定参数启动:
List<String> command = new ArrayList<>();
command.add(chromePath);
command.add("--headless"); *// 无头模式*
command.add("--disable-gpu");
command.add("--no-sandbox");
command.add("--remote-debugging-port=" + remoteDebuggingPort);
*// 其他参数...*
启动后,需要获取页面级别的WebSocket URL,因为Chrome的输出中提供的是浏览器级别的WebSocket URL,不能直接用于页面操作。
解决方案是使用HTTP API获取页面级URL:
URL url = new URL("http://localhost:" + debuggingPort + "/json/list");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
*// 获取并解析JSON响应,找到类型为"page"的标签页*
3. 建立WebSocket连接
使用Java-WebSocket库建立WebSocket连接:
public class ChromeDevToolsClient extends WebSocketClient {
public ChromeDevToolsClient(String *webSocketUrl*) {
super(URI.create(webSocketUrl));
*// 连接到WebSocket*
}
}
4. 发送命令和处理响应
ChromeDevToolsClient负责通过WebSocket发送命令并处理响应:
private JsonObject sendCommand(String method, Map<String, Object> params) {
*// 创建命令对象*
Map<String, Object> command = new HashMap<>();
command.put("id", id);
command.put("method", method);
command.put("params", params);
*// 发送命令*
String commandJson = gson.toJson(command);
send(commandJson);
*// 等待响应*
return future.get(TIMEOUT, TimeUnit.SECONDS);
}
5. 生成PDF
最后,使用Page.printToPDF命令生成PDF:
Map<String, Object> params = new HashMap<>();
params.put("landscape", options.isLandscape());
*// 其他PDF选项...*
JsonObject response = sendCommand("Page.printToPDF", params);
String base64Data = response.getAsJsonObject("result").get("data").getAsString();
byte[] pdfData = Base64.getDecoder().decode(base64Data);
*// 写入文件*
Files.write(new File(outputPath).toPath(), pdfData);
技术难点解决
1. WebSocket URL类型问题
项目中最关键的技术难点是获取正确类型的WebSocket URL。Chrome提供两种级别的WebSocket URL:
-
浏览器级别(/devtools/browser/…):只能控制浏览器本身
-
页面级别(/devtools/page/…):可以操作页面内容
最初,项目直接使用Chrome启动时提供的URL,这是浏览器级别的,导致无法执行页面操作。解决方法是通过HTTP API获取页面级别URL:
*// 使用/json/list API获取所有标签页*
*// 查找type=page的标签页并获取其webSocketDebuggerUrl*
2. 文件URL处理
另一个难点是正确处理本地文件URL。不同操作系统下,文件路径格式不同:
private String formatFileUrl(File file) {
String fileUrl = file.toURI().toURL().toString();
*// 处理Windows路径,确保格式正确*
if (File.separatorChar == '\\') {
if (!fileUrl.startsWith("file:///")) {
fileUrl = fileUrl.replace("file:/", "file:///");
}
}
return fileUrl;
}
3. 异步通信与命令响应匹配
WebSocket是异步通信,需要正确匹配请求和响应:
private final Map<Integer, CompletableFuture<JsonObject>> pendingRequests = new ConcurrentHashMap<>();
@Override
public void onMessage(String message) {
JsonObject response = gson.fromJson(message, JsonObject.class);
if (response.has("id")) {
int id = response.get("id").getAsInt();
CompletableFuture<JsonObject> future = pendingRequests.remove(id);
if (future != null) {
future.complete(response);
}
}
}
代码示例
基本用法
*// 使用默认设置*
try (HtmlToPdfConverter converter = HtmlToPdfConverter.builder().build()) {
*// 转换HTML到PDF*
converter.convert("input.html", "output.pdf");
}
自定义PDF选项
try (HtmlToPdfConverter converter = HtmlToPdfConverter.builder()
.chromePath("C:/Program Files/Google/Chrome/Application/chrome.exe")
.build()) {
*// 创建PDF选项*
HtmlToPdfConverter.PdfOptions options = HtmlToPdfConverter.PdfOptions.builder()
.landscape(true) *// 横向布局*
.printBackground(true) *// 打印背景*
.scale(1.2) *// 缩放比例*
.paperWidth(11.0) *// 纸张宽度(英寸)*
.paperHeight(8.5) *// 纸张高度(英寸)*
.marginTop(0.5) *// 上边距(英寸)*
.build();
*// 转换HTML到PDF,使用自定义选项*
converter.convert("input.html", "output.pdf", options);
}
完善的异常处理
try (HtmlToPdfConverter converter = HtmlToPdfConverter.builder().build()) {
converter.convert("input.html", "output.pdf");
} catch (HtmlToPdfException.ChromeNotFoundException *e*) {
logger.error("找不到Chrome浏览器: {}", e.getMessage());
} catch (HtmlToPdfException.ConnectionException *e*) {
logger.error("连接Chrome失败: {}", e.getMessage());
} catch (HtmlToPdfException.PageNavigationException *e*) {
logger.error("页面导航失败: {}", e.getMessage());
} catch (HtmlToPdfException.PdfGenerationException *e*) {
logger.error("PDF生成失败: {}", e.getMessage());
}
总结与思考
通过这个项目,实现了一个纯Java的HTML转PDF解决方案,具有以下优势:
-
高质量渲染:使用Chrome的Blink引擎进行渲染,支持最新的CSS特性
-
无需额外安装:只需系统中安装有Chrome浏览器即可
-
简洁的API:提供构建器模式的流畅API,易于使用
-
错误处理:精细的异常层次结构,方便调试
-
资源管理:实现AutoCloseable接口,确保资源正确关闭
可能的改进方向
-
网络URL支持:当前版本仅支持本地HTML文件,可以扩展支持网络URL
-
批量转换:优化多文件转换性能,复用Chrome实例
-
更多PDF选项:支持更多PDF生成选项,如页眉页脚、水印等
-
异步API:提供异步转换API,适用于高并发场景
技术启示
本项目展示了如何利用现有工具的API进行创新组合,而不是重新发明轮子。通过WebSocket和CDP协议,我们连接到世界上最强大的渲染引擎之一,从而避免了自己实现复杂的HTML/CSS渲染逻辑。
这种思路适用于许多其他场景:通过协议和API层面的集成,我们可以将现有工具的功能无缝整合到我们的应用中,创造出更强大、更可靠的解决方案。
> 项目源码已开源在GitHub: chrome-html-to-pdf,欢迎Star和贡献代码!
更多推荐



所有评论(0)