Java实现PDF添加水印的完整方案(支持灵活配置、平铺、多页策略)

前言

在企业级应用中,PDF水印是文档安全管理的必备功能。无论是版权保护、版本标识,还是敏感信息管控,水印都扮演着重要角色。

本文将提供一个生产级的PDF水印解决方案,基于Apache PDFBox实现,具备以下特点:

  • 高度可配置:支持文字、字体、颜色、旋转角度、透明度等全方位参数配置
  • 智能页面控制:支持全部页面、奇偶页、指定页码等灵活策略
  • 平铺模式:支持水印平铺布局,避免单一水印被截断
  • 多水印组合:支持在同一文档中添加多个不同的水印
  • 建造者模式:链式调用,代码简洁优雅

一、技术选型

为什么选择Apache PDFBox?

对比项 Apache PDFBox iText OpenPDF
开源协议 Apache 2.0(免费) AGPL(商业收费) LGPL(较宽松)
功能完整性 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
社区活跃度 中等 中等
学习曲线 平缓 陡峭 较平缓
推荐指数 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐

结论:对于企业项目,PDFBox是最优选择------开源免费、功能完整、社区活跃。

Maven依赖

xml 复制代码
		<!-- Apache PDFBox for PDF处理 -->
		<dependency>
			<groupId>org.apache.pdfbox</groupId>
			<artifactId>pdfbox</artifactId>
			<version>2.0.29</version>
		</dependency>

二、核心代码实现

2.1 水印配置类(WatermarkConfig)

采用建造者模式设计,支持链式调用,所有参数都可灵活配置。

java 复制代码
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.PDFont;

import java.awt.Color;
import java.util.ArrayList;
import java.util.List;

/**
 * PDF水印配置类
 * 使用建造者模式构建,支持链式调用
 */
public class WatermarkConfig {
    
    // ==================== 基础配置 ====================
    private String watermarkText;           // 水印文字(必填)
    private PDFont font;                     // 字体
    private float fontSize;                  // 字体大小
    private Color color;                     // 颜色(支持透明度)
    
    // ==================== 旋转配置 ====================
    private int rotationAngle;               // 旋转角度(度),支持正负值
    
    // ==================== 位置配置 ====================
    private WatermarkPosition position;      // 水印位置枚举
    private Float customX;                   // 自定义X坐标(position=CUSTOM时使用)
    private Float customY;                   // 自定义Y坐标(position=CUSTOM时使用)
    
    // ==================== 平铺配置 ====================
    private boolean tilingEnabled;           // 是否启用平铺模式
    private int tilingDensityX;              // X轴平铺密度(每页重复次数)
    private int tilingDensityY;              // Y轴平铺密度(每页重复次数)
    private float tilingOffsetX;             // X轴偏移量(百分比 0-1)
    private float tilingOffsetY;             // Y轴偏移量(百分比 0-1)
    
    // ==================== 透明度配置 ====================
    private float opacity;                   // 透明度 0-1(0完全透明,1完全不透明)
    
    // ==================== 页面过滤配置 ====================
    private List<Integer> targetPageNumbers; // 目标页码列表(从0开始)
    private PageFilterStrategy pageFilterStrategy; // 页面过滤策略
    
    // ==================== 多水印配置 ====================
    private List<WatermarkConfig> multiWatermarks; // 多水印配置列表

    /**
     * 水印位置枚举
     */
    public enum WatermarkPosition {
        CENTER,           // 居中
        TOP_LEFT,         // 左上角
        TOP_RIGHT,        // 右上角
        BOTTOM_LEFT,      // 左下角
        BOTTOM_RIGHT,     // 右下角
        TOP_CENTER,       // 上中
        BOTTOM_CENTER,    // 下中
        CUSTOM            // 自定义坐标
    }

    /**
     * 页面过滤策略
     */
    public enum PageFilterStrategy {
        ALL_PAGES,        // 所有页面(默认)
        EVEN_PAGES,       // 仅偶数页
        ODD_PAGES,        // 仅奇数页
        SPECIFIC_PAGES    // 指定页码
    }

