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

发布于:2025-07-05 ⋅ 阅读:(22) ⋅ 点赞:(0)

在前端设计编辑器、画布工具或导出图像时,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> 必须显示指定 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";

        // 读取 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 Saucerjsoup 预处理更复杂的 DOM 结构

 

 


网站公告

今日签到

点亮在社区的每一天
去签到