在前端设计编辑器、画布工具或导出图像时,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">
<section
class="img-com-item-box"
style="
left: 385px;
top: 416px;
width: 1703px;
height: 1718px;
position: absolute;
z-index: 1;
rotate: 0deg;
"
>
<img
src="http://192.168.167.41:60/api/admin/sys-file/oss/file?fileName=b9122c84bbfa4f10b0b4512fc79b1151.png"
alt=""
/>
</section>
<section
class="img-com-item-box_2"
style="
left: 2644px;
top: 637px;
width: 947px;
height: 1088px;
position: absolute;
z-index: 3;
rotate: 0deg;
"
>
<img
src="http://192.168.167.41:60/api/admin/sys-file/oss/file?fileName=38b73efed79a4281a34e2ab73835874d.png"
alt=""
/>
</section>
<section
class="img-com-item-box"
style="
left: 3676px;
top: 637px;
width: 947px;
height: 1088px;
position: absolute;
z-index: 5;
rotate: 0deg;
"
>
<img
src="http://192.168.167.41:60/api/admin/sys-file/oss/file?fileName=5e9406454f7f453d9415754b01955e97.png"
alt=""
/>
</section>
<section
class="frame-section"
style="
left: 394px;
top: 425px;
width: 1685px;
height: 1700px;
z-index: 2;
rotate: 0deg;
"
>
<div class="frame-container">
<svg
class="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">
<image
class="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>
<image
x="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>
<section
class="frame-section"
style="
left: 2650px;
top: 642px;
width: 937px;
height: 1077px;
z-index: 4;
rotate: 0deg;
"
>
<div class="frame-container">
<svg
class="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">
<image
class="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>
<image
x="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>
<section
class="frame-section"
style="
left: 3682px;
top: 642px;
width: 937px;
height: 1077px;
z-index: 6;
rotate: 0deg;
"
>
<div class="frame-container">
<svg
class="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">
<image
class="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>
<image
x="-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>
必须显示指定 width
和 height
属性。需用正则自动注入。
✅ 最终解决思路
由于 OpenHTMLToPDF 不支持 <mask>
和 SVG 渲染,我们采取折中策略:
提取所有
<svg>...</svg>
标签;使用 Apache Batik 将其渲染为 PNG 图像(遮罩生效);
将原 SVG 替换为
<img src="data:image/png;base64,...">
;用 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";
// 读取 HTML
String 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 和 height
Matcher whM = whPattern.matcher(svgBlock);
String w = null, h = null;
if (whM.find()) {
w = whM.group(1);
h = whM.group(2);
}
try {
// 注入 <image> width/height
if (w != null && h != null) {
svgBlock = svgBlock.replaceAll(
"(?i)<image(?![^>]*\\bwidth=)",
"<image width=\"" + w + "\" height=\"" + h + "\""
);
}
// 转换 SVG -> PNG
PNGTranscoder transcoder = new PNGTranscoder();
TranscoderInput input = new TranscoderInput(new StringReader(svgBlock));
ByteArrayOutputStream pngBaos = new ByteArrayOutputStream();
transcoder.transcode(input, new TranscoderOutput(pngBaos));
pngBaos.flush();
// 转 Base64
String 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();
// 生成 PDF
try (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 Saucer
或jsoup
预处理更复杂的 DOM 结构