使用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代码要节省许多了。
总结
其实解决这个问题还是走了不少弯路的,究其原因可能是从示例代码出发去寻找解决方案,而并没有去系统地阅读各种资料。在这个过程中,我找到了一些比较重要的文档:
- 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中也可正常使用。
如果您有其他解决方案,也请一起交流一下。