    /**
     * 私有构造方法,使用Builder构建
     */
    private WatermarkConfig() {
        // 设置默认值
        this.font = PDType1Font.HELVETICA_BOLD;
        this.fontSize = 36f;
        this.color = new Color(200, 200, 200, 128); // 默认灰色半透明
        this.rotationAngle = 45;
        this.position = WatermarkPosition.CENTER;
        this.tilingEnabled = false;
        this.tilingDensityX = 3;
        this.tilingDensityY = 3;
        this.tilingOffsetX = 0.25f;
        this.tilingOffsetY = 0.25f;
        this.opacity = 0.5f;
        this.pageFilterStrategy = PageFilterStrategy.ALL_PAGES;
        this.targetPageNumbers = new ArrayList<>();
    }

    /**
     * Builder建造者类
     */
    public static class Builder {
        private WatermarkConfig config;

        public Builder() {
            this.config = new WatermarkConfig();
        }

        public Builder text(String text) {
            config.watermarkText = text;
            return this;
        }

        public Builder font(PDFont font) {
            config.font = font;
            return this;
        }

        public Builder fontSize(float fontSize) {
            config.fontSize = fontSize;
            return this;
        }

        public Builder color(Color color) {
            config.color = color;
            return this;
        }

        public Builder rotation(int angle) {
            config.rotationAngle = angle;
            return this;
        }

        public Builder position(WatermarkPosition position) {
            config.position = position;
            return this;
        }

        public Builder customPosition(float x, float y) {
            config.position = WatermarkPosition.CUSTOM;
            config.customX = x;
            config.customY = y;
            return this;
        }

        public Builder enableTiling(int densityX, int densityY) {
            config.tilingEnabled = true;
            config.tilingDensityX = densityX;
            config.tilingDensityY = densityY;
            return this;
        }

        public Builder enableTiling(int densityX, int densityY, float offsetX, float offsetY) {
            config.tilingEnabled = true;
            config.tilingDensityX = densityX;
            config.tilingDensityY = densityY;
            config.tilingOffsetX = offsetX;
            config.tilingOffsetY = offsetY;
            return this;
        }

        public Builder opacity(float opacity) {
            config.opacity = opacity;
            return this;
        }

        public Builder targetPages(PageFilterStrategy strategy) {
            config.pageFilterStrategy = strategy;
            return this;
        }

        public Builder targetPages(Integer... pageNumbers) {
            config.pageFilterStrategy = PageFilterStrategy.SPECIFIC_PAGES;
            for (Integer num : pageNumbers) {
                config.targetPageNumbers.add(num);
            }
            return this;
        }

        public Builder addMultiWatermark(WatermarkConfig watermarkConfig) {
            if (config.multiWatermarks == null) {
                config.multiWatermarks = new ArrayList<>();
            }
            config.multiWatermarks.add(watermarkConfig);
            return this;
        }

        public WatermarkConfig build() {
            if (config.watermarkText == null || config.watermarkText.trim().isEmpty()) {
                throw new IllegalArgumentException("水印文字不能为空");
            }
            return config;
        }
    }

    // ==================== Getter方法 ====================
    public String getWatermarkText() { return watermarkText; }
    public PDFont getFont() { return font; }
    public float getFontSize() { return fontSize; }
    public Color getColor() { return color; }
    public int getRotationAngle() { return rotationAngle; }
    public WatermarkPosition getPosition() { return position; }
    public Float getCustomX() { return customX; }
    public Float getCustomY() { return customY; }
    public boolean isTilingEnabled() { return tilingEnabled; }
    public int getTilingDensityX() { return tilingDensityX; }
    public int getTilingDensityY() { return tilingDensityY; }
    public float getTilingOffsetX() { return tilingOffsetX; }
    public float getTilingOffsetY() { return tilingOffsetY; }
    public float getOpacity() { return opacity; }
    public List<Integer> getTargetPageNumbers() { return targetPageNumbers; }
    public PageFilterStrategy getPageFilterStrategy() { return pageFilterStrategy; }
    public List<WatermarkConfig> getMultiWatermarks() { return multiWatermarks; }
}

