Java 使用 OpenHTMLToPDF + Batik 将含 SVG 遮罩的 HTML 转为 PDF 的完整实践

在前端设计编辑器、画布工具或导出图像时,SVG 遮罩(<mask> 是一个常见而强大的手段。它能用于精准的图像裁剪、圆角遮罩、形状组合等。但当我们尝试在 Java 后端将这些 HTML(含 SVG 遮罩)导出为 PDF 时,会遇到不少技术挑战。

本篇将介绍如何使用 OpenHTMLToPDF + Batik 在 Java 中高质量导出带有 SVG 遮罩的 HTML 为 PDF。

🧩 背景

你有如下 HTML 页面:

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><style>.canvas-main {background-size: cover;/* background-position: center center; *//* background-repeat: no-repeat; *//* transform: translate(52.9992px, 218px) scale(0.5, 0.5); *//* transform-origin: 0px 0px; */background-image: url(http://192.168.167.41:60/api/admin/sys-file/oss/file?fileName=117bd883dc90488aacf678cce2918979.png);display: flex;justify-content: center;align-items: center;position: relative;}.img-com-item-box {position: absolute;}.frame-section {position: absolute;}.frame-section .frame-container {width: 100%;height: 100%;position: relative;}.frame-section .frame-container .svg-show {position: absolute;}.frame-section .frame-container .frame-mask-img {width: 100%;height: 100%;}</style></head><body><div class="canvas-main" style="width: 4843px; height: 2362px"><sectionclass="img-com-item-box"style="left: 385px;top: 416px;width: 1703px;height: 1718px;position: absolute;z-index: 1;rotate: 0deg;"><imgsrc="http://192.168.167.41:60/api/admin/sys-file/oss/file?fileName=b9122c84bbfa4f10b0b4512fc79b1151.png"alt=""/></section><sectionclass="img-com-item-box_2"style="left: 2644px;top: 637px;width: 947px;height: 1088px;position: absolute;z-index: 3;rotate: 0deg;"><imgsrc="http://192.168.167.41:60/api/admin/sys-file/oss/file?fileName=38b73efed79a4281a34e2ab73835874d.png"alt=""/></section><sectionclass="img-com-item-box"style="left: 3676px;top: 637px;width: 947px;height: 1088px;position: absolute;z-index: 5;rotate: 0deg;"><imgsrc="http://192.168.167.41:60/api/admin/sys-file/oss/file?fileName=5e9406454f7f453d9415754b01955e97.png"alt=""/></section><sectionclass="frame-section"style="left: 394px;top: 425px;width: 1685px;height: 1700px;z-index: 2;rotate: 0deg;"><div class="frame-container"><svgclass="svg-show"width="1685"height="1700"xmlns:xlink="http://www.w3.org/1999/xlink"xmlns="http://www.w3.org/2000/svg"viewBox="0, 0, 1685, 1700"preserveAspectRatio="none"><defs><mask id="mask-ebd493f2-c9cd-486e-83a8-2eb465714b0e"><imageclass="frame-mask-img"transform="rotate(0,50 47.52851711026616)"xlink:href="http://192.168.167.41:60/api/admin/sys-file/oss/file?fileName=e6128d0920924286abcffc3e082ff7b4.png"preserveAspectRatio="none"></image></mask></defs><imagex="0"y="-413.75"class="frame-content-img"width="1685"height="2527.5"xlink:href="http://192.168.167.41:60/api/admin/sys-file/oss/file?fileName=67a2d043f8a14ea0b1a41f68d9eb527a.jpg"preserveAspectRatio="none"transform="rotate(0,50 47.52851711026616)"mask="url(#mask-ebd493f2-c9cd-486e-83a8-2eb465714b0e)"></image></svg></div></section><sectionclass="frame-section"style="left: 2650px;top: 642px;width: 937px;height: 1077px;z-index: 4;rotate: 0deg;"><div class="frame-container"><svgclass="svg-show"xmlns:xlink="http://www.w3.org/1999/xlink"width="937"height="1077"xmlns="http://www.w3.org/2000/svg"viewBox="0, 0, 937, 1077"preserveAspectRatio="none"style="left: 0px; top: 0px; z-index: 1"><defs><mask id="mask-3498e8fb-01c4-4174-9367-90d00a0e88b4"><imageclass="frame-mask-img"transform="rotate(0,50 47.52851711026616)"xlink:href="http://192.168.167.41:60/api/admin/sys-file/oss/file?fileName=133c3f908db241b7ba69fba81554283a.png"preserveAspectRatio="none"></image></mask></defs><imagex="0"y="-164.25"class="frame-content-img"width="937"height="1405.5"xlink:href="http://192.168.167.41:60/api/admin/sys-file/oss/file?fileName=e4b3a4925110470a87bf4727a8b1947f.jpg"preserveAspectRatio="none"transform="rotate(0,50 47.52851711026616)"mask="url(#mask-3498e8fb-01c4-4174-9367-90d00a0e88b4)"></image></svg></div></section><sectionclass="frame-section"style="left: 3682px;top: 642px;width: 937px;height: 1077px;z-index: 6;rotate: 0deg;"><div class="frame-container"><svgclass="svg-show"xmlns:xlink="http://www.w3.org/1999/xlink"width="937"height="1077"xmlns="http://www.w3.org/2000/svg"viewBox="0, 0, 937, 1077"preserveAspectRatio="none"style="left: 0px; top: 0px; z-index: 1"><defs><mask id="mask-736c2bea-238e-4337-8766-74926f680215"><imageclass="frame-mask-img"transform="rotate(0,50 47.52851711026616)"xlink:href="http://192.168.167.41:60/api/admin/sys-file/oss/file?fileName=18c264e8c2ea4cdd83eee9aab8ce0a26.png"preserveAspectRatio="none"></image></mask></defs><imagex="-190"y="-801.2020709452534"class="frame-content-img"width="1527"height="2289.4041418905067"xlink:href="http://192.168.167.41:60/api/admin/sys-file/oss/file?fileName=4dad23a7e0a24b09a5859811a2c20af9.jpg"preserveAspectRatio="none"transform="rotate(0,50 47.52851711026616)"mask="url(#mask-736c2bea-238e-4337-8766-74926f680215)"></image></svg></div></section></div></body>
</html>

你希望将这整个页面导出为 PDF,并保留 SVG 遮罩效果


😵‍💫 遇到的问题

我们使用 OpenHTMLToPDF 来将 HTML 转换为 PDF,但会遇到如下几个常见错误:

❌ 问题 1:xlink 未声明前缀

错误信息如下:

org.xml.sax.SAXParseException: 与元素类型 "image" 相关联的属性 "xlink:href" 的前缀 "xlink" 未绑定。

✅ 解决方案

<svg> 标签中加上:

xmlns:xlink="http://www.w3.org/1999/xlink"

❌ 问题 2:Batik 报错 <image> 缺失宽高属性

错误信息如下:

The attribute "width" of the element <image> is required 

✅ 解决方案

Batik 渲染 SVG 时要求所有 <image> 必须显示指定 widthheight 属性。需用正则自动注入。


✅ 最终解决思路

由于 OpenHTMLToPDF 不支持 <mask> 和 SVG 渲染,我们采取折中策略:

  1. 提取所有 <svg>...</svg> 标签

  2. 使用 Apache Batik 将其渲染为 PNG 图像(遮罩生效);

  3. 将原 SVG 替换为 <img src="data:image/png;base64,...">

  4. 用 OpenHTMLToPDF 渲染生成最终 PDF


📦 Maven 依赖

<dependencies> <dependency> <groupId>com.openhtmltopdf</groupId> <artifactId>openhtmltopdf-pdfbox</artifactId> <version>1.0.10</version> </dependency><dependency> <groupId>org.apache.xmlgraphics</groupId><artifactId>batik-transcoder</artifactId> <version>1.14</version> </dependency>
</dependencies>

🧪 最终代码:HTML 转 PDF

package com.example.psd.image;import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
import org.apache.batik.transcoder.TranscoderInput;
import org.apache.batik.transcoder.TranscoderOutput;
import org.apache.batik.transcoder.image.PNGTranscoder;import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;public class HtmlToPdfConverter {public static void main(String[] args) throws IOException {String htmlFilePath  = "C:\\Users\\y\\Desktop\\input.html";String outputPdfPath = "C:\\Users\\y\\Desktop\\画布元素导出output.pdf";// 读取 HTMLString html = new String(java.nio.file.Files.readAllBytes(new File(htmlFilePath).toPath()), StandardCharsets.UTF_8);// 提取所有 <svg> 标签Pattern svgPattern = Pattern.compile("<svg[\\s\\S]*?</svg>", Pattern.CASE_INSENSITIVE);Matcher svgMatcher = svgPattern.matcher(html);StringBuffer sb = new StringBuffer();// 提取宽高Pattern whPattern = Pattern.compile("<svg[^>]*?\\bwidth\\s*=\\s*\"(\\d+)\"[^>]*?\\bheight\\s*=\\s*\"(\\d+)\"",Pattern.CASE_INSENSITIVE);while (svgMatcher.find()) {String svgBlock = svgMatcher.group();// 提取 width 和 heightMatcher whM = whPattern.matcher(svgBlock);String w = null, h = null;if (whM.find()) {w = whM.group(1);h = whM.group(2);}try {// 注入 <image> width/heightif (w != null && h != null) {svgBlock = svgBlock.replaceAll("(?i)<image(?![^>]*\\bwidth=)","<image width=\"" + w + "\" height=\"" + h + "\"");}// 转换 SVG -> PNGPNGTranscoder transcoder = new PNGTranscoder();TranscoderInput input = new TranscoderInput(new StringReader(svgBlock));ByteArrayOutputStream pngBaos = new ByteArrayOutputStream();transcoder.transcode(input, new TranscoderOutput(pngBaos));pngBaos.flush();// 转 Base64String base64 = Base64.getEncoder().encodeToString(pngBaos.toByteArray());String imgTag = "<img src=\"data:image/png;base64," + base64 +"\" style=\"width:100%;height:auto;\"/>";svgMatcher.appendReplacement(sb, Matcher.quoteReplacement(imgTag));} catch (Exception ex) {ex.printStackTrace();svgMatcher.appendReplacement(sb, Matcher.quoteReplacement(svgBlock));}}svgMatcher.appendTail(sb);String flattenedHtml = sb.toString();// 生成 PDFtry (OutputStream os = new FileOutputStream(outputPdfPath)) {PdfRendererBuilder builder = new PdfRendererBuilder();builder.useFastMode();builder.withHtmlContent(flattenedHtml, new File(htmlFilePath).toURI().toString());builder.toStream(os);builder.run();System.out.println("✅ PDF 导出成功:" + outputPdfPath);} catch (Exception e) {e.printStackTrace();}}
}

🎯 总结

问题原因解决方式
xlink:href 前缀未声明SVG 中使用了 xlink 属性<svg> 增加 xmlns:xlink
Batik 报 <image> 缺失宽高Batik 要求 <image> 必须有显式宽高用正则添加 width/height
OpenHTMLToPDF 不支持 <mask>其内部不解析复杂 SVG提前转成 PNG 再嵌入


🚀 效果预览

最终 PDF 中,SVG 遮罩效果完整保留、页面布局不受破坏,适合用于商品模板、画布设计导出等业务场景。


🧠 拓展建议

  • ✅ 可以封装为方法支持多个 HTML 文件批量处理

  • ✅ 支持远程图片或 Blob 链接,加入 ImageReplacer

  • ✅ 配合 Flying Saucerjsoup 预处理更复杂的 DOM 结构