返回 导航

SpringBoot / Cloud

hangge.com

SpringBoot - 实现将HTML内容转换为Word文档教程(使用docx4j生成docx文件)

作者:hangge | 2025-11-20 08:46
    项目中我们有时需要将将 HTML(或 XHTML)转换为 Word,例如:导出富文本编辑器内容、生成报告、将网页内容归档为可编辑的 DOCX 等。docx4jJava 生态中操作 Office Open XML.docx)的成熟库,提供了从 XHTML/HTMLWordML 的转换工具,下面我将通过样例演示如何使用 docx4j 实现服务器端将 HTML 内容转换生成 DOCX

1,准备工作

(1)首先我们需要在项目的 pom.xml 中添加 docx4j 以及相关的依赖。
<!--  docx4j核心依赖 -->
<dependency>
	<groupId>org.docx4j</groupId>
	<artifactId>docx4j-core</artifactId>
	<version>11.4.8</version>
</dependency>
<!--  HTML 转 DOCX,需要这个 -->
<dependency>
	<groupId>org.docx4j</groupId>
	<artifactId>docx4j-ImportXHTML</artifactId>
	<version>11.4.8</version>
</dependency>
<dependency>
	<groupId>org.docx4j</groupId>
	<artifactId>docx4j-JAXB-ReferenceImpl</artifactId>
	<version>11.4.8</version> <!-- 或最新 Jakarta 版本 -->
</dependency>

<!-- 用于解析/修改 HTML(处理 img) -->
<dependency>
	<groupId>org.jsoup</groupId>
	<artifactId>jsoup</artifactId>
	<version>1.16.1</version>
</dependency>

<!-- apache commons-io:简化文件/流操作 -->
<dependency>
	<groupId>commons-io</groupId>
	<artifactId>commons-io</artifactId>
	<version>2.14.0</version>
</dependency>

<!-- 添加 JAXB 依赖(JDK 11+) -->
<dependency>
	<groupId>org.glassfish.jaxb</groupId>
	<artifactId>jaxb-runtime</artifactId>
	<version>4.0.3</version>
</dependency>
<dependency>
	<groupId>jakarta.xml.bind</groupId>
	<artifactId>jakarta.xml.bind-api</artifactId>
	<version>4.0.0</version>
</dependency>

(2)接着我们要创建一个空白的 docx 文件(这里我命名为 template.docx),并将其放置在项目的 resources 目录下。
提示:这么做是因为 HTMLDOCX 的渲染需要预定义样式。docx4j 官方推荐使用一个“正常的 Word 文档模板”来导入 HTML,由于其自带样式、表格、字体等,导出效果最接近 Word

2,样例代码

(1)我这里创建一个 Controller,该 Controller 暴露 /export-docx 接口,接收形如 { html: "...", filename?: "xxx.docx" }JSON 数据,返回 application/vnd.openxmlformats-officedocument.wordprocessingml.document 二进制流。
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.convert.in.xhtml.XHTMLImporterImpl;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.apache.commons.io.FileUtils;

import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.UUID;