2.2 水印执行工具类(EnhancedPdfWatermarkUtil)

核心执行逻辑,支持流式和文件两种模式。

java 复制代码
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.util.Matrix;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * 增强版PDF水印工具
 * 支持灵活配置、页面过滤、平铺模式、多水印组合
 */
public class EnhancedPdfWatermarkUtil {

    /**
     * 添加水印(流模式)- 适合Web场景
     *
     * @param inputStream 输入PDF流
     * @param outputStream 输出PDF流
     * @param config 水印配置
     * @throws IOException IO异常
     */
    public static void addWatermark(InputStream inputStream, OutputStream outputStream,
                                    WatermarkConfig config) throws IOException {
        try (PDDocument document = PDDocument.load(inputStream)) {
            addWatermarkToDocument(document, config);
            document.save(outputStream);
        }
    }

    /**
     * 添加水印(文件模式)- 适合批量处理
     *
     * @param sourceFile 源文件
     * @param targetFile 目标文件
     * @param config 水印配置
     * @throws IOException IO异常
     */
    public static void addWatermark(File sourceFile, File targetFile,
                                    WatermarkConfig config) throws IOException {
        try (PDDocument document = PDDocument.load(sourceFile)) {
            addWatermarkToDocument(document, config);
            document.save(targetFile);
        }
    }

    /**
     * 向文档添加水印(支持多水印配置)
     *
     * @param document PDF文档对象
     * @param config 水印配置
     * @throws IOException IO异常
     */
    private static void addWatermarkToDocument(PDDocument document, WatermarkConfig config) throws IOException {
        int pageCount = document.getNumberOfPages();

        // 处理主水印
        for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
            if (shouldAddWatermarkToPage(pageIndex, config)) {
                PDPage page = document.getPage(pageIndex);
                addWatermarkToPage(document, page, config);
            }
        }

