Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I want to sign a InputStream from a PDF file without using a temporary file.
Here I convert InputStream to File and this work fine :

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();

But I want to do this directly :

PDDocument document = PDDocument.load(inputStream);

Problem: at run

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)

All ideas are welcome.
Thank you.

EDIT: It's now working with the release of PDFBox 2.0.0.

like image 467
Cyril Bremaud Avatar asked Sep 28 '22 12:09

Cyril Bremaud


1 Answers

The cause

The immediate hindrance is in the method PDDocument.saveIncremental() itself:

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)

The member incrementalFile used in the first line is only set during a PDDocument.load with a File parameter.

Thus, this method cannot be used.

A work-around

Fortunately the method PDDocument.saveIncremental() only uses methods and values publicly available with the sole exception of signInterface, but you know the value of it because you set it in your code in the line right before the saveIncremental call:

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

Thus, instead of calling PDDocument.saveIncremental() you can do the equivalent in your code.

To do so you furthermore need a replacement value for the InputStream input. It needs to return a stream with the identical content as inputStream in your

PDDocument document = PDDocument.load(inputStream);

So you need to use that stream twice. 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.

Thus, replace your

PDDocument document = PDDocument.load(inputStream);

...

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

by

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();

and add a new method saveIncremental to your class inspired by the original 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();
        }
    }
}

On the side

I said above

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.

Actually there is another reason to do so: COSWriter.doWriteSignature() retrieves the length of the original PDF like this:

long inLength = incrementalInput.available();

(COSWriter.java)

The documentation of InputStream.available() states, though:

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

To re-use inputStream instead of using a byte[] and ByteArrayInputStreams as above, therefore, inputStream not only needs to support reset() but also needs to be one of the few InputStream implementations which return the total number of bytes in the stream as available.

FileInputStream and ByteArrayInputStream both do return the total number of bytes in the stream as available.

There may still be more issues when using generic InputStreams instead of these two.

like image 187
mkl Avatar answered Oct 05 '22 07:10

mkl