返回

iText7 精确移除 PDF 二维码:两种技术方案详解

java

使用 iText7 从 PDF 各页面移除二维码的技术方案

处理 PDF 文档时,移除特定内容是一项常见任务。二维码普遍应用,其存在有时会与文档最终用途不符。寻求移除 PDF 各页面的二维码却发现文档中其他的文本和组件也遭到扭曲,问题棘手。这需要一个有效的技术解决方案: 使用 iText7 从 PDF 页面删除二维码。本文就此展开详细探讨。

一、 问题分析及挑战

直接操作 PDF 文件以移除特定元素(比如二维码),难点在于如何在保留 PDF 文档其他部分内容完整性的同时精确识别并删除目标。上面使用的原始方案利用循环遍历每一页的所有图像资源,通过尺寸来粗略判断图像是否为二维码,然后将其内容清空。方法逻辑简单粗暴,不足之处在于:

  • 二维码识别精度不足 :单纯通过尺寸范围判断图像是否为二维码不严谨,尺寸在特定区间的非二维码图像也会被误删。而且现实世界的二维码尺寸可能会超过或小于上面预定的范围。
  • 误删 :在循环里面,stream.clear()直接对PdfStream对象做清除操作。这个stream里面很可能包含的并非纯粹是二维码信息,如果这样被清除了,其他PDF元素的显示必定出问题。
  • 内容删除不完全 : 即便正确识别出二维码,直接清除PdfStream可能仅删除图像数据,却未将其引用或占位符一同移除,这样处理文档结构也可能造成PDF文档渲染时遇到一些不符合预期的问题。

所以该方法具有极高的误伤率。因此,一套精确且高效的方法亟待出炉。

二、 解决方案及实施步骤

要精准移除 PDF 文档中的二维码,须依赖 iText7 库提供的一套强大机制。可以有两种不同的策略:基于现有内容分析过滤、创建具有遮盖功能的注解。

1. 内容解析与过滤

一个更高级的技术方案:依靠对页面内容( PdfCanvasProcessor)的精细解析实现。核心逻辑变为:找到页面上全部的图片资源(ImageRenderInfo),将它们交给现有的二维码识别工具 (比如ZXing)检测是否为QR Code,确定二维码的类型和它精确的矩形包围盒范围,最终通过给原Pdf资源创建一个对应的过滤项去删除它们。

步骤:

  1. 准备二维码识别库:将像ZXing这样处理QR Code的工具引入项目。
  2. 针对PDF页面解析其画布:对PdfDocument的每一页都新建PdfCanvasProcessor来做画布解析工作。这里需要一个IEventListener接口实现类用于收集画布中图片的信息。
    public class ImageCleanupEventListener implements IEventListener {
        private PdfDocument pdfDocument;

        // 其他可能用到的字段...

        public ImageCleanupEventListener(PdfDocument doc){
            this.pdfDocument = doc;
        }

        @Override
        public void eventOccurred(IEventData data, EventType type) {
            if (type == EventType.RENDER_IMAGE) {
                try {
                    ImageRenderInfo imageRenderInfo = (ImageRenderInfo) data;
                    Rectangle occupiedArea = calculateImageOccupiedArea(imageRenderInfo);

                    PdfImageXObject image = imageRenderInfo.getImage();
                    BufferedImage bufferedImage = image.getBufferedImage();

                    boolean isQR = detectQRCodeWithZxing(bufferedImage);

                    if(isQR) {
                        // 拿到PdfName,构建过滤条件来清理图片
                        PdfName xObjectName = image.getPdfObject().getName();
                        List<PdfName> objNamesToCollect = new ArrayList<>();
                        objNamesToCollect.add(xObjectName);

                        page.getContentStream().applyFilter(new ClearSpecificXObjects(objNamesToCollect), 0);
                    }

                } catch (IOException e) {
                    // 处理异常
                }

            }

        }

        // 一些必要的工具函数,比如isQRCode
}
  1. 收集图片并进行判定: 在自定义监听器里面处理每一个 ImageRenderInfo 对象,提取位图信息交给 QR Code 工具识别是否是目标对象。
    private Rectangle calculateImageOccupiedArea(ImageRenderInfo renderInfo) {
        Matrix ctm = renderInfo.getImageCTM();
        float width = ctm.get(Matrix.I11);
        float height = ctm.get(Matrix.I22);
        float x = ctm.get(Matrix.I31);
        float y = ctm.get(Matrix.I32);

        return new Rectangle(x, y, width, height);
    }

    private boolean detectQRCodeWithZxing(BufferedImage bufferedImage) throws IOException {
        LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
        BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
        try {
            Result result = new MultiFormatReader().decode(bitmap);
            return "QR_CODE".equals(result.getBarcodeFormat().name());
        } catch (NotFoundException e) {
            // BufferedImage 中不存在二维码
            return false;
        }
    }
  1. 使用专门的过滤条件来重写PDF文件:创建一个自定义PdfContentStreamEditor类来替换掉那些被标记为待删除的对象即可,最终将修改后的内容写回即可得到删除二维码的新文档。