        // 处理多水印配置(递归处理子水印)
        if (config.getMultiWatermarks() != null) {
            for (WatermarkConfig subConfig : config.getMultiWatermarks()) {
                for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
                    if (shouldAddWatermarkToPage(pageIndex, subConfig)) {
                        PDPage page = document.getPage(pageIndex);
                        addWatermarkToPage(document, page, subConfig);
                    }
                }
            }
        }
    }

    /**
     * 判断页面是否需要添加水印
     *
     * @param pageIndex 页码索引(从0开始)
     * @param config 水印配置
     * @return true-需要添加,false-跳过
     */
    private static boolean shouldAddWatermarkToPage(int pageIndex, WatermarkConfig config) {
        switch (config.getPageFilterStrategy()) {
            case ALL_PAGES:
                return true;
            case EVEN_PAGES:
                return (pageIndex + 1) % 2 == 0;
            case ODD_PAGES:
                return (pageIndex + 1) % 2 != 0;
            case SPECIFIC_PAGES:
                return config.getTargetPageNumbers().contains(pageIndex);
            default:
                return true;
        }
    }

    /**
     * 向单页添加水印
     *
     * @param document PDF文档对象(必须传入)
     * @param page PDF页面对象
     * @param config 水印配置
     * @throws IOException IO异常
     */
    private static void addWatermarkToPage(PDDocument document, PDPage page, WatermarkConfig config) throws IOException {
        PDRectangle pageSize = page.getMediaBox();
        float pageWidth = pageSize.getWidth();
        float pageHeight = pageSize.getHeight();

        try (PDPageContentStream contentStream = new PDPageContentStream(
                document,
                page,
                PDPageContentStream.AppendMode.APPEND,
                true,
                true)) {

            // 设置字体和颜色
            contentStream.setFont(config.getFont(), config.getFontSize());
            contentStream.setNonStrokingColor(config.getColor());

            // 根据配置选择绘制模式
            if (config.isTilingEnabled()) {
                // 平铺模式
                addTiledWatermark(contentStream, config, pageWidth, pageHeight);
            } else {
                // 单点模式
                addSingleWatermark(contentStream, config, pageWidth, pageHeight);
            }
        }
    }

    /**
     * 添加平铺水印(网格布局)
     *
     * @param contentStream 内容流
     * @param config 水印配置
     * @param pageWidth 页面宽度
     * @param pageHeight 页面高度
     * @throws IOException IO异常
     */
    private static void addTiledWatermark(PDPageContentStream contentStream,
                                          WatermarkConfig config,
                                          float pageWidth, float pageHeight) throws IOException {
        // 计算旋转角度(弧度)
        float angle = (float) Math.toRadians(config.getRotationAngle());

        // 计算网格间距
        float stepX = pageWidth / config.getTilingDensityX();
        float stepY = pageHeight / config.getTilingDensityY();

        // 遍历网格绘制水印
        for (int i = 0; i < config.getTilingDensityX(); i++) {
            for (int j = 0; j < config.getTilingDensityY(); j++) {
                float x = stepX * i + stepX * config.getTilingOffsetX();
                float y = stepY * j + stepY * config.getTilingOffsetY();

                drawRotatedText(contentStream, config, x, y, angle);
            }
        }
    }

    /**
     * 添加单点水印(固定位置 - 已修复真正居中)
     *
     * @param contentStream 内容流
     * @param config 水印配置
     * @param pageWidth 页面宽度
     * @param pageHeight 页面高度
     * @throws IOException IO异常
     */
    private static void addSingleWatermark(PDPageContentStream contentStream,
                                           WatermarkConfig config,
                                           float pageWidth, float pageHeight) throws IOException {
        float x, y;
        float angle = (float) Math.toRadians(config.getRotationAngle());

        // ✅ 计算文字宽度,实现真正的左右居中
        float textWidth = config.getFont().getStringWidth(config.getWatermarkText())
                / 1000 * config.getFontSize();

        // 根据位置枚举计算坐标
        switch (config.getPosition()) {
            case CENTER:
                // ✅ 页面中心 - 文字宽度的一半 = 文字居中
                x = pageWidth / 2 - textWidth / 2;
                y = pageHeight / 2;
                break;
            case TOP_LEFT:
                x = 100;
                y = pageHeight - 100;
                break;
            case TOP_RIGHT:
                // ✅ 右边缘 - 文字宽度 - 边距
                x = pageWidth - textWidth - 100;
                y = pageHeight - 100;
                break;
            case BOTTOM_LEFT:
                x = 100;
                y = 100;
                break;
            case BOTTOM_RIGHT:
                // ✅ 右边缘 - 文字宽度 - 边距
                x = pageWidth - textWidth - 100;
                y = 100;
                break;
            case TOP_CENTER:
                // ✅ 页面水平中心 - 文字宽度的一半
                x = pageWidth / 2 - textWidth / 2;
                y = pageHeight - 100;
                break;
            case BOTTOM_CENTER:
                // ✅ 页面水平中心 - 文字宽度的一半
                x = pageWidth / 2 - textWidth / 2;
                y = 100;
                break;
            case CUSTOM:
                x = config.getCustomX();
                y = config.getCustomY();
                break;
            default:
                x = pageWidth / 2 - textWidth / 2;
                y = pageHeight / 2;
        }

        drawRotatedText(contentStream, config, x, y, angle);
    }

    /**
     * 绘制旋转文字(核心方法 - 已修复方法调用顺序)
     *
     * @param contentStream 内容流
     * @param config 水印配置
     * @param x X坐标
     * @param y Y坐标
     * @param angle 旋转角度(弧度)
     * @throws IOException IO异常
     */
    private static void drawRotatedText(PDPageContentStream contentStream,
                                        WatermarkConfig config,
                                        float x, float y, float angle) throws IOException {
        contentStream.saveGraphicsState();

        // ✅ 正确顺序:先 beginText,再 setTextMatrix
        contentStream.beginText();
        contentStream.setTextMatrix(Matrix.getRotateInstance(angle, x, y));
        contentStream.newLineAtOffset(0, 0);
        contentStream.showText(config.getWatermarkText());
        contentStream.endText();

        contentStream.restoreGraphicsState();
    }
}

三、使用示例

示例1:所有页面居中水印(最简单场景)

java 复制代码
import java.io.File;
import java.awt.Color;

