http://blog.zhaojie.me/2010/05/convert-document-to-pdf-via-openoffice.html

——————————————————————————————————————————————————————————

使用OpenOffice.org将各类文档转为PDF

2010-05-27 12:37 by 老赵, 25682 visits

最近在项目中遇到一个需求,是要将各类文档转换为PDF。这应该是个很常见的工作,而且我也只需要支持MS Word,Excel,PowerPoint等常见的文档格式就行了。于是有朋友就建议了,可以使用MS Office转嘛。当然也可以使用其他方法,例如装一些PDF打印机,把文档打印成pdf文件。不过这些做法在“授权”方面似乎都有些问题。当然,我也找 了一些商业解决方案(如Aspose)保底,咋看之下它的授权方式也并不算贵。不过现在看来,OpenOffice.org已经能够满足我的需求了。如果您有更好的做法也请告诉我。

OpenOffice.org是个开源的办公套件,提供了与MS Word,Excel,PowerPoint等对应的多个软件,在很多时候倒也足够使用。更重要的是,它支持包括MS Office 2007在内的多种格式,并且能够将其导出为PDF文件,再加上它的授权方式是LGPL,在生产环境里使用自然也不会有什么明显的限制了。此外,OOo本身也有相当多的开发文档,我对完成这个工作还是很有信心的——但我没想到的是,这过程还真不如想象中那么顺利。

编译通过也不容易

首先,我安装了OpenOffice.org主程序以及SDK。SDK随带一些示例代码,其中DocumentHandling部分正 好包含一个我需要的DocumentConverter功能。于是我打开Eclipse,倒入这个文件,很显然会出现无数错误提示:还没有引入合适的类库 嘛。那么我该引用哪些jar包呢?根据其他一些搜索到的零碎的资料提示,我该引入的是一些放在~\Basis\program\classes下的几个 jar包,比如unoil.jar、juh.jar……等等,这个包在什么地方?事实上,我在这么目录下唯独只找到unoil.jar这个独苗。莫名之 余,我一股脑的将目录中的30多个jar包全部引入,可是错误依旧。

我就蒙了,在搜索引擎里不断地用juh.jar相关的关键字进行查询,希望可以找到一些提示,一无所获。然后我动用了系统中的文件搜索, 在~/Basis目录中查找*.jar,还是没有发现juh.jar的踪影。于是我很沮丧,怎么第一步也这么不顺利。直到大约过了一个小时后,我才无意间 在~\URE\java目录下发现了那几个关键的jar包。引入后我长吁一口气:示例代码终于编译通过了。概括来说,如果需要让 DocumentConverter.java编译通过,需要引入一下三个jar包:

  • ~\URE\java\juh.jar
  • ~\URE\java\jurt.jar
  • ~\Basis\program\classes\unoil.jar

真是痛恨文档和实际现象不符的情况,消耗时间不说,心情也变糟糕了。

整理示例代码

不得不说,DocumentConverter.java真不能算是段优秀的示例代码。首先,它并没有很好地起到示范的作用。我理想中的示例代码应 该能够清晰地说明工作的方式和步骤,而不会添加太多额外的内容。这段示例代码的效果是“转化指定目录中的所有文件”,还用到了递归。再加上它没有 import任何类型,每个类型在使用时都拖着长长的“com.sun.star”,这让原本就十分冗余的Java代码变得更为难以理解。更别说注释与代 码本身的冲突,还有多余的类型强制转换等问题。为此,我根据文档说明,重新改写了一下示例代码,将整个过程拆分为多个步骤。

首先,我们打开并连接一个OOo程序,这需要创建一个XComponentContext对象:

private static XComponentContext createContext() throws Exception {
    // get the remote office component context
    return Bootstrap.bootstrap();
}

然后创建一个XComponentLoader对象:

private static XComponentLoader createLoader(XComponentContext context) throws Exception {
    // get the remote office service manager
    XMultiComponentFactory mcf = context.getServiceManager();
    Object desktop = mcf.createInstanceWithContext("com.sun.star.frame.Desktop", context);
    return UnoRuntime.queryInterface(XComponentLoader.class, desktop);
}

从Loader对象可以加载一篇文档:

private static Object loadDocument(XComponentLoader loader, String inputFilePath) throws Exception {
    // Preparing properties for loading the document
    PropertyValue[] propertyValues = new PropertyValue[1];
    propertyValues[0] = new PropertyValue();
    propertyValues[0].Name = "Hidden";
    propertyValues[0].Value = new Boolean(true);
    
    // Composing the URL by replacing all backslashs
    File inputFile = new File(inputFilePath);
    String inputUrl = "file:///" + inputFile.getAbsolutePath().replace('\\', '/');

    return loader.loadComponentFromURL(inputUrl, "_blank", 0, propertyValues);
}

接着自然就是文档转换了:

