在 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) 非关系型数据库 关系型数据库:以 表格 的形式存在,以 行和列 的形式存取数据,一系列的行和列被…...
P3 QT项目----记事本(3.8)
3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...
CocosCreator 之 JavaScript/TypeScript和Java的相互交互
引擎版本: 3.8.1 语言: JavaScript/TypeScript、C、Java 环境:Window 参考:Java原生反射机制 您好,我是鹤九日! 回顾 在上篇文章中:CocosCreator Android项目接入UnityAds 广告SDK。 我们简单讲…...
爬虫基础学习day2
# 爬虫设计领域 工商:企查查、天眼查短视频:抖音、快手、西瓜 ---> 飞瓜电商:京东、淘宝、聚美优品、亚马逊 ---> 分析店铺经营决策标题、排名航空:抓取所有航空公司价格 ---> 去哪儿自媒体:采集自媒体数据进…...
html css js网页制作成品——HTML+CSS榴莲商城网页设计(4页)附源码
目录 一、👨🎓网站题目 二、✍️网站描述 三、📚网站介绍 四、🌐网站效果 五、🪓 代码实现 🧱HTML 六、🥇 如何让学习不再盲目 七、🎁更多干货 一、👨…...
sipsak:SIP瑞士军刀!全参数详细教程!Kali Linux教程!
简介 sipsak 是一个面向会话初始协议 (SIP) 应用程序开发人员和管理员的小型命令行工具。它可以用于对 SIP 应用程序和设备进行一些简单的测试。 sipsak 是一款 SIP 压力和诊断实用程序。它通过 sip-uri 向服务器发送 SIP 请求,并检查收到的响应。它以以下模式之一…...
智能AI电话机器人系统的识别能力现状与发展水平
一、引言 随着人工智能技术的飞速发展,AI电话机器人系统已经从简单的自动应答工具演变为具备复杂交互能力的智能助手。这类系统结合了语音识别、自然语言处理、情感计算和机器学习等多项前沿技术,在客户服务、营销推广、信息查询等领域发挥着越来越重要…...
GruntJS-前端自动化任务运行器从入门到实战
Grunt 完全指南:从入门到实战 一、Grunt 是什么? Grunt是一个基于 Node.js 的前端自动化任务运行器,主要用于自动化执行项目开发中重复性高的任务,例如文件压缩、代码编译、语法检查、单元测试、文件合并等。通过配置简洁的任务…...
如何更改默认 Crontab 编辑器 ?
在 Linux 领域中,crontab 是您可能经常遇到的一个术语。这个实用程序在类 unix 操作系统上可用,用于调度在预定义时间和间隔自动执行的任务。这对管理员和高级用户非常有益,允许他们自动执行各种系统任务。 编辑 Crontab 文件通常使用文本编…...
stm32wle5 lpuart DMA数据不接收
配置波特率9600时,需要使用外部低速晶振...
Java多线程实现之Runnable接口深度解析
Java多线程实现之Runnable接口深度解析 一、Runnable接口概述1.1 接口定义1.2 与Thread类的关系1.3 使用Runnable接口的优势 二、Runnable接口的基本实现方式2.1 传统方式实现Runnable接口2.2 使用匿名内部类实现Runnable接口2.3 使用Lambda表达式实现Runnable接口 三、Runnabl…...
