是什么让 PdfStamper 在 cleanup() 之后从 pdf 中删除图像,尽管它不应该?

What makes PdfStamper to remove images from pdf after cleanup() though it shouldn't?

首先,我对 java 以及 iText 都比较陌生。

很快,我有一个程序可以从大源 pdf 文档中每 2 页复制一次,并为每对页面创建一个新文档。此外,该程序从第一页删除了一些文本信息,并使用所有者密码保护新文档。

这是我的代码。我使用 iText 5.5.13.

    //...
    final Rectangle RECT_TOP= new Rectangle(25f, 788f, 288f, 812.5f);  
    final Rectangle RECT_BOT= new Rectangle(103.5f, 36.5f, 331f, 40f); 
    //...

    PdfDocument document = new Document(reader.getPageSizeWithRotation(1));
    File tempFile = File.createTempFile("temp", ".pdf");
    PdfCopy writer = new PdfCopy(
            document, //PdfDocument
            new FileOutputStream(tempFile.getAbsolutePath()));  

   document.open(); 

    writer.addPage( writer.getImportedPage(reader, i) );
    writer.addPage( writer.getImportedPage(reader, i + 1) );                                        

    writer.freeReader(reader);
    writer.close();
    document.close(); 

    PdfReader tmpReader = new PdfReader(tempFile.getAbsolutePath());
    PdfStamper st = new PdfStamper(tmpReader, new FileOutputStream(outFile));
    List<PdfCleanUpLocation> locations = new ArrayList<PdfCleanUpLocation>();
    locations.add(new PdfCleanUpLocation(1, RECT_TOP, BaseColor.WHITE));
    locations.add(new PdfCleanUpLocation(1, RECT_BOT, BaseColor.WHITE));

    new PdfCleanUpProcessor(locations, st).cleanUp();                                                                   
    st.setEncryption(   
        "".getBytes(),
        OWNER_PASSWORD.getBytes(),  
        PdfWriter.ALLOW_COPY | PdfWriter.ALLOW_PRINTING,
        PdfWriter.ENCRYPTION_AES_256 | PdfWriter.DO_NOT_ENCRYPT_METADATA);  

    st.getWriter().freeReader(tmpReader);
    st.close();
    tmpReader.close();
    tempFile.delete();

源 PDF 在我必须 cleanUp() 的每个页面上都有一个 QR 码作为图像。区域 RECT_TOP 和 RECT_BOT 不以任何方式包含图像。

我在两个 pdf 上测试了我的代码,里面有相同的数据。其中一个是使用 BullZip PDF 打印机 (v PDF-1.5) 创建的,另一个是使用 Foxit PDF 打印机 (v PDF-1.7) 创建的。问题是 cleanUp 方法删除了 QR 代码 和来自矩形位置的数据传递给 BullZip 创建的文档中的 PdfCleanUpProcessor,但对于 foxit PDF,它可以正常工作,我真的需要它与Bullzip 文档。

我试图通过编辑注释来操纵临时 pdf 文件和 cleanUp() 的版本,但没有用。

我想了解要查看的位置以及要更改的内容(也许在 PdfCleanUpProcessor class 中的某处?)以使其正常工作。 有人知道为什么会这样吗?

更新。我设法创建了一些类似于我需要处理的 PDF,并且我发现了另一件有趣的事情:Bullzip 能够自己创建 "bad" 和 "good" 文件。我检查了不同的打印机设置,包括权限,但仍然很难说它取决于什么。在显着差异中,只有文件尺寸略小和页边距略有不同。

无论如何,我的 test files

您确实在 iText 5 PdfCleanUpProcessor 中发现了一个错误:它会丢弃所有未部分编辑的 内联 图像。

错误详情

错误位于 PdfCleanUpRenderListener 方法 renderImage:

public void renderImage(ImageRenderInfo renderInfo) {
    List<Rectangle> areasToBeCleaned = getImageAreasToBeCleaned(renderInfo);

    if (areasToBeCleaned == null) {
        chunks.add(new PdfCleanUpContentChunk.Image(false, null));
    } else if ( areasToBeCleaned.size() > 0) {
        try {
            PdfImageObject pdfImage = renderInfo.getImage();
            byte[] imageBytes = processImage(pdfImage.getImageAsBytes(), areasToBeCleaned);

            if (renderInfo.getRef() == null && pdfImage != null) { // true => inline image
                PdfDictionary dict = pdfImage.getDictionary();
                PdfObject imageMask = dict.get(PdfName.IMAGEMASK);
                Image image = Image.getInstance(imageBytes);

                if (imageMask == null) {
                    imageMask = dict.get(PdfName.IM);
                }

                if (imageMask != null && imageMask.equals(PdfBoolean.PDFTRUE)) {
                    image.makeMask();
                }

                PdfContentByte canvas = getContext().getCanvas();
                canvas.addImage(image, 1, 0, 0, 1, 0, 0, true);
            } else if (pdfImage != null && imageBytes != pdfImage.getImageAsBytes()) {
                chunks.add(new PdfCleanUpContentChunk.Image(true, imageBytes));
            }
        } catch (UnsupportedPdfException pdfException) {
            chunks.add(new PdfCleanUpContentChunk.Image(false, null));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

内联图像在这里需要特殊处理:对于 non-inlined 图像,绘制图像 XObject 的实际指令在别处处理,renderImage 只需要检查图像是否需要编辑并提供一个编辑图像版本。但是,对于内联图像,此方法还必须将图像添加到结果内容流中(除非它被完全删除)。

如您所见,对于部分被密文区域 (areasToBeCleaned.size() > 0) 覆盖的图像,块中的内嵌图像有特殊处理,但 none 对于未被密文区域覆盖的图像 (areasToBeCleaned != nullareasToBeCleaned.size() == 0).

如何修复

您可以通过在新的 else 子句中添加类似的特殊处理来解决此问题:

public void renderImage(ImageRenderInfo renderInfo) {
    List<Rectangle> areasToBeCleaned = getImageAreasToBeCleaned(renderInfo);

    if (areasToBeCleaned == null) {
        chunks.add(new PdfCleanUpContentChunk.Image(false, null));
    } else if ( areasToBeCleaned.size() > 0) {
        try {
            PdfImageObject pdfImage = renderInfo.getImage();
            byte[] imageBytes = processImage(pdfImage.getImageAsBytes(), areasToBeCleaned);

            if (renderInfo.getRef() == null && pdfImage != null) { // true => inline image
                PdfDictionary dict = pdfImage.getDictionary();
                PdfObject imageMask = dict.get(PdfName.IMAGEMASK);
                Image image = Image.getInstance(imageBytes);

                if (imageMask == null) {
                    imageMask = dict.get(PdfName.IM);
                }

                if (imageMask != null && imageMask.equals(PdfBoolean.PDFTRUE)) {
                    image.makeMask();
                }

                PdfContentByte canvas = getContext().getCanvas();
                canvas.addImage(image, 1, 0, 0, 1, 0, 0, true);
            } else if (pdfImage != null && imageBytes != pdfImage.getImageAsBytes()) {
                chunks.add(new PdfCleanUpContentChunk.Image(true, imageBytes));
            }
        } catch (UnsupportedPdfException pdfException) {
            chunks.add(new PdfCleanUpContentChunk.Image(false, null));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    } else { // add inline images not subject to redaction to the content
        try {
            PdfImageObject pdfImage = renderInfo.getImage();
            if (renderInfo.getRef() == null && pdfImage != null) { // true => inline image
                PdfDictionary dict = pdfImage.getDictionary();
                PdfObject imageMask = dict.get(PdfName.IMAGEMASK);
                Image image = Image.getInstance(pdfImage.getImageAsBytes());

                if (imageMask == null) {
                    imageMask = dict.get(PdfName.IM);
                }

                if (imageMask != null && imageMask.equals(PdfBoolean.PDFTRUE)) {
                    image.makeMask();
                }

                PdfContentByte canvas = getContext().getCanvas();
                canvas.addImage(image, 1, 0, 0, 1, 0, 0, true);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

例子

为了说明问题和修复的效果,我创建了一个包含五个内联图像的简单 PDF:

Document document = new Document(new Rectangle(500, 500));
PdfWriter writer = PdfWriter.getInstance(document, baos);
document.open();
PdfContentByte canvas = writer.getDirectContent();
for (int i = 0; i < 5; i++) {
    canvas.addImage(image, 50, 0, 0, 50, i * 100 + 25, i * 100 + 25, true);
}
document.close();

(RedactWithImageIssue 辅助方法 createPdfWithInlineImages)

看起来像这样:

像这样应用密文

PdfReader reader = new PdfReader(pdf);
PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(RESULT));
List<com.itextpdf.text.pdf.pdfcleanup.PdfCleanUpLocation> locations = new ArrayList<>();
locations.add(new com.itextpdf.text.pdf.pdfcleanup.PdfCleanUpLocation(1, new Rectangle(150, 150, 350, 350), BaseColor.RED));
new PdfCleanUpProcessor(locations, stamper).cleanUp();
stamper.close();

(RedactWithImageIssue 测试 testRedactPdfWithInlineImages)

没有和有补丁分别导致

如您所见,最初只保留了部分编辑的内联图像,但随着补丁完全在编辑区域之外的内联图像也保留了下来。