private static void convertDocument(Object doc, String outputFilePath, String convertType) throws Exception {
    // Preparing properties for converting the document
    PropertyValue[] propertyValues = new PropertyValue[2];
    // Setting the flag for overwriting
    propertyValues[0] = new PropertyValue();
    propertyValues[0].Name = "Overwrite";
    propertyValues[0].Value = new Boolean(true);
    // Setting the filter name
    propertyValues[1] = new PropertyValue();
    propertyValues[1].Name = "FilterName";
    propertyValues[1].Value = convertType;
    
    // Composing the URL by replacing all backslashs
    File outputFile = new File(outputFilePath);
    String outputUrl = "file:///" + outputFile.getAbsolutePath().replace('\\', '/');
    
    // Getting an object that will offer a simple way to store
    // a document to a URL.
    XStorable storable = UnoRuntime.queryInterface(XStorable.class, doc);
    // Storing and converting the document
    storable.storeAsURL(outputUrl, propertyValues);
}

最后还要关闭文档:

private static void closeDocument(Object doc) throws Exception {
    // Closing the converted document. Use XCloseable.clsoe if the
    // interface is supported, otherwise use XComponent.dispose
    XCloseable closeable = UnoRuntime.queryInterface(XCloseable.class, doc);
    
    if (closeable != null) {
    	closeable.close(false);
    } else {
        XComponent component = UnoRuntime.queryInterface(XComponent.class, doc);
        component.dispose();
    }
}

最后便是将上面四个步骤串联起来:

public static void main(String args[]) {
    String inputFilePath = "D:\\convert\\input.txt";
    String outputFilePath = "D:\\convert\\output.doc";
    
    // the given type to convert to
    String convertType = "swriter: MS Word 97";
    
    try {
        XComponentContext context = createContext();
        System.out.println("connected to a running office ...");
        
        XComponentLoader compLoader = createLoader(context);
        System.out.println("loader created ...");
        
        Object doc = loadDocument(compLoader, inputFilePath);
        System.out.println("document loaded ...");
        
        convertDocument(doc, outputFilePath, convertType);
        System.out.println("document converted ...");
        
        closeDocument(doc);
        System.out.println("document closed ...");
        
        System.exit(0);
    } catch (Exception e) {
        e.printStackTrace(System.err);
        System.exit(1);            
    }
}

总体来说,虽然OOo并没有提供优雅的API,但是它的主要“套路”还是比较容易摸索出来的:加载文档,使用 UnoRuntime.queryInterface方法获取各种操作接口,而各种参数都通过PropertyValue数组来提供。如果您像我一样感觉 不爽,重新作一层简单的封装也是十分容易的。

运行中的问题

到目前为止,我们只是重新整理了示例代码,还没有开始运行。当第一次运行的时候便发现有异常抛出:

com.sun.star.comp.helper.BootstrapException: no office executable found!
	at com.sun.star.comp.helper.Bootstrap.bootstrap(Bootstrap.java:246)
	at jeffz.practices.AnyToDoc.createContext(AnyToDoc.java:19)
	at jeffz.practices.AnyToDoc.main(AnyToDoc.java:87)

不过有异常信息之后,查找解决方案一般也很容易(但就我个人经验来说, 还是有很多朋友会问“抛出XX异常该怎么办”之类的问题)。经过搜索,发现遇到这个问题的人还不少,他们把juh.jar等文件复制到OOo安装目录外 (这在生产环境中几乎是必然的)之后便会产生这个异常,但如果直接引用OOo安装目录内的jar便不会有问题了——但是我目前是直接引用OOo安装目录的 jar包,不是吗?但我转念一想,我当时为编译通过而挣扎的原因,不就是“juh.jar”等文件不在它本该在的位置吗?既然这个问题和jar包与OOo 程序的相对路径有关,那么如果我把jar包放回“原来”的位置,这个问题可能就不存在了。

不过这些只是推测,我没有去进行尝试。因为既然在生产环境中还是会破坏路径问题,那我还是找一下这个问题的解决方案吧。最终在OOo的论坛上找到了答案:有人提供了一个补充包bootstrapconnector.jar,其中提供了一个方法可以让我们指定OOo的程序目录。也就是说,我们需要把之前的createContext改写成:

private static XComponentContext createContext() throws Exception {
    // get the remote office component context
    // return Bootstrap.bootstrap();
    String oooExeFolder = "C:/Program Files/OpenOffice.org 3/program/";
    return BootstrapSocketConnector.bootstrap(oooExeFolder);
}

当然,生产环境中您一般不会使用硬编码的方式制定路径,您可以把它放在配置文件或是系统变量里。再次运行即告成功。这段代码会将一个txt文件转化成旧有的Word格式,事实上您可以将txt替换成OOo所支持的任何一种格式,比如rtf,docs,odt等等。

那么接下来的问题便是,如何将目标格式改为PDF文件?很显然,目标格式是Word文件,是因为我们将类型字符串指定为“swriter: MS Word 97”,那么PDF格式是多少?这靠猜测是没法得出结果的,最后还是从一篇文档中得到了答案:writer_pdf_Export。事实上,这么做还是不够,代码还是会在storeAsURL方法中抛出异常,而且这是一个泛泛的ErrorCodeIOException,没有具体信息(message为空)。又一阵好找,才发现storeAsURL对应着OOo的“Save as”功能,而如果是“Export”功能,则应该调用storeToURL方法。

