问题
正如已经解释过的这个答案 https://stackoverflow.com/a/21260292/1729265,这里的问题是
-
当非增量存储带有添加图像的文档时,PDFBox 1.8.9 会使用交叉引用表来执行此操作,无论原始文件使用表还是流;如果原始文件使用流,则交叉引用流字典条目将复制到trailer字典;
...
0000033667 00000 n
0000033731 00000 n
trailer
<<
/DecodeParms <<
/Columns 4
/Predictor 12
>>
/Filter /FlateDecode
/ID [<5BD95916CAE5E84E9D964396022CBDCD> <6420B4547602C943AF37DD6C77496BE8>]
/Info 6 0 R
/Length 61
/Root 1 0 R
/Size 35
/Type /XRef
/W [1 2 1]
/Index [20 22]
>>
startxref
35917
%%EOF
(其中大部分trailer这里的条目毫无用处,甚至具有误导性,见下文。)
-
当增量保存签名时,COSWriter.doWriteXRefInc
uses COSDocument.isXRefStream
确定现有文档(我们上面存储的文档)是否使用交叉引用流。正如上面提到的,事实并非如此。但不幸的是,COSDocument.isXRefStream
在 PDFBox 1.8.9 中实现为
public boolean isXRefStream()
{
if (trailer != null)
{
return COSName.XREF.equals(trailer.getItem(COSName.TYPE));
}
return false;
}
因此,误导性的trailer entry Type如上所示,PDFBox 认为它必须使用交叉引用流。
结果是一个文档,其初始修订版以交叉引用表和奇怪的预告片条目结束,其第二修订版以交叉引用流结束。这是无效的。
解决方法
不过幸运的是,了解问题是如何出现的可以提供一种解决方法:消除麻烦的问题trailer条目,例如像这样:
inputBytes = os.toByteArray();
pdDocument = PDDocument.load(new ByteArrayInputStream(inputBytes));
pdDocument.getDocument().getTrailer().removeItem(COSName.TYPE); // <<<<<<<<<< Remove misleading entry <<<<<<<<<<
通过此解决方法,签名文档中的两个修订都使用交叉引用表,并且签名有效。
Beware, 如果即将推出的 PDFBox 版本更改为使用外部引用流来保存从具有交叉引用流的源加载的文档,则必须再次删除该解决方法。
不过,我认为这不会在即将到来的 1.x.x 版本中发生,并且 2.0.0 版本将引入一个根本性改变的 API,因此无论如何,原始代码将无法开箱即用。
其他想法
我也尝试过其他方法来规避这个问题,试图
- 也将第一个操作存储为增量更新,或者
- 在与签名相同的增量更新期间添加图像,
cf. SignLikeUnOriginalToo.java https://github.com/mkl-public/testarea-pdfbox1/blob/master/src/test/java/mkl/testarea/pdfbox1/sign/SignLikeUnOriginalToo.java,但失败了。 PDFBox 1.8.9 增量更新似乎仅适用于添加签名。
重新审视其他想法
在进一步研究使用 PDFBox 创建附加修订版后,我再次尝试了其他想法,现在成功了!
关键部分是将添加和更改的对象标记为已更新,包括文档目录中的路径。
应用第一个想法(添加图像作为明确的中间修订版)相当于这一变化doSign
:
...
FileOutputStream fos = new FileOutputStream(intermediateDocument);
FileInputStream fis = new FileInputStream(intermediateDocument);
byte inputBytes[] = IOUtils.toByteArray(inputStream);
PDDocument pdDocument = PDDocument.load(new ByteArrayInputStream(inputBytes));
PDJpeg ximage = new PDJpeg(pdDocument, ImageIO.read(logoStream));
PDPage page = (PDPage) pdDocument.getDocumentCatalog().getAllPages().get(0);
PDPageContentStream contentStream = new PDPageContentStream(pdDocument, page, true, true);
contentStream.drawXObject(ximage, 50, 50, 356, 40);
contentStream.close();
pdDocument.getDocumentCatalog().getCOSObject().setNeedToBeUpdate(true);
pdDocument.getDocumentCatalog().getPages().getCOSObject().setNeedToBeUpdate(true);
page.getCOSObject().setNeedToBeUpdate(true);
page.getResources().getCOSObject().setNeedToBeUpdate(true);
page.getResources().getCOSDictionary().getDictionaryObject(COSName.XOBJECT).setNeedToBeUpdate(true);
ximage.getCOSObject().setNeedToBeUpdate(true);
fos.write(inputBytes);
pdDocument.saveIncremental(fis, fos);
pdDocument.close();
pdDocument = PDDocument.load(intermediateDocument);
PDSignature signature = new PDSignature();
...
(as in SignLikeUnOriginalToo.java https://github.com/mkl-public/testarea-pdfbox1/blob/master/src/test/java/mkl/testarea/pdfbox1/sign/SignLikeUnOriginalToo.java method doSignTwoRevisions
)
应用第二个想法(添加图像作为签名修订的一部分)相当于这一变化doSign
:
...
byte inputBytes[] = IOUtils.toByteArray(inputStream);
PDDocument pdDocument = PDDocument.load(new ByteArrayInputStream(inputBytes));
PDJpeg ximage = new PDJpeg(pdDocument, ImageIO.read(logoStream));
PDPage page = (PDPage) pdDocument.getDocumentCatalog().getAllPages().get(0);
PDPageContentStream contentStream = new PDPageContentStream(pdDocument, page, true, true);
contentStream.drawXObject(ximage, 50, 50, 356, 40);
contentStream.close();
page.getResources().getCOSObject().setNeedToBeUpdate(true);
page.getResources().getCOSDictionary().getDictionaryObject(COSName.XOBJECT).setNeedToBeUpdate(true);
ximage.getCOSObject().setNeedToBeUpdate(true);
PDSignature signature = new PDSignature();
...
(as in SignLikeUnOriginalToo.java https://github.com/mkl-public/testarea-pdfbox1/blob/master/src/test/java/mkl/testarea/pdfbox1/sign/SignLikeUnOriginalToo.java method doSignOneStep
)
这两种变体显然都比原始方法更可取。