Apache POI解析Word踩坑记(二)——解析OnlyOffice编辑后docx文档的图片
继续上次的话题,在使用Apache POI处理Microsoft Word文档时,我们又遇到了新问题——这次与另一个知名的三方工具OnlyOffice有关。
在我们的业务需求中,会处理OnlyOffice在线编辑后下载的文件,然而:Apache POI无法提取OnlyOffice编辑后docx文档的图片。
提取正文内容的代码和上一篇一样,可以参考上一篇。
OnlyOffice作为第三方、功能全面且核心开源的编辑器,在docx方面应该几乎完全遵守OOXML规范,而相对来说,Apache POI对Word文档的支持就不怎么完善了。不过,Apache POI下poi-ooxml-schemas包(5.x中为poi-ooxml-full)中提供了OOXML完整的映射,这个包使用XMLBeans编译,因此这一层级能比较完整地覆盖OOXML的语法(虽然无法处理上一篇提到的VML,毕竟VML不是OOXML规范的一部分)。
因此我们排查和解决问题的思路和上一篇一致,Apache POI支持不了的,使用内部的API(XmlObject的子类)访问底层的XML对象。
首先,我们去看文档XML中对应图片的片段。其中包含无法解析图片的文本片段(w:r标签)块如下:
<w:r>
<mc:AlternateContent>
<mc:Choice Requires="wpg">
<w:drawing>
...
</w:drawing>
</mc:Choice>
<mc:Fallback>
<w:pict>
...
</w:pict>
</mc:Fallback>
</mc:AlternateContent>
</w:r>
这个块包含了一个mc:AlternateContent元素。在微软Office 的.NET API 下DocumentFormat.OpenXml 中,提供了对应的AlternateContent类。根据上述说明,这个块用于处理标记兼容性的元素,当满足Requires的条件时,使用mc:Choice中的块;否则,使用mc:FullBack中的块。
具体效果,就是我们试验的这个文档在Office 2007中用了兼容模式,而Office 2016中是正常的。

图为Word 2007该篇文档,图像显示为“兼容模式”(“兼容模式”和“正常模式”图片边角显示样式的差异对比见上一篇)

图为同一个文件在Word 2016中,图像为正常模式。
找到了原因,就需要找到提取图片的方法。OOXML的每个元素都有对应的接口和实现类,其中一部分拥有Apache POI封装的各种高级外部API。例如格式一致的文本片段Run就有如下对应的实现:
| XML标签 | 接口 | 实现类 | Apache POI 上层 |
|---|---|---|---|
| w:r | CTR | CTRImpl | XWPFRun |
遗憾的是,markup-compatibility 包中的poi-ooxml-schemas标签在并没有底层的XML类,Stack Overflow上遇到类似问题的提问者和我一样打算手动处理相应的代码。
不过反编译CTRImpl类之后,我们可以看到大部分的代码都是这样的:
private static final QName DRAWING$60 = new QName("http://schemas.openxmlformats.org/wordprocessingml/2006/main", "drawing");
...
public CTDrawing getDrawingArray(int n) {
synchronized(this.monitor()) {
this.check_orphaned();
CTDrawing drawing = null;
drawing = (CTDrawing)this.get_store().find_element_user(DRAWING$60, n);
if (drawing == null) {
throw new IndexOutOfBoundsException();
} else {
return drawing;
}
}
}
public int sizeOfDrawingArray() {
synchronized(this.monitor()) {
this.check_orphaned();
return this.get_store().count_elements(DRAWING$60);
}
}
我们全局只读,check_orphaned()作为私有方法无法调用也没太大关系,那只要仿照着调用就可以了。
看过上一篇之后当然能想到,既然mc:Fullback块中是“兼容模式”的图片,和上一篇一样从中拿到v:imagedata 的r:id部分,再用ID从关系中找到对应的图片资源。但是这里我们想尝试一下不同的解法,毕竟Apache POI提供了XWPFPicture类来处理OOXML的图像元素。
XWPFPicture的构造方法是public XWPFPicture(CTPicture ctPic, XWPFRun run) 。run设置为当前的文本片段即可,那如何获取到CTPicture 对象呢?XWPFRun类提供了一个获取内部嵌入图片的私有方法:
package org.apache.poi.xwpf.usermodel;
...
/**
* XWPFRun object defines a region of text with a common set of properties
*/
public class XWPFRun implements ISDTContents, IRunElement, CharacterRun {
...
private List<CTPicture> getCTPictures(XmlObject o) {
List<CTPicture> pics = new ArrayList<>();
XmlObject[] picts = o.selectPath("declare namespace pic='" + CTPicture.type.getName().getNamespaceURI() + "' .//pic:pic");
for (XmlObject pict : picts) {
if (pict instanceof XmlAnyTypeImpl) {
// Pesky XmlBeans bug - see Bugzilla #49934
try {
pict = CTPicture.Factory.parse(pict.toString(), DEFAULT_XML_OPTIONS);
} catch (XmlException e) {
throw new POIXMLException(e);
}
}
if (pict instanceof CTPicture) {
pics.add((CTPicture) pict);
}
}
return pics;
}
我们可以借鉴此方法实现图片获取功能。
所以我们就有获取图片的obtainEmbeddedPictures()方法:
import static org.apache.poi.ooxml.POIXMLTypeLoader.DEFAULT_XML_OPTIONS;
import org.apache.poi.xwpf.usermodel.XWPFPicture;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.impl.values.XmlAnyTypeImpl;
import org.openxmlformats.schemas.drawingml.x2006.picture.CTPicture;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.impl.CTRImpl;
import org.w3c.dom.Node;
...
/**
* 获取 XWPFRun 中嵌入的图片(包括兼容图片)
*
* @param run 要检查的文本片段
* @return 图片列表
*/
public static List<XWPFPicture> obtainEmbeddedPictures(XWPFRun run) {
List<XWPFPicture> pics = run.getEmbeddedPictures();
if (pics != null && !pics.isEmpty())
return pics;
pics = new LinkedList<>();
if (run.getCTR() instanceof CTRImpl) {
QName AlternateContent = new QName("http://schemas.openxmlformats.org/markup-compatibility/2006", "AlternateContent");
CTRImpl ctr = (CTRImpl) run.getCTR();
synchronized(ctr.monitor()) {
int alternateContentCount = ctr.get_store().count_elements(AlternateContent);
if (alternateContentCount > 0) {
for (int i=0; i<alternateContentCount; i++) {
XmlObject object = (XmlObject) ctr.get_store().find_element_user(AlternateContent, i);
XmlObject[] selected = object.selectPath("declare namespace pic='" + CTPicture.type.getName().getNamespaceURI() + "' .//pic:pic");
CTPicture pic = null;
for (XmlObject pict : selected) {
if (pict instanceof XmlAnyTypeImpl) {
try {
pic = CTPicture.Factory.parse(pict.toString(), DEFAULT_XML_OPTIONS);
} catch (XmlException e) {
}
}
if (pic instanceof CTPicture)
pics.add(new XWPFPicture(pic, run));
} // for
} // for
} // if
} // synchronized
}
return pics;
}
更多推荐


所有评论(0)