65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Open Office 和 Ghostscript 批量转换和合并多个文档为 PDF

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.63/5 (9投票s)

2008年6月1日

CPOL

11分钟阅读

viewsIcon

94719

downloadIcon

706

一个如何使用 Open Office 和 Ghostscript 将支持的格式转换为 PDF,并使用 Ghostscript 合并它们的示例

引言

使用 Open Office,可以批量将多种文档转换为 PDF,包括 Microsoft Office Word、Excel 和 Powerpoint、纯文本文件、Open Office 文档、JPEG、GIF 等等。通过 UNO (Unified Network Objects),即 Open Office 组件模型,转换过程可以连接到正在运行的 Open Office 后台实例,加载文档并将其保存为 PDF。本文及附带的 Java 程序展示了如何对多个输入文件执行此操作,然后选择性地使用 GPL Ghostscript 将结果合并为单个 PDF 文件。

Using the Code

该程序是一个命令行 Java 程序。它在 Windows XP 上使用 NetBeads IDE 开发和测试,并在 Fedora Core 7 和 Ubuntu 8.04 上仅使用命令行 javacCLASSPATH 构建和测试。在所有情况下都使用了 Sun JDK 1.6。该程序附带一个配置文件,必须与 pdfcm 包捆绑在一起。(有关编译和要求的详细信息,请参阅“要求和编译细节”部分。)

Usage: java -jar pdfcm.Main [-m mergeFile] [-d] file1 [file2 [file3...]]
Usage (jarfile): java -jar pdfcm.jar [-m mergeFile] [-d] file1 [file2 [file3...]]

    Converts all given input files to PDF.  Output filenames have the same base
    filenames as the input files and the extension "pdf".  PDF files on the
    input are not processed.

    INPUT OPTIONS

    -m mergeFile
        Causes converted PDF files and existing PDF (unprocessed) files on the
        input to be merged into a single PDF file given by mergeFile as a final
        step.

    -d
        Causes input files to be removed after successful processing.  When used
        in conjunction with the -m option, all intermediate files as well as any
        PDF files on the input are removed after successful processing.

    In case of a name collision between an input filename and the merge filename,
    in the case that the -d option is given, the collision will be resolved.  If
    the -d option is not given, then an error will be generated to prevent accidental
    overwrite of a file.

关注点

执行 PDF 转换的类是 PDFConvert 类。简而言之,它循环遍历输入文件,使用 Open Office 以批处理模式打开每个文件,并使用适当的过滤器将每个文件导出为 PDF。之后,如果指定了选项,它会收集生成的 PDF 文件并使用 GPL Ghostscript 将它们合并成一个大的 PDF 文件。文件类型仅通过文件名扩展名识别,并在配置文件中映射到 Open Office 过滤器。

使用 Open Office 进行 PDF 转换

为了方便读者在阅读时进行剪切和粘贴,必须包含以下包。

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

转换方法的签名是 public boolean DoConvert(String[] inputFiles),其中数组 inputFiles 包含命令行上给出的输入文件列表,并已调整为包含其完整的绝对路径。第一步是建立与 Open Office 的连接。在实际运行程序之前,请确保 Open Office 已正确安装在系统上,并验证您或预期用户可以使用 Open Office 创建文档。在使用程序之前,请务必点击任何“首次使用”注册屏幕。

用于与 Open Office 通信的类是

// Declare Open Office components
XComponentContext xContext = null;
XMultiComponentFactory xMCF = null;
XComponentLoader xComponentLoader = null;
XStorable xStorable = null;
XCloseable xCloseable = null;
Object desktop = null;
Object document = null;

有关上述每个组件的详细信息,请参阅 documentation.openoffice.org 上的 Open Office SDK 文档。就我们的目的而言,可以说这些对象封装了我们与 Open Office 通信所需的 API。在下一个代码片段中,使用 BootstrapSocketConnector 连接到 Open Office。Open Office 将在另一个进程中运行并监听一个套接字,而 BootstrapSocketConnector 负责连接细节。BootStrapConnector 包含在一个 jarfile 中,该 jarfile 经 Open Office 论坛的常驻贡献者 Hol.Sten 许可,随源代码一起提供。Open Office 论坛中有许多关于连接替代方案的优秀资源,但除非要运行多个 Open Office 实例,或者正在使用另一台服务器上的 Open Office 实例,或者有任何其他特殊要求,否则只需要引导连接器。

