在 Java 中使用 Apache POI 为 Word 文档添加水印
在 Java 中使用 Apache POI 为 Word 文档添加水印
在日常办公中,我们经常需要给 Word 文档添加水印,以标明文件的机密性或归属权。本文将介绍如何使用 Apache POI 库在 Java 中给 Word 文档添加水印。
技术栈
- Apache POI:用于操作 Word 文档(
.docx
) - VML(矢量标记语言):用于绘制水印文本
实现思路
- 读取 Word 文档
- 在页眉中插入水印
- 处理水印样式,如字体、颜色、透明度等
- 生成新文件并保存
代码实现
以下是一个完整的 WordWatermarkUtils
工具类,它支持向 .docx
文件中添加水印。
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";}
}
如何使用
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);
}
完整代码
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"; // 文本旋转角度,如果不需要旋转水印,保持为 0private 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);// 距离页面顶部10ptsb.append(";left: ").append("auto"); // 防止左对齐sb.append(";margin-right: ").append(30); // 距离页面右侧10ptsb.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 文档中添加水印。该方法适用于各种办公场景,如合同文件、内部文档等。如果你有更复杂的需求,如图片水印或动态水印,也可以扩展此方法。
希望本文对你有所帮助,欢迎交流讨论!
相关文章:
在 Java 中使用 Apache POI 为 Word 文档添加水印
在 Java 中使用 Apache POI 为 Word 文档添加水印 在日常办公中,我们经常需要给 Word 文档添加水印,以标明文件的机密性或归属权。本文将介绍如何使用 Apache POI 库在 Java 中给 Word 文档添加水印。 技术栈 Apache POI:用于操作 Word 文…...

贪心算法二
> 作者:დ旧言~ > 座右铭:松树千年终是朽,槿花一日自为荣。 > 目标:了解什么是贪心算法,并且掌握贪心算法。 > 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安! >…...

【大模型安全】大模型的技术风险
【大模型安全】大模型的技术风险 1.DDoS攻击2.常见的传统网络攻击方式3.恶意意图的识别4.AI生成虚假信息传播5.利用AI进行黑客攻击6.模型对抗攻击7.后门攻击8.Prompt攻击9.数据投毒攻击10.模型窃取攻击11.数据窃取攻击 1.DDoS攻击 2023年11月9日凌晨,OpenAI在官网公…...
Java 线程池中 shutdown 与 shutdownNow 的区别是什么?
Java 线程池中 shutdown 与 shutdownNow 的区别 核心行为差异 | 方法 | 行为描述 | |----------------|----------------------------------------------------------------------------| | shutdown | 平缓关闭线程池:1. 停止接受新任务。2. 已提交的任务ÿ…...

基于Spring Boot的共享学习经验系统的设计与实现
目录 摘 要 第1章 绪论 1.1研究背景与意义 1.2国内外现状 1.3研究目标 第2章 需求分析 2.1业务需求 2.1.1业务概述 2.1.2业务流程 2.2.1用例概述 2.2.2用例描述 2.3非功能性需求 第3章 系统设计 3.1技术路线 3.2系统功能模块设计 3.3系统架构 3.4数据库设计 3.4.1概念结构设…...
【简单的C++围棋游戏开发示例】
C围棋游戏开发简单示例(控制台版) 核心代码实现 #include <iostream> #include <vector> #include <queue> using namespace std;const int SIZE 9; // 简化棋盘为9x9:ml-citation{ref"1" data"citationList&…...
单片机中的基础外设GPIO的知识和应用—(6)
GPIO(通用输入输出)是单片机与外部世界交互的重要接口。单片机的GPIO引脚可以灵活配置为输入、输出、中断或复用功能,广泛应用于LED控制、按键读取、传感器通信等场景。下文以STM32F103C8T6的GPIO为例。有些51单片机IO功能有的稍微有不同&…...

10-Agent循环分析新闻并输出总结报告
目录 关键词 摘要 速览 自动新闻总结与行业分析报告生成流程 创建深度行业分析报告的工作流 测试用例执行与调试 业务逻辑与循环处理任务 演示如何在循环体中添加链接读取工具 使用大模型处理和分析新闻信息 构建循环分析新闻并生成综合报告的流程 分析和优化慢速循…...
十二、Redis Cluster(集群)详解:原理、搭建、数据分片与读写分离
Redis Cluster(集群)详解:原理、搭建、数据分片与读写分离 Redis Cluster 是 Redis 官方提供的分布式存储方案,通过数据分片(Sharding)实现 水平扩展(scalability),并提供 高可用性(HA) 和 故障自动转移(failover) 能力,解决了单机 Redis 内存受限、主从复制故障…...
贪心算法解题框架+经典反例分析,效率提升300%
贪心算法是一种在每一步选择中都采取当前状态下的最优决策,从而希望最终达到全局最优解的算法策略。以下从其定义、特点、一般步骤、应用场景及实例等方面进行讲解: 定义与基本思想 • 贪心算法在对问题求解时,总是做出在当前看来是最好的选…...
策略设计模式-下单
1、定义一个下单context类 通过这类来判断具体使用哪个实现类,可以通过一些枚举或者条件来判断 import com.alibaba.fastjson.JSON; import com.tc.common.exception.BusinessException; import com.tc.common.user.YjkUserDetails; import com.tc.institution.cons…...

