找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

277

积分

0

好友

27

主题
发表于 昨天 23:45 | 查看: 11| 回复: 0

在诸如企业办公自动化、数据报表分发或文档安全管控等场景中,我们常常需要对 Excel 文件添加水印,以标识文档状态(如“机密”、“草稿”、“内部使用”等)或起到防伪溯源的作用。然而,现实中的需求往往更为复杂:目标 Excel 文件可能已经包含旧的水印,直接叠加新水印会导致视觉混乱与格式错乱。

因此,一个健壮的解决方案需要具备两个核心能力:

  1. 识别并清除已有水印
  2. 精准添加新的水印

本文将基于 Apache POI 库,详细讲解如何使用Java实现这一完整的“替换”流程,适用于 .xlsx 格式的 Excel 文件,并提供可运行的代码示例。

一、Excel 水印的本质

首先需要明确:Excel 并不像 Word 或 PDF 那样原生支持“水印”功能。我们通常所说的“Excel 水印”,是通过以下方式模拟实现的:

  • 在工作表绘图层(Drawing Layer)插入半透明图片
  • 使用文本框(TextBox)绘制旋转文字
  • 设置工作表背景图(此方式无法打印,且为全表覆盖)
  • 利用页眉/页脚(仅在打印时可见)

其中,在绘图层插入 PNG 图片是最常用且最灵活的方式,也是本文采用的技术路线。它的优点在于:

  • 可自由控制位置、大小、透明度
  • 支持任意文字或图形内容
  • 在屏幕显示和打印时均可见(除非用户手动隐藏图形对象)

二、技术选型:Apache POI

Apache POI 是 Java 处理 Microsoft Office 文档的事实标准库。对于 .xlsx 文件(Office 2007+ 格式),我们使用其 XSSF 模块,它基于 Open XML 标准构建。

三、代码实现详解

3.1 Maven 依赖

<dependencies>
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>4.1.2</version>
    </dependency>
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>4.1.2</version>
    </dependency>
    <!-- 可选:增强图像处理兼容性 -->
    <dependency>
        <groupId>com.twelvemonkeys.imageio</groupId>
        <artifactId>imageio-core</artifactId>
        <version>3.9.4</version>
    </dependency>
</dependencies>

3.2 生成水印图片工具类

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class WaterMarkHandler {
    public static ByteArrayOutputStream createWaterMark(String content) throws IOException {
        int width = 500;
        int height = 300;
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        String fontType = "微软雅黑";
        int fontStyle = Font.BOLD;
        int fontSize = 20;
        Font font = new Font(fontType, fontStyle, fontSize);
        Graphics2D g2d = image.createGraphics();
        image = g2d.getDeviceConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT);
        g2d.dispose();
        g2d = image.createGraphics();
        g2d.setColor(new Color(0, 0, 0, 20)); // 设置字体颜色和透明度
        g2d.setStroke(new BasicStroke(1));
        g2d.setFont(font);
        g2d.rotate(-0.5, (double) image.getWidth() / 2, (double) image.getHeight() / 2); // 设置倾斜度
        FontRenderContext context = g2d.getFontRenderContext();
        Rectangle2D bounds = font.getStringBounds(content, context);
        double x = (width - bounds.getWidth()) / 2;
        double y = (height - bounds.getHeight()) / 2;
        double ascent = -bounds.getY();
        double baseY = y + ascent;
        g2d.drawString(content, (int) x, (int) baseY);
        g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
        g2d.dispose();
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        ImageIO.write(image, "png", os);
        return os;
    }
}

3.3 主处理逻辑:清除旧水印 + 添加新水印