@RestController
@CrossOrigin
public class ExportController {
    /**
     * 接收 HTML,转换为 docx 并返回
     * 请求体例子:
     * { "html": "<html>...</html>", "filename": "report.docx" }
     */
    @PostMapping("/export-docx")
    public ResponseEntity<byte[]> exportDocx(@RequestBody ExportRequest req) {
        String html = req.getHtml();
        String filename = (req.getFilename() == null || req.getFilename().isEmpty()) ?
                ("document-" + System.currentTimeMillis() + ".docx") : req.getFilename();

        // 将 html 进行预处理:处理图片 data: URI 和远程图片(下载为临时文件,替换 src)
        File[] tmpFiles = new File[0];
        try {
            Document doc = Jsoup.parse(html);
            // 处理 <img> 标签
            Elements imgs = doc.select("img");
            if (!imgs.isEmpty()) {
                tmpFiles = new File[imgs.size()];
                int idx = 0;
                for (Element img : imgs) {
                    String src = img.attr("src");
                    if (src == null || src.isEmpty()) continue;

                    // data:image/...;base64,...
                    if (src.startsWith("data:")) {
                        // parse base64 data URI
                        String base64Part = src.substring(src.indexOf(",") + 1);
                        byte[] data = Base64.getDecoder().decode(base64Part);
                        String ext = guessExtensionFromData(src); // png/jpg...
                        File tmp = File.createTempFile("img_" + UUID.randomUUID(), "." + ext);
                        FileUtils.writeByteArrayToFile(tmp, data);
                        tmpFiles[idx++] = tmp;
                        img.attr("src", tmp.toURI().toString()); // file://...
                    } else if (src.startsWith("http://") || src.startsWith("https://")) {
                        // download remote image to temp file
                        URL url = new URL(src);
                        String ext = guessExtensionFromUrl(src); // try to infer ext
                        File tmp = File.createTempFile("img_" + UUID.randomUUID(),
                                ext != null ? "." + ext : ".img");
                        try (InputStream in = url.openStream()) {
                            FileUtils.copyInputStreamToFile(in, tmp);
                            tmpFiles[idx++] = tmp;
                            img.attr("src", tmp.toURI().toString());
                        } catch (Exception ex) {
                            // 下载失败则忽略图片(或保留原 src),不阻塞全局转换
                            ex.printStackTrace();
                        }
                    } else {
                        // 其他相对路径或 file:// 等,保留或根据需要处理
                    }
                }
            }

            // Optional: 取回修改过的完整 html(包含内联 <head><style> 的建议)
            // 如果用户传入的是片段(没有 <html>),Jsoup.parse 会补齐
            String processedHtml = doc.outerHtml();
            // 先处理自闭合标签
            processedHtml = processedHtml.replaceAll("<(img|meta|br|hr)([^>]*)>", "<$1$2/>");
            processedHtml = processedHtml.replaceAll("<(col)(?!group)([^>]*)>", "<$1$2/>");
            // 加载带默认样式的 template.docx
            InputStream is = getClass().getResourceAsStream("/template.docx");
            WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.load(is);

            // 使用 docx4j 将 HTML 导入 docx
            XHTMLImporterImpl xHTMLImporter = new XHTMLImporterImpl(wordMLPackage);
            // 若需要可以传入 base uri:xHTMLImporter.setHyperlinkStyle ??
            // 导入 HTML 内容
            wordMLPackage.getMainDocumentPart().getContent().addAll(
                    xHTMLImporter.convert(processedHtml, null)
            );

            // 保存到 byte[]
            try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                wordMLPackage.save(baos);
                byte[] bytes = baos.toByteArray();

                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.parseMediaType(
                        "application/vnd.openxmlformats-officedocument.wordprocessingml.document"));
                headers.setContentDispositionFormData("attachment", filename);

                return ResponseEntity.ok()
                        .headers(headers)
                        .body(bytes);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            return ResponseEntity.status(500).body(("Conversion failed: "
                    + ex.getMessage()).getBytes(StandardCharsets.UTF_8));
        } finally {
            // 清理临时文件
            if (tmpFiles != null) {
                for (File f : tmpFiles) {
                    if (f != null && f.exists()) f.delete();
                }
            }
        }
    }

    // 从 data: URI 头部猜测图片格式
    private static String guessExtensionFromData(String dataUri) {
        // data:image/png;... => png
        try {
            String mime = dataUri.substring(5, dataUri.indexOf(";"));
            if (mime.contains("/")) {
                return mime.substring(mime.indexOf("/") + 1);
            }
        } catch (Exception ignored) {}
        return "png";
    }

    // 从 URL 路径猜测图片格式
    private static String guessExtensionFromUrl(String url) {
        try {
            String lower = url.toLowerCase();
            if (lower.contains(".png")) return "png";
            if (lower.contains(".jpg") || lower.contains(".jpeg")) return "jpg";
            if (lower.contains(".gif")) return "gif";
            if (lower.contains(".svg")) return "svg";
            if (lower.contains(".bmp")) return "bmp";
        } catch (Exception ignored) {}
        return null;
    }

    // DTO
    public static class ExportRequest {
        private String html;
        private String filename;

        public String getHtml() { return html; }
        public void setHtml(String html) { this.html = html; }
        public String getFilename() { return filename; }
        public void setFilename(String filename) { this.filename = filename; }
    }
}

(2)关键代码说明:
  • 我这里使用了 JsoupHTML 解析并把 data:/http(s): 图片下载成临时文件,然后替换 <img src=""> file:// 路径。XHTMLImporterImpl 在导入时可以读取 file:// 路径并把图片内嵌到 docx
  • wordMLPackage.save(baos) 会把生成好的 docx 写入 ByteArrayOutputStream,然后直接返回给前端下载。
  • 临时文件在 finally 中尝试删除,注意生产环境要考虑安全与并发清理策略(可以放到专门 tmp 目录并定期清理)。
  • 如果 HTML 包含外部 CSS 文件,建议前端先把关键样式内联或把 <style> 放到传入 HTML <head> 中,这会提升导出的样式保真度。

3,运行测试

(1)假设我需要将如下 html 页面内容转换成 word
<html>
<head>
    <meta charset="utf-8" />
    <style>
        body {
            font-family: "Arial", sans-serif;
            font-size: 12p;
        }

        table {
            border-collapse: collapse;
        }

        th {
            background: #F9FAFB;
            font-weight: 600;
        }

        td,
        th {
            border: 1px solid #ccc;
            padding: 4px;
        }
    </style>
</head>
<body>
    <p>欢迎访问<a href="https://www.hangge.com">hangge.com</a></p>
    <p><img src="https://www.hangge.com/blog/images/logo.png" alt="logo"></p>
    <p></p>
    <p>测试<strong>加粗</strong>测试<em>斜体</em>测试<u>下划线</u>测试<s>删除线</s></p>
    <p></p>
    <p><span style="color: rgb(255, 102, 255);">修改文字颜色</span></p>
    <p><span style="font-size: 22px;">调整文字大小</span></p>
    <p></p>
    <p>下面是一个3x3表格</p>
    <table>
        <thead>
            <tr>
                <th>ID</th>
                <th>用户名称</th>
                <th>用户年龄</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>1</td>
                <td>刘小龙</td>
                <td>88</td>
            </tr>
            <tr>
                <td>2</td>
                <td>张大伟</td>
                <td>66</td>
            </tr>
        </tbody>
    </table>
</body>
</html>

(2)我们使用 Postman 测试这个 /export-docx 接口,传入 html 内容和文件名,并将结果保存到本地。

(3)打开保存下来的 report.docx 文件,可以看到该 word 文档内容如下:

(4)特别要说明的是,我们提交的 html 数据也可以只包含 body 里面的内容,后端会自动补齐 <html> 等内容。

评论

全部评论(0)

回到顶部