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

如何用PDFBox 2.0.27移除内容上方水印?求可用实现代码

移除PDF内容上方水印的PDFBox实现(2.0.27版本)

当然可以实现移除内容上方的水印,PDF页面的内容是按绘制顺序渲染的——内容上方的水印(前景水印)是在正文之后绘制的,只要解析页面内容流,识别并过滤掉水印对应的绘制指令,就能生成干净的PDF文件。

下面是两种常见水印场景的实现代码,你可以根据自己的水印类型调整:

场景1:移除已知文本内容的前景水印

如果你的水印是固定文本(比如"CONFIDENTIAL"),用这段代码:

import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.contentstream.operator.Operator;
import org.apache.pdfbox.contentstream.PDFStreamEngine;
import org.apache.pdfbox.contentstream.operator.state.*;
import org.apache.pdfbox.contentstream.operator.text.*;
import org.apache.pdfbox.contentstream.operator.graphics.*;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.List;

public class RemoveForegroundTextWatermark {
    // 替换成你要移除的水印文本
    private static final String TARGET_WATERMARK = "CONFIDENTIAL";

    public static void main(String[] args) {
        try (PDDocument doc = PDDocument.load(new File("你的输入文件路径.pdf"))) {
            for (PDPage page : doc.getPages()) {
                PDStream newContent = new PDStream(doc);
                try (PDPageContentStream contentStream = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.OVERWRITE, true, true)) {
                    WatermarkFilterEngine engine = new WatermarkFilterEngine(contentStream);
                    engine.processPage(page);
                }
                page.setContents(newContent);
            }
            doc.save("你的输出文件路径.pdf");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 自定义流引擎,过滤水印文本
    private static class WatermarkFilterEngine extends PDFStreamEngine {
        private final PDPageContentStream outputStream;
        private boolean skipWatermark = false;

        public WatermarkFilterEngine(PDPageContentStream outputStream) {
            this.outputStream = outputStream;
            // 注册必要的操作符处理器
            registerOperatorProcessor("BT", new BeginText());
            registerOperatorProcessor("ET", new EndText());
            registerOperatorProcessor("Tj", new ShowText());
            registerOperatorProcessor("TJ", new ShowTextAdjusted());
            registerOperatorProcessor("Tf", new SetFontAndSize());
            registerOperatorProcessor("q", new SaveGraphicsState());
            registerOperatorProcessor("Q", new RestoreGraphicsState());
            registerOperatorProcessor("Do", new Invoke());
        }

        @Override
        protected void processOperator(Operator operator, List<?> operands) throws IOException {
            String opName = operator.getName();

            // 判断当前文本是否是水印
            if ("Tj".equals(opName) || "TJ".equals(opName)) {
                String text = getTextFromOperand(operands, opName);
                if (text.contains(TARGET_WATERMARK)) {
                    skipWatermark = true;
                    return; // 跳过水印绘制
                }
            } else if ("ET".equals(opName)) {
                skipWatermark = false; // 文本块结束,重置标记
            }

            // 非水印指令写入新流
            if (!skipWatermark) {
                outputStream.addOperand(operator);
                operands.forEach(outputStream::addOperand);
                outputStream.endOperand();
            }
        }

        private String getTextFromOperand(List<?> operands, String opName) {
            if ("Tj".equals(opName)) {
                return operands.get(0).toString();
            } else {
                StringBuilder sb = new StringBuilder();
                for (Object obj : (List<?>) operands.get(0)) {
                    if (obj instanceof String) sb.append(obj);
                }
                return sb.toString();
            }
        }
    }
}

场景2:移除图像类型的前景水印

如果水印是图片,可以通过尺寸、位置等特征过滤:

import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.util.Matrix;
import org.apache.pdfbox.contentstream.operator.Operator;
import org.apache.pdfbox.contentstream.PDFStreamEngine;
import org.apache.pdfbox.contentstream.operator.state.*;
import org.apache.pdfbox.contentstream.operator.graphics.*;

import java.io.File;
import java.io.IOException;
import java.util.List;

public class RemoveForegroundImageWatermark {
    // 替换成你水印图片的实际尺寸(允许误差)
    private static final float WATERMARK_WIDTH = 220f;
    private static final float WATERMARK_HEIGHT = 110f;
    private static final float ERROR_RANGE = 15f;

