最近在項目中遇到一個需求,是要將各類文檔轉換為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中也可正常使用。
如果您有其他解決方案,也請一起交流一下。