This commit is contained in:
litianxiang
2026-06-01 13:30:24 +08:00
parent db75948bd7
commit 3ddf4051e3
4 changed files with 214 additions and 1 deletions

View File

@@ -0,0 +1,156 @@
package com.aida.seller.util;
import com.aida.seller.common.config.WatermarkProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
@RequiredArgsConstructor
public class ImageWatermarkUtil {
private final WatermarkProperties watermarkProperties;
private final MinioUtil minioUtil;
private static final String WATERMARK_CACHE_PREFIX = "watermark:";
private static final int PRESIGNED_URL_EXPIRE_SECONDS = 7 * 24 * 60 * 60;
/**
* 全局水印结果缓存key = 原图 logical path hashvalue = 带水印图片的 logical path
*/
private final Map<String, String> watermarkCache = new ConcurrentHashMap<>();
/**
* 对指定 MinIO 资源添加平铺文字水印,返回带水印图片的 presigned URL。
* 相同原图结果会被缓存 30 天。
*/
public String applyWatermark(String minioResource) {
try {
String logicalPath = minioUtil.isPresignedUrl(minioResource)
? minioUtil.getLogicalPathFromPresignedUrl(minioResource)
: minioResource.trim();
String cacheKey = String.valueOf(logicalPath.hashCode());
String cachedPath = watermarkCache.get(cacheKey);
if (cachedPath != null) {
return minioUtil.getImageUrl(cachedPath, PRESIGNED_URL_EXPIRE_SECONDS);
}
try (InputStream originalStream = minioUtil.downloadFile(logicalPath);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] watermarkedBytes = addTextWatermark(originalStream,
watermarkProperties.getText(),
detectContentType(logicalPath));
String newPath = uploadWatermarkedImage(watermarkedBytes, logicalPath);
watermarkCache.put(cacheKey, newPath);
return minioUtil.getImageUrl(newPath, PRESIGNED_URL_EXPIRE_SECONDS);
}
} catch (Exception e) {
log.error("添加水印失败 resource={}, error={}", minioResource, e.getMessage(), e);
return minioResource;
}
}
/**
* 向图片流添加平铺文字水印,返回处理后的字节数组。
*/
public byte[] addTextWatermark(InputStream imageStream, String text, String contentType) throws Exception {
BufferedImage original = ImageIO.read(imageStream);
if (original == null) {
throw new IllegalArgumentException("无法读取图片格式");
}
int width = original.getWidth();
int height = original.getHeight();
BufferedImage output = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = output.createGraphics();
try {
g2d.drawImage(original, 0, 0, width, height, null);
int baseFontSize = Math.max(12, (int) (Math.min(width, height) * watermarkProperties.getFontSizeRatio()));
Font font = new Font("Arial", Font.BOLD, baseFontSize);
g2d.setFont(font);
int[] rgba = watermarkProperties.getColorComponents();
g2d.setColor(new Color(rgba[0], rgba[1], rgba[2], rgba.length > 3 ? rgba[3] : 128));
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
double radians = Math.toRadians(watermarkProperties.getRotationDegrees());
int stepX = (int) (baseFontSize * watermarkProperties.getSpacingRatio());
int stepY = (int) (baseFontSize * watermarkProperties.getSpacingRatio());
AffineTransform rotateTransform = new AffineTransform();
rotateTransform.translate(width / 2.0, height / 2.0);
rotateTransform.rotate(radians);
rotateTransform.translate(-width / 2.0, -height / 2.0);
g2d.setTransform(rotateTransform);
for (int y = -height; y < height * 2; y += stepY) {
for (int x = -width; x < width * 2; x += stepX) {
g2d.drawString(text, x, y);
}
}
} finally {
g2d.dispose();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
String formatName = contentType != null && contentType.contains("png") ? "PNG" : "JPEG";
ImageIO.write(output, formatName, baos);
return baos.toByteArray();
}
private String uploadWatermarkedImage(byte[] watermarkedBytes, String originalPath) {
int lastSlash = originalPath.lastIndexOf('/');
String basePath = lastSlash > 0 ? originalPath.substring(0, lastSlash) : "";
String fileName = lastSlash >= 0 ? originalPath.substring(lastSlash + 1) : originalPath;
String nameWithoutExt = fileName.contains(".")
? fileName.substring(0, fileName.lastIndexOf('.'))
: fileName;
String ext = fileName.contains(".") ? fileName.substring(fileName.lastIndexOf('.')) : ".jpg";
String watermarkedPath = (basePath.isEmpty() ? "" : basePath + "/") + "watermarked/" + nameWithoutExt + "_wm" + ext;
String contentType = ext.contains("png") ? "image/png" : "image/jpeg";
return minioUtil.uploadBytes(watermarkedBytes, watermarkedPath, contentType,
watermarkProperties.getBucketName() != null ? watermarkProperties.getBucketName() : "aida-user");
}
private String detectContentType(String path) {
String lower = path.toLowerCase();
if (lower.endsWith(".png")) return "image/png";
if (lower.endsWith(".gif")) return "image/gif";
if (lower.endsWith(".webp")) return "image/webp";
return "image/jpeg";
}
/**
* 清理指定原图的水印缓存(供外部在原图更新时调用)
*/
public void evictCache(String minioResource) {
try {
String logicalPath = minioUtil.isPresignedUrl(minioResource)
? minioUtil.getLogicalPathFromPresignedUrl(minioResource)
: minioResource.trim();
watermarkCache.remove(String.valueOf(logicalPath.hashCode()));
} catch (Exception e) {
log.warn("清理水印缓存失败 resource={}", minioResource, e);
}
}
}