在 Java 生態中,iText 雖是 PDF 生成的首選開源庫,但面對數百張業務表單,為每張設計導出模板顯然不現實。wkhtmltopdf 基於 WebKit 渲染引擎,能直接將現成的 HTML 頁面(含 CSS、JS)高保真轉為 PDF,成為「以網頁代模板」的最佳替代方案。本文從原理、安裝、命令行調用、到生產級 Java 封裝類,全鏈路解析落地實踐。
# Windows 示例 wkhtmltopdf.exe https://www.cnblogs.com D:/cnblogs.pdf # 指定頁眉、邊距、頁碼 wkhtmltopdf --header-center "報表" --header-line --margin-top 2cm https://example.com report.pdf
核心思路:Runtime.getRuntime().exec() 啟動進程,必須單獨線程消費 stdout/stderr,否則緩衝區填滿導致進程掛起。
public class StreamGobbler extends Thread {
private final InputStream is;
public StreamGobbler(InputStream is) { this.is = is; }
@Override
public void run() {
try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
log.debug("[wkhtmltopdf] {}", line); // 接入 SLF4J
}
} catch (IOException e) {
log.error("流讀取異常", e);
}
}
}
public class HtmlToPdfConverter {
// wkhtmltopdf 可執行文件路徑(建議通過配置文件注入)
private static final String WKHTMLTOPDF_PATH = "D:/wkhtmltopdf/bin/wkhtmltopdf.exe";
/**
* HTML 轉 PDF
* @param srcPath 源 HTML 路徑(支持本地文件、HTTP/HTTPS URL)
* @param destPath 目標 PDF 保存路徑
* @return 轉換成功返回 true
*/
public static boolean convert(String srcPath, String destPath) {
File destFile = new File(destPath);
File parentDir = destFile.getParentFile();
if (!parentDir.exists() && !parentDir.mkdirs()) {
log.error("無法創建目標目錄: {}", parentDir);
return false;
}
// 構建命令參數
List cmd = new ArrayList<>();
cmd.add(WKHTMLTOPDF_PATH);
cmd.add("--header-line"); // 頁眉底線
cmd.add("--header-center"); cmd.add("機密報表"); // 頁眉居中內容
cmd.add("--header-spacing"); cmd.add("10"); // 頁眉與正文間距
cmd.add("--footer-right"); cmd.add("[page]/[topage]"); // 頁腳頁碼
cmd.add("--encoding"); cmd.add("UTF-8"); // 關鍵:防中文亂碼
cmd.add("--enable-javascript"); // 支持 JS 渲染
cmd.add("--javascript-delay"); cmd.add("1000"); // 等待 JS 執行 1s
cmd.add(srcPath);
cmd.add(destPath);
try {
ProcessBuilder pb = new ProcessBuilder(cmd);
pb.redirectErrorStream(false); // 分離錯誤流
Process process = pb.start();
// 異步消費輸出流與錯誤流
new StreamGobbler(process.getInputStream()).start();
new StreamGobbler(process.getErrorStream()).start();
int exitCode = process.waitFor();
if (exitCode != 0) {
log.error("wkhtmltopdf 退出碼非 0: {}", exitCode);
return false;
}
return true;
} catch (IOException | InterruptedException e) {
log.error("轉換過程異常", e);
Thread.currentThread().interrupt();
return false;
}
}
}
| 參數 | 說明 | 示例 |
|---|---|---|
| --page-size | 紙張尺寸 | A4, Letter, Legal |
| --orientation | 頁面方向 | Portrait (默認), Landscape |
| --margin-top/bottom/left/right | 四邊邊距 | 10mm, 2cm, 0.5in |
| --header-html | 自定義 HTML 頁眉 | --header-html header.html |
| --footer-html | 自定義 HTML 頁腳 | --footer-html footer.html |
| --toc | 生成目錄 | --toc --toc-header-text "目錄" |
| --disable-javascript | 禁用 JS(提速/安全) | --disable-javascript |
| --no-background | 不打印背景色/圖 | --no-background |
| --dpi | 分辨率 | 300 (高質量打印) |
在 --header-center、--footer-right 等參數中可使用:
[page] 當前頁碼[topage] 總頁數[section] 當前一級標題[subsection] 當前二級標題[date] [time] 系統日期時間[webpage] 源頁面 URLyum install wqy-zenhei-fonts 或拷貝 .ttf 到 /usr/share/fonts 並 fc-cache -fv)。--javascript-delay 與 --load-error-handling ignore,並在 Java 層設置 process.waitFor(timeout, TimeUnit.SECONDS)。/tmp 或指定 --user-style-sheet 避免殘留。| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| wkhtmltopdf | HTML/CSS/JS 原生支持、零模板開發 | 依賴外部二進制、佔用內存大、並發受限 | 現有網頁直接導出、報表樣式複雜 |
| iText / pdfHTML | 純 Java、高性能、精確控制 | 需編寫模板代碼、CSS 支持不全 | 高併發、精密排版、純數據報表 |
| Flying Saucer (xhtmlrenderer) | 純 Java、支持 CSS 2.1 | 不支持現代 CSS (Flex/Grid)、JS 無支持 | 簡單 XHTML 轉 PDF |
掌握 wkhtmltopdf 的 Java 集成,讓你在「快速交付」與「樣式豐富」之間找到平衡點。下一篇將探討如何構建高可用的 PDF 轉換微服務。