    public static void main(String[] args) {
        try (PDDocument doc = PDDocument.load(new File("你的输入文件路径.pdf"))) {
            for (PDPage page : doc.getPages()) {
                PDStream newContent = new PDStream(doc);
                try (PDPageContentStream contentStream = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.OVERWRITE, true, true)) {
                    ImageFilterEngine engine = new ImageFilterEngine(contentStream, page);
                    engine.processPage(page);
                }
                page.setContents(newContent);
            }
            doc.save("你的输出文件路径.pdf");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static class ImageFilterEngine extends PDFStreamEngine {
        private final PDPageContentStream outputStream;
        private final PDPage page;
        private Matrix currentMatrix;

        public ImageFilterEngine(PDPageContentStream outputStream, PDPage page) {
            this.outputStream = outputStream;
            this.page = page;

            // 记录当前变换矩阵(用于计算图像实际显示尺寸)
            registerOperatorProcessor("cm", new ConcatenateMatrix() {
                @Override
                public void process(Operator operator, List<?> operands) throws IOException {
                    super.process(operator, operands);
                    currentMatrix = getGraphicsState().getCurrentTransformationMatrix();
                }
            });

            // 过滤水印图像
            registerOperatorProcessor("Do", new Invoke() {
                @Override
                public void process(Operator operator, List<?> operands) throws IOException {
                    COSName xObjName = (COSName) operands.get(0);
                    PDResources resources = page.getResources();
                    PDXObject xObj = resources.getXObject(xObjName);

                    if (xObj instanceof PDFormXObject) {
                        // 递归处理嵌套的表单对象
                        processFormXObject((PDFormXObject) xObj);
                    } else if (xObj instanceof org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject) {
                        org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject image = (org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject) xObj;
                        float displayWidth = image.getWidth() * currentMatrix.getScaleX();
                        float displayHeight = image.getHeight() * currentMatrix.getScaleY();

                        // 判断是否是目标水印图像
                        if (Math.abs(displayWidth - WATERMARK_WIDTH) < ERROR_RANGE &&
                            Math.abs(displayHeight - WATERMARK_HEIGHT) < ERROR_RANGE) {
                            return; // 跳过水印绘制
                        }
                    }
                    // 非水印图像写入新流
                    outputStream.addOperand(operator);
                    outputStream.addOperand(xObjName);
                    outputStream.endOperand();
                }
            });

            // 注册其他必要操作符处理器
            registerOperatorProcessor("BT", new BeginText());
            registerOperatorProcessor("ET", new EndText());
            registerOperatorProcessor("Tj", new ShowText());
            registerOperatorProcessor("q", new SaveGraphicsState());
            registerOperatorProcessor("Q", new RestoreGraphicsState());
        }

        private void processFormXObject(PDFormXObject form) throws IOException {
            saveGraphicsState();
            concatenateMatrix(form.getMatrix());
            processStream(form, form.getResources(), form.getContentStream());
            restoreGraphicsState();
        }

        @Override
        protected void processOperator(Operator operator, List<?> operands) throws IOException {
            if (!"Do".equals(operator.getName())) {
                outputStream.addOperand(operator);
                operands.forEach(outputStream::addOperand);
                outputStream.endOperand();
            }
        }
    }
}

使用说明

  1. 确保项目引入PDFBox 2.0.27依赖:
<dependency>
    <groupId>org.apache.pdfbox</groupId>
    <artifactId>pdfbox</artifactId>
    <version>2.0.27</version>
</dependency>
  1. 根据水印类型选择对应代码,修改水印文本或尺寸参数
  2. 替换代码中的输入输出文件路径,运行即可生成处理后的PDF

注意事项

  • 如果水印是嵌套在表单对象里的,代码已经做了递归处理
  • 若文本水印有旋转、缩放变形,可以结合变换矩阵做更精准的判断
  • 复杂水印可能需要增加颜色、字体等特征识别逻辑,你可以在代码中扩展判断条件

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 12:52:40