Java實(shí)現(xiàn)PDF添加水印的完整方案
前言
在企業(yè)級(jí)應(yīng)用中,PDF水印是文檔安全管理的必備功能。無(wú)論是版權(quán)保護(hù)、版本標(biāo)識(shí),還是敏感信息管控,水印都扮演著重要角色。
本文將提供一個(gè)生產(chǎn)級(jí)的PDF水印解決方案,基于Apache PDFBox實(shí)現(xiàn),具備以下特點(diǎn):
- ? 高度可配置:支持文字、字體、顏色、旋轉(zhuǎn)角度、透明度等全方位參數(shù)配置
- ? 智能頁(yè)面控制:支持全部頁(yè)面、奇偶頁(yè)、指定頁(yè)碼等靈活策略
- ? 平鋪模式:支持水印平鋪布局,避免單一水印被截?cái)?/li>
- ? 多水印組合:支持在同一文檔中添加多個(gè)不同的水印
- ? 建造者模式:鏈?zhǔn)秸{(diào)用,代碼簡(jiǎn)潔優(yōu)雅
一、技術(shù)選型
為什么選擇Apache PDFBox?
| 對(duì)比項(xiàng) | Apache PDFBox | iText | OpenPDF |
|---|---|---|---|
| 開(kāi)源協(xié)議 | Apache 2.0(免費(fèi)) | AGPL(商業(yè)收費(fèi)) | LGPL(較寬松) |
| 功能完整性 | ????? | ????? | ???? |
| 社區(qū)活躍度 | 高 | 中等 | 中等 |
| 學(xué)習(xí)曲線(xiàn) | 平緩 | 陡峭 | 較平緩 |
| 推薦指數(shù) | ????? | ??? | ???? |
結(jié)論:對(duì)于企業(yè)項(xiàng)目,PDFBox是最優(yōu)選擇——開(kāi)源免費(fèi)、功能完整、社區(qū)活躍。
Maven依賴(lài)
<!-- Apache PDFBox for PDF處理 --> <dependency> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox</artifactId> <version>2.0.29</version> </dependency>
二、核心代碼實(shí)現(xiàn)
2.1 水印配置類(lèi)(WatermarkConfig)
采用建造者模式設(shè)計(jì),支持鏈?zhǔn)秸{(diào)用,所有參數(shù)都可靈活配置。
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水印配置類(lèi)
* 使用建造者模式構(gòu)建,支持鏈?zhǔn)秸{(diào)用
*/
public class WatermarkConfig {
// ==================== 基礎(chǔ)配置 ====================
private String watermarkText; // 水印文字(必填)
private PDFont font; // 字體
private float fontSize; // 字體大小
private Color color; // 顏色(支持透明度)
// ==================== 旋轉(zhuǎn)配置 ====================
private int rotationAngle; // 旋轉(zhuǎn)角度(度),支持正負(fù)值
// ==================== 位置配置 ====================
private WatermarkPosition position; // 水印位置枚舉
private Float customX; // 自定義X坐標(biāo)(position=CUSTOM時(shí)使用)
private Float customY; // 自定義Y坐標(biāo)(position=CUSTOM時(shí)使用)
// ==================== 平鋪配置 ====================
private boolean tilingEnabled; // 是否啟用平鋪模式
private int tilingDensityX; // X軸平鋪密度(每頁(yè)重復(fù)次數(shù))
private int tilingDensityY; // Y軸平鋪密度(每頁(yè)重復(fù)次數(shù))
private float tilingOffsetX; // X軸偏移量(百分比 0-1)
private float tilingOffsetY; // Y軸偏移量(百分比 0-1)
// ==================== 透明度配置 ====================
private float opacity; // 透明度 0-1(0完全透明,1完全不透明)
// ==================== 頁(yè)面過(guò)濾配置 ====================
private List<Integer> targetPageNumbers; // 目標(biāo)頁(yè)碼列表(從0開(kāi)始)
private PageFilterStrategy pageFilterStrategy; // 頁(yè)面過(guò)濾策略
// ==================== 多水印配置 ====================
private List<WatermarkConfig> multiWatermarks; // 多水印配置列表
/**
* 水印位置枚舉
*/
public enum WatermarkPosition {
CENTER, // 居中
TOP_LEFT, // 左上角
TOP_RIGHT, // 右上角
BOTTOM_LEFT, // 左下角
BOTTOM_RIGHT, // 右下角
TOP_CENTER, // 上中
BOTTOM_CENTER, // 下中
CUSTOM // 自定義坐標(biāo)
}
/**
* 頁(yè)面過(guò)濾策略
*/
public enum PageFilterStrategy {
ALL_PAGES, // 所有頁(yè)面(默認(rèn))
EVEN_PAGES, // 僅偶數(shù)頁(yè)
ODD_PAGES, // 僅奇數(shù)頁(yè)
SPECIFIC_PAGES // 指定頁(yè)碼
}
/**
* 私有構(gòu)造方法,使用Builder構(gòu)建
*/
private WatermarkConfig() {
// 設(shè)置默認(rèn)值
this.font = PDType1Font.HELVETICA_BOLD;
this.fontSize = 36f;
this.color = new Color(200, 200, 200, 128); // 默認(rèn)灰色半透明
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建造者類(lèi)
*/
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 水印執(zhí)行工具類(lèi)(EnhancedPdfWatermarkUtil)
核心執(zhí)行邏輯,支持流式和文件兩種模式。
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;
/**
* 增強(qiáng)版PDF水印工具
* 支持靈活配置、頁(yè)面過(guò)濾、平鋪模式、多水印組合
*/
public class EnhancedPdfWatermarkUtil {
/**
* 添加水?。髂J剑? 適合Web場(chǎng)景
*
* @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);
}
}
/**
* 添加水?。ㄎ募J剑? 適合批量處理
*
* @param sourceFile 源文件
* @param targetFile 目標(biāo)文件
* @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文檔對(duì)象
* @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);
}
}
}
}
}
/**
* 判斷頁(yè)面是否需要添加水印
*
* @param pageIndex 頁(yè)碼索引(從0開(kāi)始)
* @param config 水印配置
* @return true-需要添加,false-跳過(guò)
*/
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;
}
}
/**
* 向單頁(yè)添加水印
*
* @param document PDF文檔對(duì)象(必須傳入)
* @param page PDF頁(yè)面對(duì)象
* @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)) {
// 設(shè)置字體和顏色
contentStream.setFont(config.getFont(), config.getFontSize());
contentStream.setNonStrokingColor(config.getColor());
// 根據(jù)配置選擇繪制模式
if (config.isTilingEnabled()) {
// 平鋪模式
addTiledWatermark(contentStream, config, pageWidth, pageHeight);
} else {
// 單點(diǎn)模式
addSingleWatermark(contentStream, config, pageWidth, pageHeight);
}
}
}
/**
* 添加平鋪水?。ňW(wǎng)格布局)
*
* @param contentStream 內(nèi)容流
* @param config 水印配置
* @param pageWidth 頁(yè)面寬度
* @param pageHeight 頁(yè)面高度
* @throws IOException IO異常
*/
private static void addTiledWatermark(PDPageContentStream contentStream,
WatermarkConfig config,
float pageWidth, float pageHeight) throws IOException {
// 計(jì)算旋轉(zhuǎn)角度(弧度)
float angle = (float) Math.toRadians(config.getRotationAngle());
// 計(jì)算網(wǎng)格間距
float stepX = pageWidth / config.getTilingDensityX();
float stepY = pageHeight / config.getTilingDensityY();
// 遍歷網(wǎng)格繪制水印
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);
}
}
}
/**
* 添加單點(diǎn)水印(固定位置 - 已修復(fù)真正居中)
*
* @param contentStream 內(nèi)容流
* @param config 水印配置
* @param pageWidth 頁(yè)面寬度
* @param pageHeight 頁(yè)面高度
* @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());
// ? 計(jì)算文字寬度,實(shí)現(xiàn)真正的左右居中
float textWidth = config.getFont().getStringWidth(config.getWatermarkText())
/ 1000 * config.getFontSize();
// 根據(jù)位置枚舉計(jì)算坐標(biāo)
switch (config.getPosition()) {
case CENTER:
// ? 頁(yè)面中心 - 文字寬度的一半 = 文字居中
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:
// ? 頁(yè)面水平中心 - 文字寬度的一半
x = pageWidth / 2 - textWidth / 2;
y = pageHeight - 100;
break;
case BOTTOM_CENTER:
// ? 頁(yè)面水平中心 - 文字寬度的一半
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);
}
/**
* 繪制旋轉(zhuǎn)文字(核心方法 - 已修復(fù)方法調(diào)用順序)
*
* @param contentStream 內(nèi)容流
* @param config 水印配置
* @param x X坐標(biāo)
* @param y Y坐標(biāo)
* @param angle 旋轉(zhuǎn)角度(弧度)
* @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:所有頁(yè)面居中水?。ㄗ詈?jiǎn)單場(chǎng)景)
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");
// 構(gòu)建配置
WatermarkConfig config = new WatermarkConfig.Builder()
.text("機(jī)密文檔")
.fontSize(48f)
.color(new Color(200, 0, 0, 128)) // 半透明紅色
.rotation(30)
.position(WatermarkConfig.WatermarkPosition.CENTER)
.opacity(0.6f)
.build();
// 執(zhí)行添加水印
EnhancedPdfWatermarkUtil.addWatermark(sourceFile, targetFile, config);
System.out.println("水印添加成功!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
效果:所有頁(yè)面中央出現(xiàn)"機(jī)密文檔"水印,紅色半透明,旋轉(zhuǎn)30度。
示例2:奇偶頁(yè)不同水印 + 平鋪效果
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");
// 奇數(shù)頁(yè)配置:平鋪灰色水印
WatermarkConfig oddPageConfig = new WatermarkConfig.Builder()
.text("內(nèi)部專(zhuān)用")
.fontSize(32f)
.color(Color.GRAY)
.rotation(45)
.enableTiling(3, 3) // 3x3平鋪
.targetPages(WatermarkConfig.PageFilterStrategy.ODD_PAGES)
.build();
// 偶數(shù)頁(yè)配置:居中紅色大水印
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("奇偶頁(yè)水印添加成功!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
效果:
- 奇數(shù)頁(yè):灰色"內(nèi)部專(zhuān)用"水印,3x3平鋪
- 偶數(shù)頁(yè):紅色"CONFIDENTIAL"水印,居中大字號(hào)
示例3:指定頁(yè)碼 + 自定義位置
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("指定頁(yè)水印.pdf");
// 配置:只在第1、3、5頁(yè)添加水印
WatermarkConfig config = new WatermarkConfig.Builder()
.text("草稿版本")
.fontSize(24f)
.color(Color.BLUE)
.rotation(0) // 不旋轉(zhuǎn)
.customPosition(200f, 300f) // 自定義坐標(biāo)
.targetPages(0, 2, 4) // 頁(yè)碼從0開(kāi)始,即第1、3、5頁(yè)
.build();
EnhancedPdfWatermarkUtil.addWatermark(sourceFile, targetFile, config);
System.out.println("指定頁(yè)水印添加成功!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
效果:第1、3、5頁(yè)的坐標(biāo)(200, 300)處出現(xiàn)藍(lán)色"草稿版本"水印,不旋轉(zhuǎn)。
示例4:Web流式輸出 + 高密度平鋪
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();
// 設(shè)置響應(yīng)頭
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "attachment; filename=watermarked.pdf");
// 流式輸出
try (OutputStream output = response.getOutputStream()) {
EnhancedPdfWatermarkUtil.addWatermark(pdfInput, output, config);
}
}
}
效果:用戶(hù)下載的PDF文件中,所有頁(yè)面都布滿(mǎn)5x4網(wǎng)格的"禁止外傳"水印。
四、核心功能詳解
4.1 頁(yè)面過(guò)濾策略
| 策略 | 說(shuō)明 | 使用場(chǎng)景 |
|---|---|---|
ALL_PAGES | 所有頁(yè)面 | 默認(rèn)場(chǎng)景,確保每頁(yè)都有水印 |
EVEN_PAGES | 僅偶數(shù)頁(yè) | 奇偶頁(yè)不同水印的需求 |
ODD_PAGES | 僅奇數(shù)頁(yè) | 奇偶頁(yè)不同水印的需求 |
SPECIFIC_PAGES | 指定頁(yè)碼 | 僅對(duì)特定頁(yè)面(如封面、結(jié)尾頁(yè))加水印 |
代碼示例:
// 偶數(shù)頁(yè)加水印 .targetPages(WatermarkConfig.PageFilterStrategy.EVEN_PAGES) // 指定第1、3、5頁(yè)加水?。?yè)碼從0開(kāi)始) .targetPages(0, 2, 4)
4.2 水印位置配置
支持8種預(yù)設(shè)位置 + 自定義坐標(biāo):
| 位置枚舉 | 說(shuō)明 |
|---|---|
CENTER | 頁(yè)面居中 |
TOP_LEFT | 左上角 |
TOP_RIGHT | 右上角 |
BOTTOM_LEFT | 左下角 |
BOTTOM_RIGHT | 右下角 |
TOP_CENTER | 上中 |
BOTTOM_CENTER | 下中 |
CUSTOM | 自定義坐標(biāo) |
代碼示例:
// 使用預(yù)設(shè)位置 .position(WatermarkConfig.WatermarkPosition.TOP_RIGHT) // 使用自定義坐標(biāo) .customPosition(500f, 200f)
4.3 平鋪模式詳解
平鋪模式通過(guò)網(wǎng)格布局,讓水印覆蓋整個(gè)頁(yè)面,避免單一水印被截?cái)嗟膯?wèn)題。
核心參數(shù):
densityX:X軸平鋪密度(每頁(yè)重復(fù)次數(shù))densityY:Y軸平鋪密度(每頁(yè)重復(fù)次數(shù))offsetX:X軸偏移量(百分比 0-1)offsetY:Y軸偏移量(百分比 0-1)
可視化示意:
3x3平鋪(offsetX=0.25, offsetY=0.25): ┌─────────────────────────────┐ │ ? ? ? │ │ ? ? ? │ │ ? ? ?│ └─────────────────────────────┘ 5x4平鋪(offsetX=0.2, offsetY=0.3): ┌─────────────────────────────┐ │ ? ? ? ? ? │ │ ? ? ? ? ? │ │ ? ? ? ? ? │ │? ? ? ? ? │ └─────────────────────────────┘
代碼示例:
// 3x3平鋪,默認(rèn)偏移 .enableTiling(3, 3) // 5x4平鋪,自定義偏移 .enableTiling(5, 4, 0.2f, 0.3f)
4.4 多水印組合
支持在同一文檔中添加多個(gè)不同的水印,滿(mǎn)足復(fù)雜業(yè)務(wù)場(chǎng)景。
典型應(yīng)用場(chǎng)景:
- 主水印 + 副水印(如"機(jī)密" + “禁止外傳”)
- 不同頁(yè)面不同水?。ㄆ媾柬?yè)策略)
- 不同位置不同樣式的水印組合
代碼示例:
WatermarkConfig config = new WatermarkConfig.Builder()
.text("主水印")
.addMultiWatermark(watermarkConfig1)
.addMultiWatermark(watermarkConfig2)
.addMultiWatermark(watermarkConfig3)
.build();
五、常見(jiàn)問(wèn)題與解決方案
Q1:水印顯示亂碼或字體不正確?
原因:PDFBox默認(rèn)字體不支持中文。
解決方案:加載外部中文字體文件。
// 加載中文字體
PDFont chineseFont = PDType0Font.load(document,
new File("C:/Windows/Fonts/simhei.ttf"));
WatermarkConfig config = new WatermarkConfig.Builder()
.font(chineseFont)
.text("機(jī)密文檔")
.build();
Q2:水印在原有內(nèi)容下方,被遮擋了?
原因:PDFBox默認(rèn)將新內(nèi)容添加在頁(yè)面內(nèi)容流末尾。
解決方案:調(diào)整內(nèi)容流添加順序(需要修改PDFBox底層處理邏輯,較復(fù)雜)。
替代方案:提高水印透明度,使其即使被遮擋也能隱約可見(jiàn)。
.opacity(0.8f) // 提高不透明度
Q3:水印文字過(guò)長(zhǎng),超出頁(yè)面邊界?
原因:字體大小或位置設(shè)置不當(dāng)。
解決方案:
- 調(diào)小字體大小
- 使用平鋪模式,讓水印分布在頁(yè)面多個(gè)位置
- 調(diào)整旋轉(zhuǎn)角度,減少水平空間占用
.fontSize(24f) // 調(diào)小字體 .rotation(60) // 增大旋轉(zhuǎn)角度
Q4:如何添加圖片水???
當(dāng)前方案限制:本文僅提供文字水印實(shí)現(xiàn)。
擴(kuò)展方案:使用PDFBox的PDImageXObject加載圖片,通過(guò)drawImage方法繪制。
// 示例代碼(需自行擴(kuò)展WatermarkConfig)
PDImageXObject image = PDImageXObject.createFromFile("watermark.png", document);
contentStream.drawImage(image, x, y, width, height);
六、性能優(yōu)化建議
6.1 批量處理優(yōu)化
對(duì)于大批量PDF文件處理,建議:
- 使用線(xiàn)程池:并行處理多個(gè)PDF文件
- 復(fù)用字體對(duì)象:避免重復(fù)加載字體文件
- 流式處理:大文件使用流模式,避免內(nèi)存溢出
示例代碼:
// 使用線(xiàn)程池批量處理
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 內(nèi)存優(yōu)化
對(duì)于超大PDF文件(幾百M(fèi)B以上):
- 使用流模式:避免一次性加載整個(gè)文件到內(nèi)存
- 分頁(yè)處理:逐頁(yè)處理,及時(shí)釋放內(nèi)存
- 增加JVM堆內(nèi)存:
-Xmx2g或更大
示例代碼:
// 流模式處理大文件
try (InputStream input = new FileInputStream("large.pdf");
OutputStream output = new FileOutputStream("output.pdf")) {
EnhancedPdfWatermarkUtil.addWatermark(input, output, config);
}
七、總結(jié)
本文提供了一個(gè)生產(chǎn)級(jí)的PDF水印解決方案,具備以下核心優(yōu)勢(shì):
- ? 高度可配置:支持文字、字體、顏色、旋轉(zhuǎn)角度、透明度等全方位參數(shù)配置
- ? 智能頁(yè)面控制:支持全部頁(yè)面、奇偶頁(yè)、指定頁(yè)碼等靈活策略
- ? 平鋪模式:支持水印平鋪布局,避免單一水印被截?cái)?/li>
- ? 多水印組合:支持在同一文檔中添加多個(gè)不同的水印
- ? 建造者模式:鏈?zhǔn)秸{(diào)用,代碼簡(jiǎn)潔優(yōu)雅
- ? 雙模式支持:既支持文件操作(批量處理),也支持流操作(Web響應(yīng)場(chǎng)景)
適用場(chǎng)景:
- 企業(yè)文檔管理系統(tǒng)
- 合同、報(bào)告等敏感文檔處理
- Web下載文件自動(dòng)添加水印
- 批量PDF文件處理任務(wù)
以上就是Java實(shí)現(xiàn)PDF添加水印的完整方案的詳細(xì)內(nèi)容,更多關(guān)于Java PDF添加水印的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
spring?mvc?AOP切面方法未執(zhí)行的一種情況的分析和處理過(guò)程
這篇文章主要介紹了spring?mvc?AOP切面方法未執(zhí)行的一種情況的分析和處理,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05
Java數(shù)據(jù)結(jié)構(gòu)之插入排序與希爾排序
在本篇文章,我們將為小伙伴們進(jìn)行排序概念的基本講解并具體講解其中的兩種基礎(chǔ)排序:插入排序和希爾排序,希望小伙伴們能夠從中有所收獲2023-04-04
Java網(wǎng)絡(luò)編程實(shí)例——簡(jiǎn)單模擬在線(xiàn)聊天
學(xué)了java網(wǎng)絡(luò),也是該做個(gè)小案例來(lái)鞏固一下了。本次案例將使用UDP和多線(xiàn)程模擬即時(shí)聊天,簡(jiǎn)單練練手。2021-05-05
Java集合和IO流實(shí)現(xiàn)水果攤項(xiàng)目
最近閑來(lái)無(wú)事,使用java基礎(chǔ)知識(shí)集合和IO流做了一個(gè)簡(jiǎn)單的小項(xiàng)目,水果攤項(xiàng)目,用到GUI和Mysql數(shù)據(jù)庫(kù)搭建,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-06-06
SpringCloud微服務(wù)集成Dubbo的詳細(xì)過(guò)程
Apache?Dubbo?是一款易用、高性能的?WEB?和?RPC?框架,同時(shí)為構(gòu)建企業(yè)級(jí)微服務(wù)提供服務(wù)發(fā)現(xiàn)、流量治理、可觀(guān)測(cè)、認(rèn)證鑒權(quán)等能力、工具與最佳實(shí)踐,這篇文章主要介紹了SpringCloud微服務(wù)集成Dubbo,需要的朋友可以參考下2024-03-03
解決FeignClient發(fā)送post請(qǐng)求異常的問(wèn)題
這篇文章主要介紹了FeignClient發(fā)送post請(qǐng)求異常的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
測(cè)試環(huán)境頻繁Full GC問(wèn)題的解決思路分析
全文介紹了作者通過(guò)與調(diào)用方交互,發(fā)現(xiàn)welink-front服務(wù)不可用的問(wèn)題,通過(guò)jmap-heap和jstat-gccause命令,作者找到了問(wèn)題的原因是元數(shù)據(jù)區(qū)內(nèi)存使用率過(guò)高,觸發(fā)了FullGC,作者通過(guò)分析GC日志和堆內(nèi)存使用情況,確定了問(wèn)題的根本原因2025-01-01