public class ClearSpecificXObjects implements IPdfFilter {
    protected Set<PdfName> xObjectNamesToClear;

    public ClearSpecificXObjects(List<PdfName> xObjectNamesToClear) {
        this.xObjectNamesToClear = new HashSet<>(xObjectNamesToClear);
    }

    @Override
    public byte[] decode(byte[] encodedBytes, PdfName filterName, PdfObject decodeParams, PdfDictionary streamDictionary) throws IOException {
        PdfName xObjectName = streamDictionary.getAsName(PdfName.Name);
        if (xObjectName != null && xObjectNamesToClear.contains(xObjectName)) {
            return new byte[0]; // 返回空字节数组来删除对象
        }
        return encodedBytes; // 对于非目标对象保持不变
    }
}

通过这种手段将极大程度保证精准移除。这种方案不依赖假设,适应性更强。

2. 添加遮盖区域

这种思路不再去删除二维码信息了,只是添加遮盖物去阻止其渲染,具体方法如下:

在找到全部的二维码的位置和大小之后,可以使用白色或其他不透明的矩形区域覆盖这些位置。PdfCanvas 的绘制功能很强,可以完成此需求。以下为具体的实现方法。

步骤:

  1. 在方法1步骤1-3中已完成了二维码识别和其具体信息的定位,所以,请使用之前的工作内容即可。

  2. 添加白色遮盖层:当 ImageCleanupEventListener中确认找到 QR Code 对象之后,立刻得到它被渲染的尺寸和大小,使用 PdfCanvas 接口将相同大小和坐标区域的白色矩形画到该图片上层。

// 修改 ImageCleanupEventListener 类的定义
public class ImageCleanupEventListener implements IEventListener {

//...之前的代码和成员变量省略...

@Override
public void eventOccurred(IEventData data, EventType type) {

//...之前的代码和处理流程省略...

    if (isQR) {
        PdfCanvas canvas = new PdfCanvas(page);
        canvas.saveState()
              .setFillColor(ColorConstants.WHITE) // 使用白色填充矩形以覆盖区域
              .rectangle(occupiedArea)
              .fill()
              .restoreState();

        // 如果还需要从资源字典中去除也可以继续之前的删除逻辑代码...

        }
    }
}

这样得到的最后结果就是在 QR Code 上添加了一个不透明区域来将其掩盖。这相当于从最后一步解决问题,而不是在数据层。好处是简单易懂,不操作复杂的 PdfStream 对象避免了出错风险。这种方法对文件结构基本不会做改动,稳健且可靠。它从渲染结果上达到目标要求,不处理PDF源数据层面的麻烦工作。这种方案可以做到“无感删除”,几乎无错地消除全部痕迹。对于某些打印流场景可以发挥极大的作用。

三、方案总结

本文详细解析使用 iText7 删除 PDF 文档中二维码遇到的技术难题。对两种可行的方法都详细列举步骤、附上示例代码,两种方法各有优势。选择哪种实现需依据应用环境和具体需求权衡来做出最终决策。安全提示:请记得处理所有相关的异常情况,并及时关闭和释放文档资源来确保操作安全性。使用该库处理PDF文件请始终保证遵守所有法律法规和行业合规性要求。谨慎使用此技术方案以确保数据的正确处理,防止重要信息的意外损失。使用专业工具(例如 Acrobat Pro)对得到的文档做正确性检测也尤为关键。