最后,我们终于成功地将其他格式转化为PDF文件了。

完整代码

在这里贴出“txt转pdf”完整的可运行的示例代码:

import java.lang._;
import java.io.File;
import ooo.connector.BootstrapSocketConnector;
import com.sun.star.lang.XComponent;
import com.sun.star.uno.XComponentContext;
import com.sun.star.uno.UnoRuntime;
import com.sun.star.beans.PropertyValue;
import com.sun.star.frame.XComponentLoader;
import com.sun.star.frame.XStorable;
import com.sun.star.util.XCloseable;

object AnyToPdf extends Application {
  
  // get the remote office component context
  def createContext() : XComponentContext = {
    val oooExeFolder = "C:/Program Files/OpenOffice.org 3/program/"
    BootstrapSocketConnector.bootstrap(oooExeFolder)
  }
  
  def createComponentLoader(context: XComponentContext) : XComponentLoader = {
    // get the remote office service manager
    val mcf = context.getServiceManager()
    val desktop = mcf.createInstanceWithContext("com.sun.star.frame.Desktop", context)
    UnoRuntime.queryInterface(classOf[XComponentLoader], desktop)
  }
  
  def loadDocument(loader: XComponentLoader, inputFilePath: String) : Object = {
    // Preparing properties for loading the document
    val propertyValue = new PropertyValue()
    propertyValue.Name = "Hidden"
    propertyValue.Value = new Boolean(true)
    
    // Composing the URL by replacing all backslashs
    val inputFile = new File(inputFilePath)
    val inputUrl = "file:///" + inputFile.getAbsolutePath().replace('\\', '/')
    loader.loadComponentFromURL(inputUrl, "_blank", 0, Array(propertyValue))
  }
  
  def convertDocument(doc: Object, outputFilePath: String, convertType: String) {
    // Preparing properties for converting the document
    // Setting the flag for overwriting
    val overwriteValue = new PropertyValue()
    overwriteValue.Name = "Overwrite"
    overwriteValue.Value = new Boolean(true)
    // Setting the filter name
    val filterValue = new PropertyValue()
    filterValue.Name = "FilterName"
    filterValue.Value = convertType
    
    // Composing the URL by replacing all backslashs
    val outputFile = new File(outputFilePath)
    val outputUrl = "file:///" + outputFile.getAbsolutePath().replace('\\', '/')
    
    // Getting an object that will offer a simple way to store
    // a document to a URL.
    val storable = UnoRuntime.queryInterface(classOf[XStorable], doc)
    // Storing and converting the document
    storable.storeToURL(outputUrl, Array(overwriteValue, filterValue))
  }
  
  def closeDocument(doc: Object) {
    // Closing the converted document. Use XCloseable.clsoe if the
    // interface is supported, otherwise use XComponent.dispose
    val closeable = UnoRuntime.queryInterface(classOf[XCloseable], doc)
    if (closeable != null) {
      closeable.close(false)
    } else {
      val component = UnoRuntime.queryInterface(classOf[XComponent], doc)
      component.dispose()
    }
  }
  
  val inputFilePath = "D:\\convert\\input.txt"
  val outputFilePath = "D:\\convert\\output.pdf"
		
  // Getting the given type to convert to
  val convertType = "writer_pdf_Export"
  
  val context = createContext()
  println("connected to a running office ...")
  
  val loader = createComponentLoader(context)
  println("loader created ...")
  
  val doc = loadDocument(loader, inputFilePath)
  println("document loaded ...")
  
  convertDocument(doc, outputFilePath, convertType)
  println("document converted ...")
  
  closeDocument(doc)
  println("document closed ...")
}

很显然,这里不是我所厌恶的Java语言。这是一段Scala代码,就从最基本的代码使用上看,Scala也已经比Java代码要节省许多了。

总结

其实解决这个问题还是走了不少弯路的,究其原因可能是从示例代码出发去寻找解决方案,而并没有去系统地阅读各种资料。在这个过程中,我找到了一些比较重要的文档:

当然,最详细文档莫过于完整的开发人员指南了,如果您想要详细了解这方面的内容,这应该也属于必读内容之一。

有了OpenOffice.org,就相当于我们拥有了一套完整的文档操作类库,可以用来实现各种功能。除了转PDF以外,例如我们还可以将一篇数 百万字的小说加载为文档,再每十页导出一份图片,方便用户在线预览顺便防点拷贝。此外,虽然我是在Windows下操作OOo,但是OOo和Java本身 都是跨平台的,因此同样的代码也可以运行在Linux平台上。我目前正在尝试在Ubuntu Server上部署一份OOo和代码,如果有什么特别的情况,我也会另行记录。

事实上有一点我之前一直没有提到:如果您使用Windows及.NET进行开发,OOo也提供了C++/CLI接口,可以使用C#、F#进行编程, 且代码与本文描述的几乎如出一辙(只要把queryInterface方法调用改成直接转换),在.NET 4.0中也可正常使用。

如果您有其他解决方案,也请一起交流一下。

posted on 2015-12-06 20:35  网络大豆  阅读(948)  评论(0编辑  收藏  举报