项目中需要将某数据显示的内容,提供一个下载 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 环境。
a、jsoup 抓取指定 URL 的内容
b、使用 jsoup 清理内容,转为 XHTML
c、调用 docx4j-ImportXHTML,生成 WordprocessingMLPackage 对象(docx4j)
d、另存为 DOCX 与 PDF
这里使用了 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>
该类创建模拟 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); } }
样式问题请查看注释。
<!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 样式,用中文全角空格( ) */ /*text-indent: 2em;*/ } </style> </head> <body> <div style="text-align: center"> <h1>标题1:大家好</h1> </div> <h2>标题2:大家好</h2> <h3>标题3:大家好</h3> <p>  这是一个中文段落。这是一个中文段落。这是一个中文段落。这是一个中文段落。这是一个中文段落。这是一个中文段落。这是一个中文段落。这是一个中文段落。这是一个中文段落。这是一个中文段落。</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>
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; } }
源码:https://onedrive.live.com/redir?resid=55dc3c0254f15cff%21159