在 Java 中使用 Apache POI 为 Word 文档添加水印

在 Java 中使用 Apache POI 为 Word 文档添加水印

在日常办公中,我们经常需要给 Word 文档添加水印,以标明文件的机密性或归属权。本文将介绍如何使用 Apache POI 库在 Java 中给 Word 文档添加水印。

技术栈
  • Apache POI :用于操作 Word 文档(.docx
  • VML(矢量标记语言):用于绘制水印文本
实现思路
  1. 读取 Word 文档
  2. 在页眉中插入水印
  3. 处理水印样式,如字体、颜色、透明度等
  4. 生成新文件并保存
代码实现

以下是一个完整的 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 文档中添加水印。该方法适用于各种办公场景,如合同文件、内部文档等。如果你有更复杂的需求,如图片水印或动态水印,也可以扩展此方法。

希望本文对你有所帮助,欢迎交流讨论!

相关推荐
白晨并不是很能熬夜17 分钟前
【JVM】字节码指令集
java·开发语言·汇编·jvm·数据结构·后端·javac
火烧屁屁啦23 分钟前
【JavaEE进阶】Spring AOP详解
java·spring·java-ee
卡布奇诺-海晨32 分钟前
JVM之Arthas的dashboard命令以及CPU飙高场景
java·spring boot
学c真好玩37 分钟前
Spring
java·后端·spring
沉默王二40 分钟前
更快更强!字节满血版DeepSeek在IDEA中真的爽!
java·前端·程序员
2301_8074492040 分钟前
字符串相乘——力扣
java·算法·leetcode
小五Z1 小时前
RabbitMQ高级特性--消息确认机制
java·rabbitmq·intellij-idea
Kevinyu_1 小时前
Maven
java·maven
nickxhuang1 小时前
【基础知识】回头看Maven基础
java·maven
SeaTunnel2 小时前
2025年 Apache SeaTunnel 2月份社区月报速递
apache