public class Example1_SimpleWatermark {
    public static void main(String[] args) {
        try {
            File sourceFile = new File("原始.pdf");
            File targetFile = new File("带水印.pdf");
            
            // 构建配置
            WatermarkConfig config = new WatermarkConfig.Builder()
                .text("机密文档")
                .fontSize(48f)
                .color(new Color(200, 0, 0, 128)) // 半透明红色
                .rotation(30)
                .position(WatermarkConfig.WatermarkPosition.CENTER)
                .opacity(0.6f)
                .build();
            
            // 执行添加水印
            EnhancedPdfWatermarkUtil.addWatermark(sourceFile, targetFile, config);
            
            System.out.println("水印添加成功!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

效果:所有页面中央出现"机密文档"水印,红色半透明,旋转30度。


示例2:奇偶页不同水印 + 平铺效果

java 复制代码
import java.io.File;
import java.awt.Color;

public class Example2_OddEvenWatermark {
    public static void main(String[] args) {
        try {
            File sourceFile = new File("原始.pdf");
            File targetFile = new File("奇偶水印.pdf");
            
            // 奇数页配置:平铺灰色水印
            WatermarkConfig oddPageConfig = new WatermarkConfig.Builder()
                .text("内部专用")
                .fontSize(32f)
                .color(Color.GRAY)
                .rotation(45)
                .enableTiling(3, 3) // 3x3平铺
                .targetPages(WatermarkConfig.PageFilterStrategy.ODD_PAGES)
                .build();
            
            // 偶数页配置:居中红色大水印
            WatermarkConfig evenPageConfig = new WatermarkConfig.Builder()
                .text("CONFIDENTIAL")
                .fontSize(56f)
                .color(new Color(255, 0, 0, 150))
                .rotation(-30)
                .position(WatermarkConfig.WatermarkPosition.CENTER)
                .targetPages(WatermarkConfig.PageFilterStrategy.EVEN_PAGES)
                .build();
            
            // 组合多水印配置
            WatermarkConfig combinedConfig = new WatermarkConfig.Builder()
                .text("主水印")
                .addMultiWatermark(oddPageConfig)
                .addMultiWatermark(evenPageConfig)
                .build();
            
            EnhancedPdfWatermarkUtil.addWatermark(sourceFile, targetFile, combinedConfig);
            
            System.out.println("奇偶页水印添加成功!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

效果

  • 奇数页:灰色"内部专用"水印,3x3平铺
  • 偶数页:红色"CONFIDENTIAL"水印,居中大字号

示例3:指定页码 + 自定义位置

java 复制代码
import java.io.File;
import java.awt.Color;

public class Example3_SpecificPages {
    public static void main(String[] args) {
        try {
            File sourceFile = new File("原始.pdf");
            File targetFile = new File("指定页水印.pdf");
            
            // 配置:只在第1、3、5页添加水印
            WatermarkConfig config = new WatermarkConfig.Builder()
                .text("草稿版本")
                .fontSize(24f)
                .color(Color.BLUE)
                .rotation(0) // 不旋转
                .customPosition(200f, 300f) // 自定义坐标
                .targetPages(0, 2, 4) // 页码从0开始,即第1、3、5页
                .build();
            
            EnhancedPdfWatermarkUtil.addWatermark(sourceFile, targetFile, config);
            
            System.out.println("指定页水印添加成功!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

效果:第1、3、5页的坐标(200, 300)处出现蓝色"草稿版本"水印,不旋转。


示例4:Web流式输出 + 高密度平铺

java 复制代码
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import java.awt.Color;
import java.io.InputStream;
import java.io.OutputStream;

public class Example4_WebStream {
    public void downloadWatermarkedPdf(InputStream pdfInput, 
                                        HttpServletResponse response) throws Exception {
        // 配置:高密度平铺水印
        WatermarkConfig config = new WatermarkConfig.Builder()
            .text("禁止外传")
            .font(PDType1Font.HELVETICA_BOLD)
            .fontSize(28f)
            .color(new Color(150, 150, 150, 100))
            .rotation(45)
            .enableTiling(5, 4, 0.2f, 0.3f) // 5x4平铺,带偏移
            .opacity(0.4f)
            .targetPages(WatermarkConfig.PageFilterStrategy.ALL_PAGES)
            .build();
        
        // 设置响应头
        response.setContentType("application/pdf");
        response.setHeader("Content-Disposition", "attachment; filename=watermarked.pdf");
        
        // 流式输出
        try (OutputStream output = response.getOutputStream()) {
            EnhancedPdfWatermarkUtil.addWatermark(pdfInput, output, config);
        }
    }
}

效果:用户下载的PDF文件中,所有页面都布满5x4网格的"禁止外传"水印。


四、核心功能详解

4.1 页面过滤策略

策略 说明 使用场景
ALL_PAGES 所有页面 默认场景,确保每页都有水印
EVEN_PAGES 仅偶数页 奇偶页不同水印的需求
ODD_PAGES 仅奇数页 奇偶页不同水印的需求
SPECIFIC_PAGES 指定页码 仅对特定页面(如封面、结尾页)加水印

代码示例

java 复制代码
// 偶数页加水印
.targetPages(WatermarkConfig.PageFilterStrategy.EVEN_PAGES)

// 指定第1、3、5页加水印(页码从0开始)
.targetPages(0, 2, 4)

4.2 水印位置配置

支持8种预设位置 + 自定义坐标:

位置枚举 说明
CENTER 页面居中
TOP_LEFT 左上角
TOP_RIGHT 右上角
BOTTOM_LEFT 左下角
BOTTOM_RIGHT 右下角
TOP_CENTER 上中
BOTTOM_CENTER 下中
CUSTOM 自定义坐标

代码示例

java 复制代码
// 使用预设位置
.position(WatermarkConfig.WatermarkPosition.TOP_RIGHT)

// 使用自定义坐标
.customPosition(500f, 200f)

4.3 平铺模式详解

平铺模式通过网格布局,让水印覆盖整个页面,避免单一水印被截断的问题。

核心参数

  • densityX:X轴平铺密度(每页重复次数)
  • densityY:Y轴平铺密度(每页重复次数)
  • offsetX:X轴偏移量(百分比 0-1)
  • offsetY:Y轴偏移量(百分比 0-1)

可视化示意

复制代码
3x3平铺(offsetX=0.25, offsetY=0.25):
┌─────────────────────────────┐
│  ✓        ✓        ✓      │
│     ✓        ✓        ✓    │
│        ✓        ✓        ✓│
└─────────────────────────────┘

5x4平铺(offsetX=0.2, offsetY=0.3):
┌─────────────────────────────┐
│   ✓   ✓   ✓   ✓   ✓       │
│  ✓   ✓   ✓   ✓   ✓        │
│ ✓   ✓   ✓   ✓   ✓         │
│✓   ✓   ✓   ✓   ✓          │
└─────────────────────────────┘

代码示例

java 复制代码
// 3x3平铺,默认偏移
.enableTiling(3, 3)

// 5x4平铺,自定义偏移
.enableTiling(5, 4, 0.2f, 0.3f)

4.4 多水印组合

支持在同一文档中添加多个不同的水印,满足复杂业务场景。

典型应用场景

  • 主水印 + 副水印(如"机密" + "禁止外传")
  • 不同页面不同水印(奇偶页策略)
  • 不同位置不同样式的水印组合

代码示例

java 复制代码
WatermarkConfig config = new WatermarkConfig.Builder()
    .text("主水印")
    .addMultiWatermark(watermarkConfig1)
    .addMultiWatermark(watermarkConfig2)
    .addMultiWatermark(watermarkConfig3)
    .build();

五、常见问题与解决方案

Q1:水印显示乱码或字体不正确?

原因:PDFBox默认字体不支持中文。

解决方案:加载外部中文字体文件。

java 复制代码
// 加载中文字体
PDFont chineseFont = PDType0Font.load(document, 
    new File("C:/Windows/Fonts/simhei.ttf"));

WatermarkConfig config = new WatermarkConfig.Builder()
    .font(chineseFont)
    .text("机密文档")
    .build();

Q2:水印在原有内容下方,被遮挡了?

原因:PDFBox默认将新内容添加在页面内容流末尾。

解决方案:调整内容流添加顺序(需要修改PDFBox底层处理逻辑,较复杂)。

替代方案:提高水印透明度,使其即使被遮挡也能隐约可见。

java 复制代码
.opacity(0.8f) // 提高不透明度

Q3:水印文字过长,超出页面边界?

原因:字体大小或位置设置不当。

解决方案

  • 调小字体大小
  • 使用平铺模式,让水印分布在页面多个位置
  • 调整旋转角度,减少水平空间占用
java 复制代码
.fontSize(24f) // 调小字体
.rotation(60) // 增大旋转角度

Q4:如何添加图片水印?

当前方案限制:本文仅提供文字水印实现。

扩展方案 :使用PDFBox的PDImageXObject加载图片,通过drawImage方法绘制。

java 复制代码
// 示例代码(需自行扩展WatermarkConfig)
PDImageXObject image = PDImageXObject.createFromFile("watermark.png", document);
contentStream.drawImage(image, x, y, width, height);

六、性能优化建议

6.1 批量处理优化

对于大批量PDF文件处理,建议:

  1. 使用线程池:并行处理多个PDF文件
  2. 复用字体对象:避免重复加载字体文件
  3. 流式处理:大文件使用流模式,避免内存溢出

示例代码

java 复制代码
// 使用线程池批量处理
ExecutorService executor = Executors.newFixedThreadPool(4);

List<File> files = Arrays.asList(new File("dir").listFiles());
for (File file : files) {
    executor.submit(() -> {
        // 处理逻辑
        EnhancedPdfWatermarkUtil.addWatermark(source, target, config);
    });
}

executor.shutdown();

6.2 内存优化

对于超大PDF文件(几百MB以上):

  1. 使用流模式:避免一次性加载整个文件到内存
  2. 分页处理:逐页处理,及时释放内存
  3. 增加JVM堆内存-Xmx2g 或更大

示例代码

java 复制代码
// 流模式处理大文件
try (InputStream input = new FileInputStream("large.pdf");
     OutputStream output = new FileOutputStream("output.pdf")) {
    EnhancedPdfWatermarkUtil.addWatermark(input, output, config);
}

七、总结

本文提供了一个生产级的PDF水印解决方案,具备以下核心优势:

  • 高度可配置:支持文字、字体、颜色、旋转角度、透明度等全方位参数配置
  • 智能页面控制:支持全部页面、奇偶页、指定页码等灵活策略
  • 平铺模式:支持水印平铺布局,避免单一水印被截断
  • 多水印组合:支持在同一文档中添加多个不同的水印
  • 建造者模式:链式调用,代码简洁优雅
  • 双模式支持:既支持文件操作(批量处理),也支持流操作(Web响应场景)

适用场景

  • 企业文档管理系统
  • 合同、报告等敏感文档处理
  • Web下载文件自动添加水印
  • 批量PDF文件处理任务
相关推荐
一路向北⁢2 小时前
Spring Boot 3 整合 SSE (Server-Sent Events) 企业级最佳实践(二)
java·数据库·spring boot·sse·通信
chilavert3182 小时前
技术演进中的开发沉思-349:高效并发(下)
java·jvm
PPPPPaPeR.2 小时前
从零实现一个简易 Shell:理解 Linux 进程与命令执行
linux·开发语言·c++
Yorlen_Zhang2 小时前
python Tkinter Frame 深度解析与实战指南
开发语言·python
lly2024062 小时前
Eclipse 关闭项目详解
开发语言
LXS_3572 小时前
C++常用容器(下)---stack、queue、list、set、map
开发语言·c++·学习方法·改行学it
德育处主任Pro2 小时前
『NAS』告别付费和广告,在群晖部署PDF工具箱-bentopdf
pdf·nas
愚者游世2 小时前
list Initialization各版本异同
开发语言·c++·学习·程序人生·算法
Poetinthedusk2 小时前
WPF应用跟随桌面切换
开发语言·wpf