前言

在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一样控制浏览器。

整个转换流程如下:

  1. 启动Chrome浏览器(无头模式)并获取WebSocket URL
  2. 通过WebSocket连接到Chrome DevTools Protocol
  3. 导航到指定的HTML文件
  4. 等待页面完全加载
  5. 使用Page.printToPDF命令生成PDF
  6. 保存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解决方案,具有以下优势:

  1. 高质量渲染:使用Chrome的Blink引擎进行渲染,支持最新的CSS特性

  2. 无需额外安装:只需系统中安装有Chrome浏览器即可

  3. 简洁的API:提供构建器模式的流畅API,易于使用

  4. 错误处理:精细的异常层次结构,方便调试

  5. 资源管理:实现AutoCloseable接口,确保资源正确关闭

可能的改进方向

  1. 网络URL支持:当前版本仅支持本地HTML文件,可以扩展支持网络URL

  2. 批量转换:优化多文件转换性能,复用Chrome实例

  3. 更多PDF选项:支持更多PDF生成选项,如页眉页脚、水印等

  4. 异步API:提供异步转换API,适用于高并发场景

技术启示

本项目展示了如何利用现有工具的API进行创新组合,而不是重新发明轮子。通过WebSocket和CDP协议,我们连接到世界上最强大的渲染引擎之一,从而避免了自己实现复杂的HTML/CSS渲染逻辑。

这种思路适用于许多其他场景:通过协议和API层面的集成,我们可以将现有工具的功能无缝整合到我们的应用中,创造出更强大、更可靠的解决方案。


> 项目源码已开源在GitHub: chrome-html-to-pdf,欢迎Star和贡献代码!

更多推荐