在软件世界中,已经有大量所见即所得的HTML编辑器,绝大多数的编辑器都是用JavaScript及基于JavaScript的库构建而成。这些编辑器在处理各种与HTML相关的格式化以及生成HTML源数据时运转良好,但并不具备我们在商业报告中所需要的各种能力。例如,在一个具有典型的审阅/批准生命周期的发布中,创建图形、图表、跟踪变化以及插入注释是非常实用的。加之,Word天生就是离线工作的。创建新的文档或编辑现有文档时,无需网络连接。而浏览器/JavaScript的离线能力仍然有限。考虑到这些能力,有效利用为此目的专门开发的原生应用看起来是更好的解决方案。
Apache Poor Obfuscation Implementation(POI)是一个以加强版面向商业的报告和预览为目的,读取MS Word和MS Excel等Microsoft文档的杰出Java框架。它通过将Word文档转化为易读的HTML格式,提供了增强的数据读取能力。
POI库支持Office Open XML文件格式——OOXML(文字处理和电子表格的一种表示形式)。它包含了用于读取文档中各种区块的API。在将文档加载到POI内存中时,它会获取文档所有的元数据和内容。我们可以通过遍历文档中的各个区块(例如,段落、表格、单元格等。),很容易就读取到这些信息。不过,仅仅使用POI,我们还无法实现HTML等效元素的生成。
例如,一段带有背景颜色的文本会被渲染为带有字体类型、背景颜色等格式化样式的HTML span元素。我们应该可以根据从POI中读取到的该文档区块的所有相关属性或样式,用Simple API for XML (SAX) API创建HTML元素。为了实现读取低级别文档区块的功能,应该有一个访问者模式风格的API顺序地读取文档中各个区块的内容和属性(格式化风格等)。
Xdocreport是在POI内核和POI-OOXML基础上用通用的OOXML-SCHEMA构建而成。它会在POI内核的帮助下加载文档,然后在poi-ooxml和ooxml-schemas的帮助下读取内容和元数据。由于使用了模式库,Xdocreport可以很方便地浏览文档中的元素。Xdocreport提供了访问者风格的API用于读取文档中的每个区块并以HTML格式生成内容。通过扩展该库,就可以处理各种格式的HTML样式并且克服对表结构和编号渲染的限制。下面的代码片段展示了如何使用所见即所得的方法完成这项工作。
关于如何使用扩展的xdocreport控制HTML渲染,请参考下方的体系架构图
现在,我们开始尝试将包含各种组件的扩展docx文档,例如段落、表格、编号以及图片等,转换为HTML格式。
加载docx文件流用于创建XWPFDocument对象
FileInputStream fstream = new FileInputStream("Example.docx");XWPFDocument document = new XWPFDocument (fstream);//创建选项。这些选项用于控制图片的渲染等,XHTMLOptions options = XHTMLOptions.create();// 创建输出流用于存储生成的HTML源文件ByteArrayOutputStream out = new ByteArrayOutputStream();IContentHandlerFactory factory = DefaultContentHandlerFactory.INSTANCE;options.setIgnoreStylesIfUnused(false);XHTMLMapper mapper = new XHTMLMapper (document, factory.create(out, null, options), options);mapper.start();out.close();
我们可以将输出流转化成String对象并创建HTML文件。
String html = new String(out.toByteArray(), “UTF-8”);
我们可以很容易地将生成的HTML源数据附在servlet响应输出流当中。
我们可以扩展xdocreport中的XHTMLMapper类自定义从MS Word转化而来的默认的组件样式。我们还可以自定义HTML组件的渲染行为。
例如,上标/下标是作为附加在span元素上的CSS样式生成的。但是,如果某个较早版本的浏览器无法理解这些CSS样式,而只能理解sup和sub标签该怎么办?所以,作为渲染的一部分,会强制生成sup/sub HTML标签,而不是CSS样式。下面的例子展示了如何重写visitRun方法以生成sup/sub HTML标签:
@Overrideprotected void visitRun(XWPFRun run, boolean pageNumber, String url, Object paragraphContainer) throws Exception { boolean isSuper = false; boolean isSub = false;if (rPr.getVertAlign() != null) { int align = rPr.getVertAlign().getVal().intValue(); if (STVerticalAlignRun.INT_SUPERSCRIPT == align) { isSuper = true; } else if (STVerticalAlignRun.INT_SUBSCRIPT == align) { isSub = true; } } ...if (isSuper || isSub) { startElement(isSuper ? "SUP" : "SUB", null); } . . . if (isSuper || isSub) { endElement(isSuper ? "SUP" : "SUB"); }
我们还可以自定义组件。例如,默认情况下,所有生成的超链接都会直接打开目标,不会在链接上附加任何属性。虽然我们无法将所有的配置都转化成HTML,我们仍需要支持在新的窗口中打开所有的超链接。我们可以通过重写visitRun方法达成这一目的:
@Overrideprotected void visitRun(XWPFRun run, boolean pageNumber, String url, Object paragraphContainer) throws Exception { boolean isUrl = url != null; if (isUrl) { AttributesImpl hyperlinkAttributes = new AttributesImpl(); SAXHelper.addAttrValue(hyperlinkAttributes, "href", url); SAXHelper.addAttrValue(hyperlinkAttributes, "target", "_blank"); startElement("a", hyperlinkAttributes); } . . . if (isUrl) { characters(" "); endElement("a"); }
对于项目编号,对应生成的HTML可以使ul和li元素。不过,并不是所有的MS Word编号类型或样式都可以转化成等效的HTML列表项,因为MS Word有自己的渲染能力和图片或剪贴画库。简单的ul和li HTML元素无法满足这个要求。这时我们可以利用span元素。第一个span元素中将包含实际的编号字符,第二个包含具体数据。因此,实现基本的项目编号,我们可以重写“startVisitParagraph”方法。下面的代码片段展示了如何渲染基本的ASCII字符项目编号(一个小圆点)。
startElement(SPAN_ELEMENT, attributes);String text = itemContext.getText();if (StringUtils.isNotEmpty(text)) { text = text.replace('\u2020', '\u2022'); //loop to replace all text = text + " "; SAXHelper.characters(contentHandler, StringEscapeUtils.escapeHtml(text));}endElement(SPAN_ELEMENT);
在HTML中,制表符没有直接的表示法。我们可以输出固定数量的空格字符作为制表符的近似替代品。代码片段如下:
@Overrideprotected void visitTabs(CTTabs o, Object paragraphContainer) throws Exception { if (paragraph != null && o == null) { startElement(SPAN_ELEMENT, null); characters(" ");//所需添加一定数量的空格 endElement(SPAN_ELEMENT); return; } super.visitTabs(o, paragraphContainer);}
到现在为止,我们已经将具有格式化样式的文本数据转化为HTML源。不过,文档中可能还有一些内嵌或外链一些图片在其中。这些图片需要被抽取出来并在浏览器中展示。
我们可以用默认的附带XHTMLOptions选项的URIResolver解析内嵌图片。默认的解析器会扫描“word/media/”下的docx压缩文件夹,所有的内嵌图片都存储在这里。渲染后的HTML图片代码如下所示
<img src="word/media/image1.jpeg" width="189pt" height="141pt"/>
但是,浏览器无法理解src的值(源路径),这会导致图片无法访问。我们可以通过自定义的解析器解决这个问题——构建一个URL传给要加载这个图片的servlet
final String imgUrl = "/MyImageLoader?imgeId=";XHTMLOptions options = XHTMLOptions.create().URIResolver(new IURIResolver(){ @Override public String resolve(String uri) { if (imgUrl == null) return "/no_image.gif"; int ls = uri.lastIndexOf('/'); if (ls >= 0) uri = uri.substring(ls+1); return imgUrl+uri; }});
上述代码所创建的选项解析并渲染的图片代码如下:
<img src="/MyImageLoader/imageId=image1.jpeg" width="189pt" height="141pt"/>
典型的serclet实现会包含内联的java注释:
resp.setContentType("image/jpeg");// 根据文件类型设置正确的内容类型 ServletOutputStream img = resp.getOutputStream();InputStream fis = getFileInputStream(); // docx文件输入流if(fis != null) { String imageId = req.getParameter("imageId"); XWPFDocument document = new XWPFDocument(fis); // 加载文档 XWPFPictureData pic = document.getPictureDataByID(imageId);// 获取图片 if (pic != null) img.write(pic.getData());}
实现自定义的图片解析器的一个主要优点是我们可以控制需要渲染的图片格式。转换的HTML在浏览器中的呈现效果如下:
如果需要在多个文档中复用同一个图片,外部图片(通过菜单插入-图片-文件名(指定图片URL)并选择“链接到文件”插入到MS Word中)更加实用。这样我们只有一个该图片的拷贝而且因为不需要将其内嵌到每个文档中还能够节省磁盘空间。这些图片会被渲染成如url所指定的HTML图片标签和源。如果我们要自定义渲染器,如渲染前的防病毒扫描,渲染的图片是否在图片格式的白名单中,我们可以重写XHTMLMapper中的“visitPicture”方法。
// 插入的外部链接图片String link = picture.getBlipFill().getBlip().getLink();PackageRelationship rel = document.getPackagePart().getRelationships().getRelationshipByID(link);if (rel != null) { String src = rel.getTargetURI().toString(); // 图片url
一旦获取到图片的URL,我们就可以基于渲染前的执行操作,如扫描、认证、授权以及允许的图片格式检查等,决定渲染行为。
我们可以进一步扩展Xdocreport,以支持更多的组件或区块。MS Word可以展示或保存在不同的布局视图中(如Web版式视图、打印视图等)。对于Web布局,HTML不应该有页边界。而且,页面背景色和页边界都可以转化成等效的HTML元素或格式。不过,仍有一些组件或区块没有等效的HTML元素,例如页码和多段落分列。
我们已经看到了如何将MS Word用作所见即所得的HTML生成器。MS Word提供了许多功能,包括可用在业务审阅和批准周期中的变更追踪。因为有可以在客户端完成编辑工作的原生应用,我们还可以离线工作。这种方式的一大优势在于通过Java可以很容易控制HTML源码的生成。从Github上可以找到相关的示例源代码。
联系客服