程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> OpenOffice.org開源辦公套件將各類文檔轉為PDF

OpenOffice.org開源辦公套件將各類文檔轉為PDF

編輯:關於.NET

最近在項目中遇到一個需求,是要將各類文檔轉換為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中也可正常使用。

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved