如何使用 PDFBox 2.0.0 从 PDF 文件签署 InputStream

How to sign an InputStream from a PDF file with PDFBox 2.0.0

我想在不使用临时文件的情况下从 PDF 文件签署 InputStream。
在这里,我将 InputStream 转换为 File 并且工作正常:

InputStream inputStream = this.signatureObjPAdES.getSignatureDocument().getInputStream();
OutputStream outputStream = new FileOutputStream(new File("C:/temp.pdf"));
int read = 0;
byte[] bytes = new byte[1024];

while ((read = inputStream.read(bytes)) != -1) {
    outputStream.write(bytes, 0, read);
}

PDDocument document = PDDocument.load(new File("C:/temp.pdf"));

...

document.addSignature(new PDSignature(this.dts.getDocumentTimeStamp()), this);
document.saveIncremental(new FileOutputStream("C:/result.pdf");
document.close();

但我想直接这样做:

PDDocument document = PDDocument.load(inputStream);

问题:在 运行

Exception in thread "main" java.lang.NullPointerException
at java.io.RandomAccessFile.<init>(Unknown Source)
at org.apache.pdfbox.io.RandomAccessBufferedFileInputStream.<init>(RandomAccessBufferedFileInputStream.java:77)
at org.apache.pdfbox.pdmodel.PDDocument.saveIncremental(PDDocument.java:961)

欢迎所有想法。
谢谢。

编辑: 它现在正在与 PDFBox 2.0.0 的发布一起工作。

原因

直接障碍在于方法PDDocument.saveIncremental()本身:

public void saveIncremental(OutputStream output) throws IOException
{
    InputStream input = new RandomAccessBufferedFileInputStream(incrementalFile);
    COSWriter writer = null;
    try
    {
        writer = new COSWriter(output, input);
        writer.write(this, signInterface);
        writer.close();
    }
    finally
    {
        if (writer != null)
        {
            writer.close();
        }
    }
}

(PDDocument.java)

第一行中使用的成员 incrementalFile 仅在带有 File 参数的 PDDocument.load 期间设置。

因此,无法使用此方法。

解决方法

幸运的是,方法 PDDocument.saveIncremental() 仅使用公开可用的方法和值,唯一的例外是 signInterface,但您知道它的值,因为您在代码中的前一行中设置了它saveIncremental 调用:

document.addSignature(new PDSignature(this.dts.getDocumentTimeStamp()), this);
document.saveIncremental(new FileOutputStream("C:/result.pdf"));

因此,您可以在代码中执行等效的操作而不是调用 PDDocument.saveIncremental()

为此,您还需要 InputStream input 的替换值。它需要 return 一个内容与

中的 inputStream 相同的流
PDDocument document = PDDocument.load(inputStream);

因此您需要使用该流两次。由于您没有说 inputStream 是否可以重置,我们将首先将其复制到 byte[] 中,我们将其转发给 PDDocument.loadnew COSWriter.

因此,替换您的

PDDocument document = PDDocument.load(inputStream);

...

document.addSignature(new PDSignature(this.dts.getDocumentTimeStamp()), this);
document.saveIncremental(new FileOutputStream("C:/result.pdf"));
document.close();

来自

byte[] inputBytes = IOUtils.toByteArray(inputStream);
PDDocument document = PDDocument.load(new ByteArrayInputStream(inputBytes));

...

document.addSignature(new PDSignature(this.dts.getDocumentTimeStamp()), this);
saveIncremental(new FileOutputStream("C:/result.pdf"),
    new ByteArrayInputStream(inputBytes), document, this);
document.close();

并在您的 class 中添加一个新方法 saveIncremental,灵感来自原始 PDDocument.saveIncremental():

void saveIncremental(OutputStream output, InputStream input, PDDocument document, SignatureInterface signatureInterface) throws IOException
{
    COSWriter writer = null;
    try
    {
        writer = new COSWriter(output, input);
        writer.write(document, signatureInterface);
        writer.close();
    }
    finally
    {
        if (writer != null)
        {
            writer.close();
        }
    }
}

在旁边

我上面说了

As you have not said whether that inputStream can be reset, we'll first copy it into a byte[] which we forward both to PDDocument.load and new COSWriter.

实际上还有一个这样做的原因:COSWriter.doWriteSignature() 像这样检索原始 PDF 的长度:

long inLength = incrementalInput.available();

(COSWriter.java)

InputStream.available() 的文档指出:

Note that while some implementations of InputStream will return the total number of bytes in the stream, many will not.

要重新使用inputStream而不是像上面那样使用byte[]ByteArrayInputStream,因此,inputStream不仅需要支持reset()但也需要成为少数InputStream实现之一,其中return流中的总字节数为available

FileInputStreamByteArrayInputStream 都 return 流中的总字节数为 available.

使用通用 InputStream 而不是这两个时,可能还会有更多问题。

Cyril Bremaud,你可以使用这种方法,因为PDDocument class有3个重载构造函数,你可以继续,如果你愿意,只提供文件路径,它也会工作。但是为了能够将 InputStream 直接传递给 PDDocument 构造函数,请使用以下代码:

lStrInputPDFfile = "samples_pdf_signing\Country Calendar.pdf";
lOsPDFInput = new java.io.FileInputStream(lStrInputPDFfile);
jPDFDocument = new org.apache.pdfbox.pdmodel.PDDocument().load(lOsPDFInput);

但这也适用于我的情况:

lStrInputPDFfile = "samples_pdf_signing\Country Calendar.pdf";
jPDFDocument = new org.apache.pdfbox.pdmodel.PDDocument().load(lStrInputPDFfile);

注意:`InputStream 是 FileInputStream 的父 class,这就是上述代码有效的原因。

更新了我的代码,请再次检查。感谢 @mkl 指出这一点。