使用OpenOffice.org将各类文档转为PDF
2010-05-27 12:37 by 老赵, 27883 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代码要节省许多了。
总结
其实解决这个问题还是走了不少弯路的,究其原因可能是从示例代码出发去寻找解决方案,而并没有去系统地阅读各种资料。在这个过程中,我找到了一些比较重要的文档:
- API/Tutorials/PDF export:对于PDF导出功能各种参数的详细解释。
- Text Documents:关于文本文档相关操作的详细说明。
- DocumentHanding:“文档操作”示例代码的解释,包括文档打印等等。
当然,最详细文档莫过于完整的开发人员指南了,如果您想要详细了解这方面的内容,这应该也属于必读内容之一。
有了OpenOffice.org,就相当于我们拥有了一套完整的文档操作类库,可以用来实现各种功能。除了转PDF以外,例如我们还可以将一篇数百万字的小说加载为文档,再每十页导出一份图片,方便用户在线预览顺便防点拷贝。此外,虽然我是在Windows下操作OOo,但是OOo和Java本身都是跨平台的,因此同样的代码也可以运行在Linux平台上。我目前正在尝试在Ubuntu Server上部署一份OOo和代码,如果有什么特别的情况,我也会另行记录。
事实上有一点我之前一直没有提到:如果您使用Windows及.NET进行开发,OOo也提供了C++/CLI接口,可以使用C#、F#进行编程,且代码与本文描述的几乎如出一辙(只要把queryInterface方法调用改成直接转换),在.NET 4.0中也可正常使用。
如果您有其他解决方案,也请一起交流一下。
可能用 NetBeans 开发 OOo 的痛感比 Eclipse 会小一些……