要理解接下来的代码片段,请记住转换状态保存在 PDFConvert 对象的两个属性中:StatusTextIsError。为了在循环处理过程中跟踪非致命错误和状态,消息会进一步累积到 StringBuffer buf 中,并在最后填充到 StatusText 中。

// Try to get reference to an Open Office process
try {
    // Should use OO installation lib/programs directory on your system
    String ooLibFolder = ooLibPath;

    // Load the Open Office context
    xContext = BootstrapSocketConnector.bootstrap(ooLibFolder);

    // Load the Open Office object factory
    xMCF = xContext.getServiceManager();

    // Get a desktop instance
    desktop = xMCF.createInstanceWithContext(
                "com.sun.star.frame.Desktop", xContext);

    // Get a reference to the desktop interface that can load files
    xComponentLoader = (XComponentLoader) UnoRuntime.queryInterface(XComponentLoader.class, desktop);

    } catch (Exception ex) {
    // Open Office error
    statusText = "Could not get usable OpenOffice: " + ex.toString();
    isError = true;
    return false;
}

此处抛出的异常几乎可以肯定是由于 Open Office 安装或配置错误,或与 Open Office 的连接错误造成的。程序除了报告无法连接到 Open Office 之外,实际上什么也做不了,然后可以重新配置/重新安装/重启 Open Office,然后您可以重试。为了调试 Open Office 连接问题,请再次确保预期用户可以离线运行 Open Office 并打开预期文件,而不会出现错误或弹出注册窗口。

接下来的代码段位于输入文件的循环处理中,也出现在 try {...} catch {...} finally {...} 块中。在以下代码段中,文件扩展名在 String 变量 ext 中,输入文件名在通过 i 索引的数组 inputFiles 中。打开文件时,我们请求 Open Office 不要打开文档窗口,因为我们正在以批处理模式运行。

// Set the document opener to not display an OO window
PropertyValue[] loaderValues = new PropertyValue[1];
loaderValues[0] = new PropertyValue();
loaderValues[0].Name = "Hidden";
loaderValues[0].Value = new Boolean(true);

然后我们将当前文件名转换为 Open Office 所需的 URI 格式。

// Convert file path to URL name format and escape spaces
String docURL = "file:///" + inputFiles[i]
.replace(File.separatorChar, '/')
.replace(" ", "%20");
lastDot = docURL.lastIndexOf('.');

接下来,文档被打开并获得一个接口,该接口可以使用适合文件类型的过滤器存储文件。然后程序将结果保存到同名但扩展名为 PDF 的文件中,并将文件名附加到已转换文件列表中。如果文件是“原生”类型之一,例如它已经是 PDF 文件,则直接将其添加到列表中。

