📋 技术备忘:图片加水印与 EXIF 保留方案
1. 问题根源:为什么信息会丢失?
- 工具底层限制 :Hutool 的
ImgUtil/Img基于 Java 原生ImageIO和Graphics2D。 - 处理机制 :
- 读取时:图片被解码为
BufferedImage,该对象仅包含像素数据,原文件的 Header(EXIF、GPS、拍摄设备等元数据)被抛弃。 - 写入时:基于像素数据重新编码生成新文件,文件头是全新的、不含元数据的"空白页"。
- 读取时:图片被解码为
- 后果:加完水印后,照片的拍摄时间、经纬度、光圈快门、原始旋转方向等关键信息全部丢失。
2. 逻辑链路:两种解决思路
思路 A:Java 原生"搬运"(高复杂度)
- 流程 :
- 使用
metadata-extractor读取原图 EXIF。 - 使用 Hutool 执行加水印逻辑。
- 使用
apache-commons-imaging手动将第一步获取的元数据注入到新图中。
- 使用
- 痛点:代码量大,且很难保证全量拷贝(如厂商私有数据 MakerNotes 容易丢失)。
思路 B:ExifTool 外部调用(推荐,高可靠)
- 流程 :
- 使用 Hutool 正常生成带水印的图片(无视 EXIF 丢失)。
- 通过 Java
Runtime.exec()调用外部exiftool执行克隆。
- 优势 :全量克隆。无论多复杂的元数据都能完美拷贝,一行命令解决。
3. 核心工具:ExifTool 命令详解
执行克隆的最简指令:
bash
exiftool -tagsFromFile source.jpg -overwrite_original target.jpg
-tagsFromFile source.jpg:指定元数据的"源文件"。-overwrite_original:关键参数 。直接修改目标文件。如果不加,ExifTool 会生成一个名为target.jpg_original的备份文件,在自动化脚本中会导致磁盘空间浪费。- 执行逻辑:它像"移植手术"一样,将源文件的 Header 部分剥离并注入到目标文件的头部,不破坏像素。
4. 代码集成实现方案
在 WatermarkHandler 中集成 ExifTool 的标准化写法:
java
public static ProcessResult processImage(String imagePath, String outputPath, List<String> watermarks) {
// 1. [像素处理层]:利用 Hutool 完成水印绘制
// Img.from(file).pressText(...).write(outputFile);
// 2. [元数据补丁层]:在文件生成后,立即调用系统命令恢复 EXIF
restoreExif(imagePath, outputPath);
return new ProcessResult(outputPath, watermarks);
}
private static void restoreExif(String sourcePath, String targetPath) {
try {
// 构建命令数组(避免空格引起的路径解析问题)
String[] cmd = {
"exiftool",
"-tagsFromFile", sourcePath,
"-overwrite_original", targetPath
};
Process process = Runtime.getRuntime().exec(cmd);
// 阻塞等待,确保后续业务拿到的图片已经是包含 EXIF 的成品
int exitCode = process.waitFor();
if (exitCode != 0) {
log.error("ExifTool 恢复信息失败,退出码: {}", exitCode);
}
} catch (Exception e) {
log.error("调用 ExifTool 异常", e);
}
}
5. 易错点与注意事项(颗粒度增强)
- 环境依赖 :
- Windows : 需要下载
exiftool.exe并将其所在目录加入系统的Path环境变量。 - Linux : 服务器需安装工具包(如
yum install exiftool),确保在终端输入exiftool有响应。
- Windows : 需要下载
- 旋转陷阱 (Orientation) :
- 如果原图带有
Orientation标签(手机竖拍),Hutool 处理后可能导致画面被强行"拍扁"或翻转。 - 优化建议 :在处理前通过
ImgUtil.getOrientation(file)预判角度,先旋转图片再打水印。
- 如果原图带有
- 性能考量 :
Runtime.exec会创建系统进程,在高并发(如每秒处理数百张图)场景下,建议考虑使用队列或通过共享进程的方式优化。