在 Java 中使用 Apache POI 为 Word 文档添加水印
在日常办公中,我们经常需要给 Word 文档添加水印,以标明文件的机密性或归属权。本文将介绍如何使用 Apache POI 库在 Java 中给 Word 文档添加水印。
技术栈
- Apache POI :用于操作 Word 文档(
.docx
) - VML(矢量标记语言):用于绘制水印文本
实现思路
- 读取 Word 文档
- 在页眉中插入水印
- 处理水印样式,如字体、颜色、透明度等
- 生成新文件并保存
代码实现
以下是一个完整的 WordWatermarkUtils
工具类,它支持向 .docx
文件中添加水印。
java
public class WordWatermarkUtils {
private String customText; // 水印文字
private String fontName = "微软雅黑"; // 字体
private int fontSize = 14; // 字体大小
private String fontColor = "#616161"; // 字体颜色
private String styleRotation = "0"; // 旋转角度
public WordWatermarkUtils(String customText) {
this.customText = customText;
}
public void addWatermarkToDoc(XWPFDocument doc) {
XWPFHeader header = doc.createHeader(HeaderFooterType.DEFAULT);
CTP ctp = header.createParagraph().getCTP();
CTR ctr = ctp.addNewR();
CTGroup group = CTGroup.Factory.newInstance();
CTShape shape = group.addNewShape();
shape.setStyle(getShapeStyle());
shape.setFillcolor(fontColor);
shape.addNewTextpath().setString(customText);
ctr.addNewPict().set(group);
}
private String getShapeStyle() {
return "position:absolute;width:300pt;height:50pt;rotation:" + styleRotation + ";fill-opacity:0.3";
}
}
如何使用
java
try (FileInputStream fis = new FileInputStream("input.docx");
FileOutputStream fos = new FileOutputStream("output.docx")) {
XWPFDocument doc = new XWPFDocument(fis);
WordWatermarkUtils watermark = new WordWatermarkUtils("机密文件");
watermark.addWatermarkToDoc(doc);
doc.write(fos);
}
完整代码
java
package com.demo;
import com.microsoft.schemas.office.office.CTLock;
import com.microsoft.schemas.vml.*;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.poi.wp.usermodel.HeaderFooterType;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFHeader;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
import weaver.general.BaseBean;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.UUID;
/**
* 微软office Word 水印机.
*/
public class WordWatermarkUtils {
private String customText; // 水印文字
private String fontName = "微软雅黑"; // word字体
private int fontSize = 14; // 字体大小
private String fontColor = "#616161"; // 字体颜色
private int widthPerWord = 3; // 一个字平均长度,单位pt,用于:计算文本占用的长度(文本总个数*单字长度)
private String styleTop = "10pt"; // 与顶部的间距
private String styleRotation = "0"; // 文本旋转角度,如果不需要旋转水印,保持为 0
private String source; // 源文件路径
private String dest = "D:\\WEAVER\\doc_tmp"; // 临时文件存储路径
BaseBean bean = new BaseBean();
public WordWatermarkUtils(String customText, String sourcePath) {
customText = customText + repeatString(" ", 8); // 水印文字之间使用8个空格分隔
this.customText = repeatString(customText, 1); // 一行水印重复水印文字次数
this.source = sourcePath;
}
/**
* 【核心方法】将输入流中的docx文档加载添加水印后输出到输出流中.
*
* @param inputStream docx文档输入流
* @param //outputStream 添加水印后docx文档的输出流
*/
public String makeSlopeWaterMark(InputStream inputStream, String filename) throws IOException {
// 创建临时文件
Path tempFile = createTempFile(inputStream);
if (tempFile == null || !Files.exists(tempFile) || Files.size(tempFile) == 0) {
throw new RuntimeException("-------- 输入文件为空或者无法被创建");
}
Path sourcePath = Paths.get(this.source); // 获取源文件路径
Path destinationDir = Paths.get(this.dest); // 将原文件复制到临时目标文件夹路径
// 确保目标文件夹存在
if (!Files.exists(destinationDir)) {
try {
Files.createDirectories(destinationDir);
} catch (IOException e) {
throw new RuntimeException("-------- 无法创建目标文件夹: " + e);
}
}
String unescapedFilenameString = StringEscapeUtils.unescapeHtml4(filename);// 转义字符复原
String tmpFileName = unescapedFilenameString.substring(0, unescapedFilenameString.lastIndexOf(".")) + "_watermark.docx";
Path tmpPath = destinationDir.resolve(tmpFileName); // 临时文件路径
Files.copy(sourcePath, tmpPath, StandardCopyOption.REPLACE_EXISTING);
bean.writeLog("-------- 通用合同右上角水印原文件复制成功并加上后缀 .docx: " + tmpPath);
try (BufferedInputStream buffIn = new BufferedInputStream(Files.newInputStream(tempFile));FileOutputStream fileOutputStream = new FileOutputStream(tmpPath.toFile())) {
// 加载文档
XWPFDocument doc = loadDocXDocument(buffIn, fileOutputStream);
if (doc == null) {
throw new RuntimeException("-------- 加载文档失败");
}
// 添加水印
try {
// 遍历文档,添加水印
for (int lineIndex = -10; lineIndex < 10; lineIndex++) {
styleTop = 200 * lineIndex + "pt";
waterMarkDocXDocument(doc);
}
// 写出添加水印后的文档
doc.write(fileOutputStream);
// 关闭
doc.close();
buffIn.close();
fileOutputStream.close();
return tmpPath.toString();
} catch (Exception e) {
throw new RuntimeException("-------- 水印操作失败: " + e);
}
} catch (Exception e) {
throw new RuntimeException("-------- 操作失败!" + e);
} finally {
// 删除原始文件
// deleteFile(sourcePath);
// 重命名临时文件为原文件名
// try {
// Files.move(tmpPath, sourcePath, StandardCopyOption.REPLACE_EXISTING);
// } catch (IOException e) {
// throw new RuntimeException("-------- 重命名失败: " + e);
// }
// 删除临时文件
deleteFile(tempFile);
}
}
/**
* 为文档添加水印<br />
* 实现参考了{@link org.apache.poi.xwpf.model.XWPFHeaderFooterPolicy# getWatermarkParagraph(String, int)}
*
* @param doc 需要被处理的docx文档对象
*/
private void waterMarkDocXDocument(XWPFDocument doc) {
int size = doc.getHeaderList().size();
if (size == 0) {
addWatermarkToHeader(doc);
} else {
// 遍历所有的节(Section)
for (Object header : doc.getHeaderList()) {
// 保留原有内容
for (Object paragraph : ((XWPFHeader) header).getParagraphs()) {
// 保持原有段落内容
((XWPFParagraph)paragraph).getText();
}
// 在页眉上添加水印(具体代码见上文)
addWatermarkToHeader((XWPFHeader)header);
}
}
}
/**
* 页眉存在其他内容
* @param header
*/
private void addWatermarkToHeader(XWPFHeader header) {
int size = header.getParagraphs().size();
if (size == 0) {
header.createParagraph();
}
CTP ctp = header.getParagraphArray(0).getCTP();
byte[] rsidr = ctp.getRsidR();
byte[] rsidrdefault = ctp.getRsidRDefault();
ctp.setRsidP(rsidr);
ctp.setRsidRDefault(rsidrdefault);
CTPPr ppr = ctp.addNewPPr();
ppr.addNewPStyle().setVal("Header");
// 添加水印
CTR ctr = ctp.addNewR();
CTRPr ctrpr = ctr.addNewRPr();
ctrpr.addNewNoProof();
CTGroup group = CTGroup.Factory.newInstance();
CTShapetype shapetype = group.addNewShapetype();
CTTextPath shapeTypeTextPath = shapetype.addNewTextpath();
shapeTypeTextPath.setOn(STTrueFalse.T);
shapeTypeTextPath.setFitshape(STTrueFalse.T);
CTLock lock = shapetype.addNewLock();
lock.setExt(STExt.VIEW);
CTShape shape = group.addNewShape();
shape.setId("PowerPlusWaterMarkObject");
shape.setSpid("_x0000_s102");
shape.setType("#_x0000_t136");
shape.setStyle(getShapeStyle()); // 设置形状样式(旋转,位置,相对路径等参数)
shape.setFillcolor(fontColor);
shape.setStroked(STTrueFalse.FALSE); // 字体设置为实心
CTTextPath shapeTextPath = shape.addNewTextpath(); // 绘制文本的路径
shapeTextPath.setStyle("font-family:" + fontName + ";font-size:" + fontSize + "pt"); // 设置文本字体与大小
shapeTextPath.setString(customText);
CTPicture pict = ctr.addNewPict();
pict.set(group);
}
/**
* 页眉不存在其他内容
* @param doc
*/
private void addWatermarkToHeader(XWPFDocument doc) {
XWPFHeader header = doc.createHeader(HeaderFooterType.DEFAULT); // 如果之前已经创建过 DEFAULT 的Header,将会复用之
int size = header.getParagraphs().size();
if (size == 0) {
header.createParagraph();
}
CTP ctp = header.getParagraphArray(0).getCTP();
byte[] rsidr = doc.getDocument().getBody().getPArray(0).getRsidR();
byte[] rsidrdefault = doc.getDocument().getBody().getPArray(0).getRsidRDefault();
ctp.setRsidP(rsidr);
ctp.setRsidRDefault(rsidrdefault);
CTPPr ppr = ctp.addNewPPr();
ppr.addNewPStyle().setVal("Header");
// 开始加水印
CTR ctr = ctp.addNewR();
CTRPr ctrpr = ctr.addNewRPr();
ctrpr.addNewNoProof();
CTGroup group = CTGroup.Factory.newInstance();
CTShapetype shapetype = group.addNewShapetype();
CTTextPath shapeTypeTextPath = shapetype.addNewTextpath();
shapeTypeTextPath.setOn(STTrueFalse.T);
shapeTypeTextPath.setFitshape(STTrueFalse.T);
CTLock lock = shapetype.addNewLock();
lock.setExt(STExt.VIEW);
CTShape shape = group.addNewShape();
shape.setId("PowerPlusWaterMarkObject");
shape.setSpid("_x0000_s102");
shape.setType("#_x0000_t136");
shape.setStyle(getShapeStyle()); // 设置形状样式(旋转,位置,相对路径等参数)
shape.setFillcolor(fontColor);
shape.setStroked(STTrueFalse.FALSE); // 字体设置为实心
CTTextPath shapeTextPath = shape.addNewTextpath(); // 绘制文本的路径
shapeTextPath.setStyle("font-family:" + fontName + ";font-size:" + fontSize + "pt"); // 设置文本字体与大小
shapeTextPath.setString(customText);
CTPicture pict = ctr.addNewPict();
pict.set(group);
}
/**
* 构建Shape的样式参数.
*
* @return
*/
private String getShapeStyle() {
StringBuilder sb = new StringBuilder();
sb.append("position: ").append("absolute"); // 文本path绘制的定位方式
sb.append(";width: ").append(customText.length() * widthPerWord).append("pt"); // 计算文本占用的长度(文本总个数*单字长度)
sb.append(";height: ").append(fontSize).append("pt"); // 字体高度
sb.append(";z-index: ").append("-251654144");// 控制层级
sb.append(";mso-wrap-edited: ").append("f");
sb.append(";margin-top: ").append(30);// 距离页面顶部10pt
sb.append(";left: ").append("auto"); // 防止左对齐
sb.append(";margin-right: ").append(30); // 距离页面右侧10pt
sb.append(";mso-position-horizontal-relative: ").append("page");// 水平基准为页面
sb.append(";mso-position-vertical-relative: ").append("page");// 垂直基准为页面
sb.append(";mso-position-horizontal: ").append("right"); // // 水平居右
//sb.append(";mso-position-vertical: ").append("bottom"); // 设置水印在底部
sb.append(";mso-position-vertical: ").append("other"); // 垂直居上
sb.append(";rotation: ").append(styleRotation);// 设置文本旋转角度
sb.append(";fill-opacity: ").append(0.6); // 设置水印的透明度为 60%
return sb.toString();
}
/**
* 加载docx格式的word文档.
*
* @param inputStream
* @param outputStream
* @return
*/
private XWPFDocument loadDocXDocument(InputStream inputStream, OutputStream outputStream) {
XWPFDocument doc;
try {
doc = new XWPFDocument(inputStream);
} catch (Exception e) {
throw new RuntimeException("文档加载失败!!");
}
return doc;
}
/**
* 创建临时文件.
*
* @param inputStream docx文档输入流
*/
private Path createTempFile(InputStream inputStream) {
Path tempFilePath = null;
inputStream = (inputStream == null) ? new ByteArrayInputStream(new byte[0]) : inputStream; // 如果传入了null输入流,转换成空数组流
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
bufferedInputStream.mark(0); // 输入流头部打上Mark(方便重读)
// 创建临时文件
try {
if (inputStream == null || inputStream.available() == 0) {
throw new RuntimeException("输入流为空或为 null.");
}
String uuid = UUID.randomUUID().toString();
tempFilePath = Files.createTempFile("dapeng_" + uuid, ".docx");
// 向临时文件写入数据
try (OutputStream tempout = Files.newOutputStream(tempFilePath)) {
IOUtils.copy(inputStream, tempout);
} catch (Exception e) { // 如果拷贝异常,删除临时文件
deleteFile(tempFilePath);
throw new RuntimeException("写入临时文件时出错.", e);
}
inputStream.close();
} catch (Exception e) {
// 这里表示创建临时文件失败
tempFilePath = null;
}
return tempFilePath;
}
/**
* 删除指定path的文件.
*
* @param path
*/
private void deleteFile(Path path) {
if (path != null && Files.exists(path)) {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
bean.writeLog("-------- 删除文件失败: " + path + " " + e);
}
}
}
/**
* 将指定字符串重复多次 (适配Java 1.8).
*/
private String repeatString(String pattern, int repeats) {
StringBuilder buffer = new StringBuilder(pattern.length() * repeats);
for (int i = 0; i < repeats; i++) {
buffer.append(pattern);
}
return buffer.toString();
}
}
总结
使用 Apache POI 和 VML,我们可以轻松地在 Word 文档中添加水印。该方法适用于各种办公场景,如合同文件、内部文档等。如果你有更复杂的需求,如图片水印或动态水印,也可以扩展此方法。
希望本文对你有所帮助,欢迎交流讨论!