在 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) 非关系型数据库 关系型数据库:以 表格 的形式存在,以 行和列 的形式存取数据,一系列的行和列被…...
stm32G473的flash模式是单bank还是双bank?
今天突然有人stm32G473的flash模式是单bank还是双bank?由于时间太久,我真忘记了。搜搜发现,还真有人和我一样。见下面的链接:https://shequ.stmicroelectronics.cn/forum.php?modviewthread&tid644563 根据STM32G4系列参考手…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...
YSYX学习记录(八)
C语言,练习0: 先创建一个文件夹,我用的是物理机: 安装build-essential 练习1: 我注释掉了 #include <stdio.h> 出现下面错误 在你的文本编辑器中打开ex1文件,随机修改或删除一部分,之后…...
Springcloud:Eureka 高可用集群搭建实战(服务注册与发现的底层原理与避坑指南)
引言:为什么 Eureka 依然是存量系统的核心? 尽管 Nacos 等新注册中心崛起,但金融、电力等保守行业仍有大量系统运行在 Eureka 上。理解其高可用设计与自我保护机制,是保障分布式系统稳定的必修课。本文将手把手带你搭建生产级 Eur…...
Matlab | matlab常用命令总结
常用命令 一、 基础操作与环境二、 矩阵与数组操作(核心)三、 绘图与可视化四、 编程与控制流五、 符号计算 (Symbolic Math Toolbox)六、 文件与数据 I/O七、 常用函数类别重要提示这是一份 MATLAB 常用命令和功能的总结,涵盖了基础操作、矩阵运算、绘图、编程和文件处理等…...
大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计
随着大语言模型(LLM)参数规模的增长,推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长,而KV缓存的内存消耗可能高达数十GB(例如Llama2-7B处理100K token时需50GB内存&a…...
安宝特方案丨船舶智造的“AR+AI+作业标准化管理解决方案”(装配)
船舶制造装配管理现状:装配工作依赖人工经验,装配工人凭借长期实践积累的操作技巧完成零部件组装。企业通常制定了装配作业指导书,但在实际执行中,工人对指导书的理解和遵循程度参差不齐。 船舶装配过程中的挑战与需求 挑战 (1…...
STM32HAL库USART源代码解析及应用
STM32HAL库USART源代码解析 前言STM32CubeIDE配置串口USART和UART的选择使用模式参数设置GPIO配置DMA配置中断配置硬件流控制使能生成代码解析和使用方法串口初始化__UART_HandleTypeDef结构体浅析HAL库代码实际使用方法使用轮询方式发送使用轮询方式接收使用中断方式发送使用中…...
快刀集(1): 一刀斩断视频片头广告
一刀流:用一个简单脚本,秒杀视频片头广告,还你清爽观影体验。 1. 引子 作为一个爱生活、爱学习、爱收藏高清资源的老码农,平时写代码之余看看电影、补补片,是再正常不过的事。 电影嘛,要沉浸,…...
Oracle11g安装包
Oracle 11g安装包 适用于windows系统,64位 下载路径 oracle 11g 安装包...
