使用 docx4j 将 Web 页面转换为 DOCX 与 PDF 格式

一、背景

项目中需要将某数据显示的内容,提供一个下载 DOCX 与 PDF 功能。在分析阶段发现 docx4j(http://www.docx4java.org/trac/docx4j)提供了转换功能。在调试开发时遇到了 HTML 格式兼容,样式丢失,PDF 中文字体等问题。

二、分析

docx4j-ImportXHTML(https://github.com/plutext/docx4j-ImportXHTML),从名称上一看就知道这个只支持 XHTML。如果是非 XHTML 格式,解析就有问题。

所以在样例中使用了 jsoup(http://jsoup.org/)将 HTML 统一转换为 XHTML,并去掉不需要的一些内容(如:script)。这时再调用 docx4j-ImportXHTML 就可以正常解析。

注:这种转换不适用于常规 HTML 页面,转换过程中会丢失样式造成混乱。在这里想要做的是一种以特定 HTML 格式编写页面模板转出 DOCX 与 PDF 的方式。

三、样例程序

样例程序中有很多注释,这理就不再深入描述。该程序支持 Linux 环境。

1、主流程

a、jsoup 抓取指定 URL 的内容
b、使用 jsoup 清理内容,转为 XHTML
c、调用 docx4j-ImportXHTML,生成 WordprocessingMLPackage 对象(docx4j)
d、另存为 DOCX 与 PDF

2、POM 文件

这里使用了 Jetty,主要作用是测试时充当假 HTTP 服务器。

直接运行 mvn clean test 就可以看到转换效果。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.noahx</groupId>
    <artifactId>html2docx</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.docx4j</groupId>
            <artifactId>docx4j-ImportXHTML</artifactId>
            <version>3.2.2</version>
            <exclusions>
                <exclusion>
                    <artifactId>slf4j-log4j12</artifactId>
                    <groupId>org.slf4j</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>log4j</artifactId>
                    <groupId>log4j</groupId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.8.1</version>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.10</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-server</artifactId>
            <version>9.2.9.v20150224</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

3、TestHtmlConverter 单元测试类

该类创建模拟 HTTP 服务器,调用转换类将 HTML 内容转换为 DOCX 与 PDF,并调用操作系统打开文件操作。

出于调试目的,日志输出级别为 DEBUG,会产生大量日志。实际运行时可以提高日志级别。

package org.noahx.html2docx;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.impl.SimpleLogger;

import java.awt.*;
import java.io.File;

/**
 * Created by noah on 3/12/15.
 */
public class TestHtmlConverter {

    private static HtmlServer htmlServer = new HtmlServer();

    @BeforeClass
    public static void before() {
        System.setProperty(SimpleLogger.DEFAULT_LOG_LEVEL_KEY, "DEBUG");
        htmlServer.start();
    }

    @AfterClass
    public static void after() {
        htmlServer.stop();
    }

    @Test
    public void test() throws Exception {

        HtmlConverter converter = new HtmlConverter();
        String url = "http://127.0.0.1:" + htmlServer.getPort() + "/report.html"; //输入要转换的网址

        File fileDocx = converter.saveUrlToDocx(url);
        File filePdf = converter.saveUrlToPdf(url);

        Desktop.getDesktop().open(fileDocx); //由操作系统打开
        Desktop.getDesktop().open(filePdf);
    }
}

4、HTML 样本文件(report.html)

样式问题请查看注释。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>测试标题</title>
    <style type="text/css">

        body {
            font-family: SimSun;
        }

        .tb {
            border-collapse: collapse;
            empty-cells: show;
            width: 100%; /*竖版时100%宽度不正确*/
        }

        .tb th {
            text-align: center;
            border: 1px solid #000000; /* pdf 输出时边颜色受 color 影响,所以指定 #000000 */
        }

        .tb td {
            border: 1px solid #000000; /* pdf 输出时边颜色受 color 影响,所以指定 #000000 */
        }

        p {
            /*不支持 text-indent 样式,用中文全角空格(&#12288;) */
            /*text-indent: 2em;*/
        }
    </style>
</head>
<body>

<div style="text-align: center">
    <h1>标题1:大家好</h1>
</div>

<h2>标题2:大家好</h2>

<h3>标题3:大家好</h3>

<p>&#12288;&#12288;这是一个中文段落。这是一个中文段落。这是一个中文段落。这是一个中文段落。这是一个中文段落。这是一个中文段落。这是一个中文段落。这是一个中文段落。这是一个中文段落。这是一个中文段落。</p>

<p/>

<table class="tb"> <!-- 100%宽 -->
    <tr>
        <td>a</td>
    </tr>
</table>
<p/>

<table class="tb" style="width: 400px"> <!-- 定宽 -->
    <tr>
        <th>第一列</th>
        <th>第二列</th>
        <th>第三列</th>
        <th>第四列</th>
    </tr>
    <tr>
        <td>abc</td>
        <td>efg</td>
        <td>efg</td>
        <td>efg</td>
    </tr>
    <tr>
        <td>abc</td>
        <td style="color: #dc1619;font-weight: bold">efg</td>
        <!--color属性不可以使用英文(red) -->
        <td>efg</td>
        <td>efg</td>
    </tr>
</table>
<p/>

<div style="text-align: center">
    <h1>表1</h1>
</div>
<table class="tb" style="width: 400px;margin: auto"> <!-- 定宽居中 -->
    <tr>
        <th>第一列</th>
        <th>第二列</th>
        <th>第三列</th>
        <th>第四列</th>
    </tr>
    <tr>
        <td>abc</td>
        <td>efg</td>
        <td>efg</td>
        <td>efg</td>
    </tr>
    <tr>
        <td>abc</td>
        <td style="color: #dc1619;font-weight: bold">efg</td>
        <!--color属性不可以使用英文(red) -->
        <td>efg</td>
        <td>efg</td>
    </tr>
</table>
<p/>

<!-- docx 输出图时左边会有上点间距 -->

<h1>图1</h1> <!-- 缩小图片 -->
<img src="chart.jpg" width="50" height="30">

<p/>

<h1>图2</h1>
<img src="chart.jpg">

<p/>

<div style="text-align: center"> <!-- 图片居中 -->
    <h1>图3</h1>
    <img src="chart.jpg">
</div>

</body>
</html>

5、主转换程序(HtmlConverter)

package org.noahx.html2docx;

import org.docx4j.Docx4J;
import org.docx4j.convert.in.xhtml.XHTMLImporterImpl;
import org.docx4j.fonts.IdentityPlusMapper;
import org.docx4j.fonts.Mapper;
import org.docx4j.fonts.PhysicalFont;
import org.docx4j.fonts.PhysicalFonts;
import org.docx4j.jaxb.Context;
import org.docx4j.model.structure.PageSizePaper;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.wml.RFonts;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Entities;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.OutputStream;
import java.net.URL;

/**
 * Created by noah on 3/10/15.
 */
public class HtmlConverter {

    /**
     * 输出文件名
     */
    public final String OUT_FILENAME = "OUT_ConvertInXHTMLURL";

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 将页面保存为 docx
     *
     * @param url
     * @return
     * @throws Exception
     */
    public File saveUrlToDocx(String url) throws Exception {
        return saveDocx(url2word(url));
    }

    /**
     * 将页面保存为 pdf
     *
     * @param url
     * @return
     * @throws Exception
     */
    public File saveUrlToPdf(String url) throws Exception {
        return savePdf(url2word(url));
    }

    /**
     * 将页面转为 {@link org.docx4j.openpackaging.packages.WordprocessingMLPackage}
     *
     * @param url
     * @return
     * @throws Exception
     */
    public WordprocessingMLPackage url2word(String url) throws Exception {
        return xhtml2word(url2xhtml(url));
    }

    /**
     * 将 {@link org.docx4j.openpackaging.packages.WordprocessingMLPackage} 存为 docx
     *
     * @param wordMLPackage
     * @return
     * @throws Exception
     */
    public File saveDocx(WordprocessingMLPackage wordMLPackage) throws Exception {

        File file = new File(genFilePath() + ".docx");
        wordMLPackage.save(file); //保存到 docx 文件

        if (logger.isDebugEnabled()) {
            logger.debug("Save to [.docx]: {}", file.getAbsolutePath());
        }
        return file;
    }

    /**
     * 将 {@link org.docx4j.openpackaging.packages.WordprocessingMLPackage} 存为 pdf
     *
     * @param wordMLPackage
     * @return
     * @throws Exception
     */
    public File savePdf(WordprocessingMLPackage wordMLPackage) throws Exception {

        File file = new File(genFilePath() + ".pdf");

        OutputStream os = new java.io.FileOutputStream(file);

        Docx4J.toPDF(wordMLPackage, os);

        os.flush();
        os.close();

        if (logger.isDebugEnabled()) {
            logger.debug("Save to [.pdf]: {}", file.getAbsolutePath());
        }
        return file;
    }

    /**
     * 将 {@link org.jsoup.nodes.Document} 对象转为 {@link org.docx4j.openpackaging.packages.WordprocessingMLPackage}
     * xhtml to word
     *
     * @param doc
     * @return
     * @throws Exception
     */
    protected WordprocessingMLPackage xhtml2word(Document doc) throws Exception {

        WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.createPackage(PageSizePaper.valueOf("A4"), true); //A4纸,//横版:true

        configSimSunFont(wordMLPackage); //配置中文字体

        XHTMLImporterImpl xhtmlImporter = new XHTMLImporterImpl(wordMLPackage);

        wordMLPackage.getMainDocumentPart().getContent().addAll( //导入 xhtml
                xhtmlImporter.convert(doc.html(), doc.baseUri()));


        return wordMLPackage;
    }

    /**
     * 将页面转为{@link org.jsoup.nodes.Document}对象,xhtml 格式
     *
     * @param url
     * @return
     * @throws Exception
     */
    protected Document url2xhtml(String url) throws Exception {
        Document doc = Jsoup.connect(url).get(); //获得

        if (logger.isDebugEnabled()) {
            logger.debug("baseUri: {}", doc.baseUri());
        }

        for (Element script : doc.getElementsByTag("script")) { //除去所有 script
            script.remove();
        }

        for (Element a : doc.getElementsByTag("a")) { //除去 a 的 onclick,href 属性
            a.removeAttr("onclick");
            a.removeAttr("href");
        }

        Elements links = doc.getElementsByTag("link"); //将link中的地址替换为绝对地址
        for (Element element : links) {
            String href = element.absUrl("href");

            if (logger.isDebugEnabled()) {
                logger.debug("href: {} -> {}", element.attr("href"), href);
            }

            element.attr("href", href);
        }

        doc.outputSettings()
                .syntax(Document.OutputSettings.Syntax.xml)
                .escapeMode(Entities.EscapeMode.xhtml);  //转为 xhtml 格式

        if (logger.isDebugEnabled()) {
            String[] split = doc.html().split("\n");
            for (int c = 0; c < split.length; c++) {
                logger.debug("line {}:\t{}", c + 1, split[c]);
            }
        }
        return doc;
    }

    /**
     * 为 {@link org.docx4j.openpackaging.packages.WordprocessingMLPackage} 配置中文字体
     *
     * @param wordMLPackage
     * @throws Exception
     */
    protected void configSimSunFont(WordprocessingMLPackage wordMLPackage) throws Exception {
        Mapper fontMapper = new IdentityPlusMapper();
        wordMLPackage.setFontMapper(fontMapper);

        String fontFamily = "SimSun";

        URL simsunUrl = this.getClass().getResource("/org/noahx/html2docx/simsun.ttc"); //加载字体文件(解决linux环境下无中文字体问题)
        PhysicalFonts.addPhysicalFont(fontFamily, simsunUrl);
        PhysicalFont simsunFont = PhysicalFonts.get(fontFamily);
        fontMapper.put(fontFamily, simsunFont);

        RFonts rfonts = Context.getWmlObjectFactory().createRFonts(); //设置文件默认字体
        rfonts.setAsciiTheme(null);
        rfonts.setAscii(fontFamily);
        wordMLPackage.getMainDocumentPart().getPropertyResolver()
                .getDocumentDefaultRPr().setRFonts(rfonts);
    }

    /**
     * 生成文件位置
     *
     * @return
     */
    protected String genFilePath() {
        return System.getProperty("user.dir") + "/" + OUT_FILENAME;
    }
}

四、转换效果

1、DOCX 转换效果

使用 docx4j 将 Web 页面转换为 DOCX 与 PDF 格式_第1张图片


2、PDF 转换效果


使用 docx4j 将 Web 页面转换为 DOCX 与 PDF 格式_第2张图片

五、源码下载

源码:https://onedrive.live.com/redir?resid=55dc3c0254f15cff%21159

你可能感兴趣的:(html,转换,docx,pdf,docx4j)