import org.apache.poi.ooxml.POIXMLDocumentPart;
import org.apache.poi.openxml4j.opc.PackagePartName;
import org.apache.poi.openxml4j.opc.PackageRelationship;
import org.apache.poi.openxml4j.opc.TargetMode;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public final class ExcelWatermarkUtil {
    /**
     * 给 Excel 添加水印(内存到内存)
     *
     * @param excelData     原始 Excel 文件的 byte[]
     * @param watermarkText 水印文字(如 "CONFIDENTIAL")
     * @return 添加水印后的 Excel byte[]
     * @throws IOException
     */
    public static byte[] addWatermarkToExcel(byte[] excelData, String watermarkText) throws IOException {
        ByteArrayOutputStream waterMark = WaterMarkHandler.createWaterMark(watermarkText);
        try (ByteArrayInputStream bis = new ByteArrayInputStream(excelData);
             ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            XSSFWorkbook workbook = new XSSFWorkbook(bis);
            // 清除所有工作表中的旧图片(即旧水印)
            for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
                XSSFSheet sheet = workbook.getSheetAt(i);
                try {
                    sheet.getCTWorksheet().unsetPicture();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            // 将新水印图片添加到工作簿
            int pictureIdx = workbook.addPicture(waterMark.toByteArray(), Workbook.PICTURE_TYPE_PNG);
            POIXMLDocumentPart poixmlDocumentPart = workbook.getAllPictures().get(pictureIdx);
            // 为每个工作表关联新水印图片
            for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
                XSSFSheet sheet = workbook.getSheetAt(i);
                PackagePartName ppn = poixmlDocumentPart.getPackagePart().getPartName();
                String relType = XSSFRelation.IMAGES.getRelation();
                PackageRelationship pr = sheet.getPackagePart().addRelationship(ppn, TargetMode.INTERNAL, relType, null);
                sheet.getCTWorksheet().addNewPicture().setId(pr.getId());
            }
            workbook.write(bos);
            return bos.toByteArray();
        }
    }
}

四、使用示例

public void addWatermarkToExcel() throws IOException {
    byte[] fileBytes = Files.readAllBytes(Paths.get("/path/to/test.xlsx"));
    byte[] addWatermarkToExcel = ExcelWatermarkUtil.addWatermarkToExcel(fileBytes, "王叔叔 12122121212 2025-12-05 09:17:35");
    Files.write(Paths.get("/path/to/test_with_new_watermark.xlsx"), addWatermarkToExcel);
}

原水印: 图片

替换后的水印: 图片

五、关键问题与优化建议

1. 如何精准识别“水印”而非普通图片?

当前示例代码删除了工作表中的所有图片对象,这在仅含水印的简单报表中可行。但如果文件包含 Logo、业务图表等,则会误删。建议采用以下策略进行优化:

  • 添加水印时记录元信息:例如,在创建水印图片时,通过底层 API 为其设置一个特定的名称(如 “WATERMARK_CONFIDENTIAL”),后续通过 picture.getPictureData().getFileName() 进行判断。
  • 基于位置与尺寸过滤:水印通常具有居中、大面积、低透明度等特征,可以通过这些属性进行筛选。
  • 使用自定义属性标记:通过 POI 的 CTPicture 等底层 API 添加自定义标记属性(属于高级用法)。

2. 水印位置适配

上述工具类生成的水印图片是固定尺寸。如需实现动态全表居中覆盖,可以结合工作表的最大行列范围进行计算和定位。

3. 性能优化建议

  • 图片缓存:对于频繁添加的相同文字水印,可将生成的图片字节数组进行缓存,避免重复生成。
  • 大文件处理:对于非常大的只读 Excel 文件,可以考虑使用 SXSSFWorkbookReadOnlySharedStringsTable 等特性来提升读取速度和降低内存占用。

六、总结

本文完整演示了如何使用 Java 和 Apache POI 库实现 “先清除旧水印,再添加新水印” 的 Excel 文档处理流程。尽管 Excel 没有原生的水印功能,但通过在其绘图层插入半透明图片的方式,我们能够灵活、高效地满足各类业务需求。

最佳实践建议:在系统设计初期,就应对水印的添加方式(如图片命名规范、固定位置、统一尺寸和透明度)进行标准化约定,这将极大简化后续对水印的识别、管理与替换逻辑,为文档安全管控打下良好基础。

Github:github.com/apache/poi




上一篇:Redis缓存实战指南:从核心原理到集群架构的高并发避坑方案
下一篇:串口通讯原理与实战:ASCII码、数据帧及波特率详解
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-7 21:48 , Processed in 0.083860 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表