You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何彻底移除PDF中Acrobat创建的XObject水印并检测水印文本?

问题解决方案

一、彻底移除Acrobat创建的水印对象

Acrobat生成的水印可能以页面注释(Stamp)、Form XObject或直接的内容流指令三种形式存在,仅用PdfContentStreamEditor修改内容流无法覆盖所有场景,需结合以下操作:

1. 移除水印类页面注释

遍历页面注释集合,识别并删除水印对应的Stamp注释:

PDDocument document = PDDocument.load(new File("input.pdf"));
for (PDPage page : document.getPages()) {
    // 复制注释集合避免遍历时修改原集合引发异常
    List<PDAnnotation> annotations = new ArrayList<>(page.getAnnotations());
    for (PDAnnotation annot : annotations) {
        if (annot instanceof PDAnnotationStamp) {
            PDAnnotationStamp stamp = (PDAnnotationStamp) annot;
            // 根据注释内容、外观或Subtype判断是否为目标水印
            if (stamp.getContents() != null && stamp.getContents().contains("EBSCO")) {
                page.getAnnotations().remove(annot);
            }
        }
    }
}

2. 删除水印对应的Form XObject

部分水印会被封装为Form XObject存储在页面资源中,需先识别再移除,并同步修改内容流跳过其调用指令:

for (PDPage page : document.getPages()) {
    PDResources resources = page.getResources();
    if (resources != null && resources.getXObjects() != null) {
        Map<String, PDXObject> xObjects = resources.getXObjects();
        List<String> watermarkXObjKeys = new ArrayList<>();
        
        // 遍历XObject,标记包含水印内容的对象
        for (Map.Entry<String, PDXObject> entry : xObjects.entrySet()) {
            PDXObject xobj = entry.getValue();
            if (xobj instanceof PDFormXObject) {
                PDFormXObject form = (PDFormXObject) xobj;
                if (hasWatermarkText(form.getContents())) {
                    watermarkXObjKeys.add(entry.getKey());
                }
            }
        }
        
        // 移除标记的XObject
        for (String key : watermarkXObjKeys) {
            xObjects.remove(key);
        }
    }
}

3. 结合PdfContentStreamEditor跳过水印相关指令

在重写的processOperator方法中,不仅跳过文本绘制指令,还要跳过水印XObject的调用指令:

@Override
protected void processOperator(Operator operator, List<COSBase> operands) throws IOException {
    String opName = operator.getName();
    
    // 跳过水印文本绘制指令
    if (("Tj".equals(opName) || "TJ".equals(opName)) && isWatermarkText(extractText(operands))) {
        return;
    }
    
    // 跳过水印XObject的调用指令
    if ("Do".equals(opName)) {
        COSName xobjName = (COSName) operands.get(0);
        PDXObject xobj = getResources().getXObject(xobjName);
        if (xobj instanceof PDFormXObject && hasWatermarkText(((PDFormXObject)xobj).getContents())) {
            return;
        }
    }
    
    super.processOperator(operator, operands);
}

二、检测水印对象中的文本

可通过解析内容流文本、结合文本属性两种方式识别水印:

1. 解析内容流提取文本匹配关键词

使用PDFTextStripper提取内容流中的文本,匹配水印特征关键词:

private boolean hasWatermarkText(PDStream contentStream) throws IOException {
    // 临时创建包含目标内容流的页面,用于文本提取
    PDPage tempPage = new PDPage(contentStream);
    PDDocument tempDoc = new PDDocument();
    tempDoc.addPage(tempPage);
    
    PDFTextStripper stripper = new PDFTextStripper();
    String contentText = stripper.getText(tempDoc);
    tempDoc.close();
    
    // 根据实际水印文本设置匹配规则,比如EBSCO相关水印
    return contentText.contains("EBSCO") || contentText.contains("仅供学术使用");
}

// 提取文本指令中的内容
private String extractText(List<COSBase> operands) {
    if (operands.isEmpty()) return "";
    COSBase base = operands.get(0);
    if (base instanceof COSString) {
        return ((COSString) base).getString();
    } else if (base instanceof COSArray) {
        // 处理TJ指令的数组内容
        StringBuilder sb = new StringBuilder();
        COSArray array = (COSArray) base;
        for (COSBase item : array) {
            if (item instanceof COSString) {
                sb.append(((COSString) item).getString());
            }
        }
        return sb.toString();
    }
    return "";
}

2. 结合文本位置与样式筛选

若水印文本有明显样式特征(如固定位置、浅灰色、大字体),可重写PDFTextStripper的writeString方法,记录文本的位置和样式信息:

class WatermarkDetectStripper extends PDFTextStripper {
    private boolean foundWatermark = false;
    
    @Override
    protected void writeString(String text, List<TextPosition> textPositions) throws IOException {
        for (TextPosition pos : textPositions) {
            // 示例:判断文本是否在页面底部(水印常见位置)、颜色为浅灰色
            if (pos.getY() < 50 && pos.getFillColor().getRGB() > 0xeeeeee) {
                if (text.contains("EBSCO")) {
                    foundWatermark = true;
                    break;
                }
            }
        }
        super.writeString(text, textPositions);
    }
    
    public boolean isWatermarkFound() {
        return foundWatermark;
    }
}

内容的提问来源于stack exchange,提问作者menteith

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 19:55:11