Go加spy++隐藏窗口
最近发现有些软件的窗口就像狗皮膏药一样,关也关不掉,一点就要登录,属实是有点不爽了。 窗口的进程不能杀死,但是窗口我不想要。思路很简单,用 spy 找到要隐藏的窗口的句柄,然后调用 Windows 的 ShowWindo…...
React基础之tsx语法
tsx在jsx的基础上添加了新的类型,除此之外没有任何区别 事件绑定 function App() { const handleClick()>{ console.log(button被点击了); } return( <div className"App"> <button onClick{handleClick}>click me</button> </di…...

一体机:DeepSeek性能的“隐形枷锁”!
一体机是DeepSeek交付的最佳方式吗? 恰恰相反,一体机是阻碍DeepSeek提升推理性能的最大绊脚石。 为啥? 只因DeepSeek这个模型有点特殊,它是个高稀疏度的MoE模型。 MoE这种混合专家模型,设计的初衷是通过“激活一堆专…...
ALBEF的动量蒸馏(Momentum distillation)
简单记录学习~ 一、传统 ITC Loss 的局限性 One-Hot Label 的缺陷 传统对比学习依赖严格对齐的图文对,通过交叉熵损失(如 softmax 归一化的相似度矩阵)强制模型将匹配的图文对相似度拉高,非匹配对相似度压低11。但 one…...
浏览器WEB播放RTSP
注意:浏览器不能直接播放RTSP,必须转换后都能播放。这一点所有的播放都是如此。 参考 https://github.com/kyriesent/node-rtsp-stream GitHub - phoboslab/jsmpeg: MPEG1 Video Decoder in JavaScript 相关文件方便下载 https://download.csdn.net…...

将PDF转为Word的在线工具
参考视频:外文翻译 文章目录 一、迅捷PDF转换器二、Smallpdf 一、迅捷PDF转换器 二、Smallpdf...

03. 对象的创建,存储和访问原理
文章目录 01. 对象创建1.1 创建过程概览1.2 类加载检查1.3 为对象分配内存1.4 将内存空间初始化为零值1.5 设置对象的必要信息1.6 总结 02. 对象的内存布局2.1 对象头区域2.2 实例数据区域2.3 对齐填充区域2.4 总结 03. 对象的访问定位其他介绍01.关于我的博客 注:读…...

机器学习-GBDT算法
目录 一. GBDT 核心思想 二. GBDT 工作原理 **(1) 损失函数优化** **(2) 负梯度拟合** **(3) 模型更新** 三. GBDT 的关键步骤 四. GBDT 的核心优势 **(1) 高精度与鲁棒性** **(2) 处理缺失值** **(3) 特征重要性分析** 五. GBDT 的缺点 **(1) 训练…...

redis基础结构
title: redis基础结构 date: 2025-03-04 08:39:12 tags: redis categories: redis笔记 Redis入门 (NoSQL, Not Only SQL) 非关系型数据库 关系型数据库:以 表格 的形式存在,以 行和列 的形式存取数据,一系列的行和列被…...

Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动
一、前言说明 在2011版本的gb28181协议中,拉取视频流只要求udp方式,从2016开始要求新增支持tcp被动和tcp主动两种方式,udp理论上会丢包的,所以实际使用过程可能会出现画面花屏的情况,而tcp肯定不丢包,起码…...

云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地
借阿里云中企出海大会的东风,以**「云启出海,智联未来|打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办,现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...

抖音增长新引擎:品融电商,一站式全案代运营领跑者
抖音增长新引擎:品融电商,一站式全案代运营领跑者 在抖音这个日活超7亿的流量汪洋中,品牌如何破浪前行?自建团队成本高、效果难控;碎片化运营又难成合力——这正是许多企业面临的增长困局。品融电商以「抖音全案代运营…...
第25节 Node.js 断言测试
Node.js的assert模块主要用于编写程序的单元测试时使用,通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试,通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...

相机从app启动流程
一、流程框架图 二、具体流程分析 1、得到cameralist和对应的静态信息 目录如下: 重点代码分析: 启动相机前,先要通过getCameraIdList获取camera的个数以及id,然后可以通过getCameraCharacteristics获取对应id camera的capabilities(静态信息)进行一些openCamera前的…...

C# 类和继承(抽象类)
抽象类 抽象类是指设计为被继承的类。抽象类只能被用作其他类的基类。 不能创建抽象类的实例。抽象类使用abstract修饰符声明。 抽象类可以包含抽象成员或普通的非抽象成员。抽象类的成员可以是抽象成员和普通带 实现的成员的任意组合。抽象类自己可以派生自另一个抽象类。例…...

让AI看见世界:MCP协议与服务器的工作原理
让AI看见世界:MCP协议与服务器的工作原理 MCP(Model Context Protocol)是一种创新的通信协议,旨在让大型语言模型能够安全、高效地与外部资源进行交互。在AI技术快速发展的今天,MCP正成为连接AI与现实世界的重要桥梁。…...
AspectJ 在 Android 中的完整使用指南
一、环境配置(Gradle 7.0 适配) 1. 项目级 build.gradle // 注意:沪江插件已停更,推荐官方兼容方案 buildscript {dependencies {classpath org.aspectj:aspectjtools:1.9.9.1 // AspectJ 工具} } 2. 模块级 build.gradle plu…...
【Java学习笔记】BigInteger 和 BigDecimal 类
BigInteger 和 BigDecimal 类 二者共有的常见方法 方法功能add加subtract减multiply乘divide除 注意点:传参类型必须是类对象 一、BigInteger 1. 作用:适合保存比较大的整型数 2. 使用说明 创建BigInteger对象 传入字符串 3. 代码示例 import j…...
【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的“no matching...“系列算法协商失败问题
【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的"no matching..."系列算法协商失败问题 摘要: 近期,在使用较新版本的OpenSSH客户端连接老旧SSH服务器时,会遇到 "no matching key exchange method found", "n…...