// If it is already PDF, add it to the list of files to "converted" files
if (StringArrayContains(nativeTypes, ext)) {
    convertedFiles.add(docURL);
    } else {
    // Open the document in Open Office
    document = xComponentLoader.loadComponentFromURL(
    docURL, "_blank", 0, loaderValues);

    // Get a reference to the document interface that can store files
    xStorable = (XStorable) UnoRuntime.queryInterface(
    XStorable.class, document);

    // Set the arguments to save to PDF.
    PropertyValue[] saveArgs = new PropertyValue[2];
    saveArgs[0] = new PropertyValue();
    saveArgs[0].Name = "Overwrite";
    saveArgs[0].Value = new Boolean(true);

    // Choose appropriate output filter
    saveArgs[1] = new PropertyValue();
    saveArgs[1].Name = "FilterName";
    if (StringArrayContains(writerTypes, ext)) {
    saveArgs[1].Value = "writer_pdf_Export";
    } else if (StringArrayContains(calcTypes, ext)) {
    saveArgs[1].Value = "calc_pdf_Export";
    } else if (StringArrayContains(drawTypes, ext)) {
    saveArgs[1].Value = "draw_pdf_Export";
    } else {
    buf.append("File " + i + " has unknown extension: " + ext);
    isError = true;
    continue;  // Skip to the next file
}

// The converted file will have the same name with a PDF extension
String sSaveUrl = docURL.substring(0, lastDot) + ".pdf";

// Save the file
xStorable.storeToURL(sSaveUrl, saveArgs);

处理了各种异常,但完成时关闭文件总是很重要的,因此执行此操作的代码会放入 finally 块中。按照推荐的做法,使用 XCloseable 接口关闭文档。然而,人们注意到 XClosable 可能为 nullXCloseable.close() 可能抛出异常。在这些情况下,捕获异常并使用 XComponent 接口显式处理对象。问题在于多个客户端可能正在使用同一个文档。例如,如果文档正在排队打印或某个模态对话框已打开,Open Office 可能会在尝试关闭文档时抛出 CloseVetoException。这不与此处的用例重叠,因此文档只是被处理。

finally {
    // Make sure the file is closed before going to the next one
    if (document != null) {
        // Get a reference to the document interface that can close a file
        xCloseable = (XCloseable) UnoRuntime.queryInterface(
        XCloseable.class, document);

        // Try to close it or explicitly dispose it
        // See http://doc.services.openoffice.org/wiki/Documentation/
        //          DevGuide/OfficeDev/Closing_Documents
        if (xCloseable != null) {
        try {
            xCloseable.close(false);
        } catch (com.sun.star.util.CloseVetoException ex) {
        XComponent xComp = (XComponent) UnoRuntime.queryInterface(
        XComponent.class, document);
        xComp.dispose();
    }
    } else {
        XComponent xComp = (XComponent) UnoRuntime.queryInterface(
        XComponent.class, document);
        xComp.dispose();
    }
}
document = null;   // Javanauts, please pardon my CSharpery    

使用 Ghostscript 合并

转换后的文件保存在 convertedFiles 数组中。要合并这些文件,需要构建一个命令行传递给操作系统,因此文件名必须转换回原生操作系统格式。请注意,将裸 Postscript 文件和 PDF 文件都提供给 GPL Ghostscript 是可以的,因此 Postscript 被包含为一种“原生”文件类型。由于 Windows 和 Unix 之间处理路径名中空格的方式存在一些奇怪之处,因此此处不复制代码。

然后使用构建的命令行打开一个进程,程序等待它完成。请注意,需要处理输出和错误流,以使子进程干净地完成。尽管可能有更优雅的方法,但对于此用例,这些 stream 只是被关闭,并且只保留退出代码。

try {
    // Execute the command
    Process mProc = Runtime.getRuntime().exec(cmd.toString());

    // Voodoo - In order to wait for an external process, you
    // have to handle its stdout(getInputStream) and stderr (getErrorStream)
    // I'm just going to close them as I'm only interested in if it succeeded or not
    InputStream iStr = mProc.getInputStream();
    iStr.close();
    InputStream eStr = mProc.getErrorStream();
    eStr.close();

    // Now wait
    int exCode = mProc.waitFor();
    if ( exCode == 0 ){
        buf.append("Merge succeeded: exit code was zero.");
    }
else {
        isError = true;
        buf.append("Merge failed: exit code was " + exCode);
    }

    } catch (java.io.IOException ex) {
    buf.append("Merge failed: " + ex.toString());
    isError = true;
    statusText = buf.toString();
    return false;
    } catch (java.lang.InterruptedException ex) {
    buf.append("Merge interrupted: " + ex.toString());
    isError = true;
    statusText = buf.toString();
    return false;
}

请注意,Ghostscript 是一个受人尊敬且维护良好的程序。如果程序在此步骤失败,那么几乎可以肯定是由于 GPL Ghostscript 安装或配置错误,或者输入文件有问题。与 Open Office 一样,这可以通过在命令 shell 中对可疑输入显式运行 GPL Ghostscript 来离线检查,因此无需重现确切的错误消息。

其他程序细节

-d 功能导致在完成时删除输入或中间文件。这对于后台处理情况非常有用,您不希望杂乱堆积,也不希望担心敏感信息被无意中泄露。对于此功能和使用说明文本中描述的其他程序功能,请参阅 Main 源代码和使用说明文本。

要求和编译细节

Open Office

Open Office 可以从 www.openoffice.org 获取。此程序已在 Windows XP 上使用 Open Office 2.3.0 版、在 Fedora Core 7 上使用 2.4.0 版以及在 Ubuntu 8.04 上使用 2.4.0 版进行测试。Open Office 的早期版本具有创建 PDF 的能力,只要使用某种 Open Office 2.X 版,此代码中使用的连接机制就应该受支持。但是,此代码尚未针对早期 Open Office 版本构建或测试。(有关更多信息,请参阅 Open Office 论坛网站上 此帖子 中引用的主题。)

GPL Ghostscript

GPL Ghostscript 可以从 pages.cs.wisc.edu/~ghost 获取。此代码已在 Windows 上使用 Ghostscript 8.6.1 版、在 Fedora Core 7 上使用 8.6.2 版以及在 Ubuntu 8.04 上使用 8.6.2 版进行测试。只要支持命令行选项 -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite,GPL Ghostscript 的早期版本就应该可以工作。注意:ESP Ghostscript 8.15 版(某些 Linux 版本附带的标准版本)在此功能上不起作用,尽管它支持上述选项。请参阅下面的进一步讨论。

bootstrapconnector.jar

bootstrapconnector.jar 是 Hol.sten 在 Open Office 开发者论坛上创建的,用于连接 Open Office 实例的简化机制。为了方便起见,此处将其包含在源代码中,但最终可从 此帖子 获取。

设置您的构建环境

该代码已在 Fedora Core 7、Ubuntu 8.04 和 Windows XP SP2 上使用 Sun JDK 1.6 编译和测试。它将无法使用非 Java 1.5 兼容版本的 GCJ 进行编译或工作。确保 CLASSPATHIDE 构建环境包含 Open Office jarfile juh.jarjurt.jarjut.jarridl.jarunoil.jarofficebean.jar。通常,这些 jarfile 可以在您的 Open Office 的 program/classes 子目录中找到。当然,bootstrapconnector.jar 必须包含在 CLASSPATH 中。

配置

config.properties 文件中,需要为您的系统设置几个配置常量。

  • ooLibPath 是您的 Open Office 安装位置,并且该文件夹中应该有一个名为 classes 的文件夹。(例如,在标准 2.3.0 安装的 Windows 系统上,这是 C:\Program Files\OpenOffice.org 2.3\program。)
  • gsExePath 是 GPL Ghostscript 可执行文件的目录。(例如,在标准安装的 Windows 系统上,这是 C:\Program Files\gs\gs8.61\bin。)
  • gsExeName 是 Ghostscript 可执行文件的名称。(在 Windows 上,它必须是随附的两个在控制台窗口中运行的可执行文件之一,因此在 Windows 系统上是 gswin32c.exe。在 UNIX 系统上,通常只是 gs。)
  • shellCommandStyle 可以在两种构建 GPL Ghostscript 命令行的技巧之间进行选择。doubleQuoted 将使其构建为使用适用于 Microsoft Windows 的双引号处理文件名和路径中的空格,而 escapeSpaces 将使其构建为使用反斜杠以 Unix 样式处理空格。
  • writerTypes 是文件扩展名列表,小写并以逗号分隔,包括开头的 '.',预计由 Open Office Writer 处理。
  • calcTypes 是文件扩展名列表,小写并以逗号分隔,包括开头的 '.',预计由 Open Office Calc 处理。
  • drawTypes 是文件扩展名列表,小写并以逗号分隔,包括开头的 '.',预计由 Open Office Draw 处理。此类别目前似乎包括演示文稿类型,因此没有单独的 Open Office Impress 类别。
  • nativeTypes 是文件扩展名列表,小写并以逗号分隔,包括开头的 '.',这些文件要么已经是 PDF,要么可以直接由 GPL Ghostscript 处理,例如 Postscript。

可能还有很多,但这些是流行的。

讨论

如上所述,当使用 ESP Ghostscript 8.15 时,合并过程失败。症状是,对于几个测试文件,ESP Ghostscript 开始将 CPU 占用率固定在 100%,并且创建输出的速度非常慢,在 2.7 GHz 双核机器上每秒只有几字节。GPL Ghostscript 代码库在 GPL 8.57 版时与 ESP Ghostscript 8.15 合并,因此 GPL 8.61 无论如何都可以被视为更新的产品。

在处理纯文本文件时,请注意 Windows 和 Unix 风格行结束符之间的差异。虽然在单个平台上没有问题,但在 Windows 上使用 CRLF 行结束符创建的文本文件在 Unix 上使用 Open Office 尝试打开时将作为 Calc 文件打开。在交互模式下,这会进一步产生弹出对话框以询问 import 参数的令人愉快的副作用,而在批处理模式下,它会导致上述 XStorable 接口在文件处理期间为 null

这个例子中省略了一些细节。尽管如此,这个程序正在生产中用于后台处理。特别是,对于一般用途

  • 应加强文件名处理,以考虑其他特殊字符和空格。
  • 应扩展和测试已处理的文件类型列表。
  • 错误处理和报告应更具信息性。

然而,这段代码的投资回报率已经到了收益递减的程度。程序本身只是一个粘合剂,真正的用处在于 Open Office 和 Ghostscript。

历史

  • 2008年6月1日:首次发布
© . All rights reserved.