继续上次的话题,在使用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支持不了的,使用内部的APIXmlObject的子类)访问底层的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;
	}
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