水印
This commit is contained in:
156
src/main/java/com/aida/seller/util/ImageWatermarkUtil.java
Normal file
156
src/main/java/com/aida/seller/util/ImageWatermarkUtil.java
Normal 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 hash,value = 带水印图片的 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user