You've already forked FrameTour-BE
feat(watermark): 添加边缘端水印处理功能
- 引入 WatermarkEdgeService 支持边缘端渲染 - 在 WatermarkConfig 中添加边缘端相关配置参数 - 在 WatermarkStage 中实现边缘端处理逻辑和降级机制 - 修改 ImageWatermarkOperatorEnum 的默认输出格式为 jpg - 移除已废弃的 DefaultImageWatermarkOperator 类 - 更新 GoodsServiceImpl 使用边缘端处理水印 - 优化 PuzzleEdgeWorkerIpInterceptor 允许本地回环地址访问 - 修正 PrinterDefaultWatermarkTemplateBuilder 样式常量名称
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package com.ycwl.basic.image.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.image.watermark.edge.WatermarkEdgeService;
|
||||
import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum;
|
||||
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@@ -41,4 +43,28 @@ public class WatermarkConfig {
|
||||
*/
|
||||
@Builder.Default
|
||||
private final Double scale = 1.0;
|
||||
|
||||
/**
|
||||
* 边缘端水印服务(可选)
|
||||
* 如果设置,将优先尝试使用边缘端处理
|
||||
*/
|
||||
private final WatermarkEdgeService edgeService;
|
||||
|
||||
/**
|
||||
* 存储适配器(边缘端处理时需要)
|
||||
* 用于上传原图和二维码到临时位置
|
||||
*/
|
||||
private final IStorageAdapter storageAdapter;
|
||||
|
||||
/**
|
||||
* 是否启用边缘端处理
|
||||
*/
|
||||
@Builder.Default
|
||||
private final boolean edgeEnabled = false;
|
||||
|
||||
/**
|
||||
* 边缘端处理超时时间(毫秒)
|
||||
*/
|
||||
@Builder.Default
|
||||
private final long edgeTimeoutMs = 30000L;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.ycwl.basic.image.pipeline.stages;
|
||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||
import com.ycwl.basic.image.watermark.ImageWatermarkFactory;
|
||||
import com.ycwl.basic.image.watermark.edge.WatermarkEdgeService;
|
||||
import com.ycwl.basic.image.watermark.entity.WatermarkInfo;
|
||||
import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum;
|
||||
import com.ycwl.basic.image.watermark.operator.IOperator;
|
||||
@@ -10,6 +11,7 @@ import com.ycwl.basic.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@@ -21,6 +23,7 @@ import java.util.List;
|
||||
/**
|
||||
* 水印处理Stage
|
||||
* 支持三级降级: 配置的水印类型 -> PRINTER_DEFAULT -> 无水印
|
||||
* 支持边缘端渲染(可选)
|
||||
*/
|
||||
@Slf4j
|
||||
@StageConfig(
|
||||
@@ -127,6 +130,19 @@ public class WatermarkStage extends AbstractPipelineStage<PhotoProcessContext> {
|
||||
File watermarkedFile = context.getTempFileManager()
|
||||
.createTempFile("watermark_" + type.getType(), "." + fileExt);
|
||||
|
||||
// 尝试边缘端处理
|
||||
if (shouldUseEdgeProcessing(type)) {
|
||||
File edgeResult = tryEdgeProcessing(context, type, currentFile, watermarkedFile);
|
||||
if (edgeResult != null && edgeResult.exists()) {
|
||||
context.updateProcessedFile(edgeResult);
|
||||
log.info("边缘端水印应用成功: type={}, size={}KB", type.getType(), edgeResult.length() / 1024);
|
||||
return StageResult.success(String.format("水印(边缘端): %s (%dKB)",
|
||||
type.getType(), edgeResult.length() / 1024));
|
||||
}
|
||||
log.warn("边缘端水印处理失败,降级到本地处理: type={}", type.getType());
|
||||
}
|
||||
|
||||
// 本地处理(降级或直接使用)
|
||||
WatermarkInfo watermarkInfo = buildWatermarkInfo(context, currentFile, watermarkedFile, type);
|
||||
|
||||
IOperator operator = ImageWatermarkFactory.get(type);
|
||||
@@ -143,6 +159,46 @@ public class WatermarkStage extends AbstractPipelineStage<PhotoProcessContext> {
|
||||
type.getType(), result.length() / 1024));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否应使用边缘端处理
|
||||
*/
|
||||
private boolean shouldUseEdgeProcessing(ImageWatermarkOperatorEnum type) {
|
||||
if (!config.isEdgeEnabled()) {
|
||||
return false;
|
||||
}
|
||||
WatermarkEdgeService edgeService = config.getEdgeService();
|
||||
if (edgeService == null) {
|
||||
return false;
|
||||
}
|
||||
IStorageAdapter storageAdapter = config.getStorageAdapter();
|
||||
return storageAdapter != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试使用边缘端处理
|
||||
*
|
||||
* @return 处理后的文件,失败返回 null
|
||||
*/
|
||||
private File tryEdgeProcessing(PhotoProcessContext context,
|
||||
ImageWatermarkOperatorEnum type,
|
||||
File currentFile,
|
||||
File watermarkedFile) {
|
||||
try {
|
||||
WatermarkEdgeService edgeService = config.getEdgeService();
|
||||
IStorageAdapter storageAdapter = config.getStorageAdapter();
|
||||
|
||||
// 构建水印信息用于边缘端处理
|
||||
WatermarkInfo info = buildWatermarkInfo(context, currentFile, watermarkedFile, type);
|
||||
|
||||
// 调用边缘端服务处理
|
||||
return edgeService.processWatermarkFromFile(info, type, storageAdapter);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("边缘端水印处理异常: type={}, error={}", type.getType(), e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建水印参数
|
||||
*/
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.ycwl.basic.image.watermark;
|
||||
import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum;
|
||||
import com.ycwl.basic.image.watermark.exception.ImageWatermarkUnsupportedException;
|
||||
import com.ycwl.basic.image.watermark.operator.IOperator;
|
||||
import com.ycwl.basic.image.watermark.operator.DefaultImageWatermarkOperator;
|
||||
import com.ycwl.basic.image.watermark.operator.LeicaWatermarkOperator;
|
||||
import com.ycwl.basic.image.watermark.operator.NormalWatermarkOperator;
|
||||
import com.ycwl.basic.image.watermark.operator.PrinterDefaultWatermarkOperator;
|
||||
@@ -18,11 +17,9 @@ public class ImageWatermarkFactory {
|
||||
}
|
||||
public static IOperator get(ImageWatermarkOperatorEnum type) {
|
||||
return switch (type) {
|
||||
case WATERMARK -> new DefaultImageWatermarkOperator();
|
||||
case NORMAL -> new NormalWatermarkOperator();
|
||||
case LEICA -> new LeicaWatermarkOperator();
|
||||
case PRINTER_DEFAULT -> new PrinterDefaultWatermarkOperator();
|
||||
default -> throw new ImageWatermarkUnsupportedException("不支持的类型" + type.name());
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import java.util.Map;
|
||||
@Component
|
||||
public class PrinterDefaultWatermarkTemplateBuilder extends AbstractWatermarkTemplateBuilder {
|
||||
|
||||
public static final String STYLE = "printer_default";
|
||||
public static final String STYLE = "pDefault";
|
||||
|
||||
// 常量配置(与 PrinterDefaultWatermarkOperator 保持一致)
|
||||
private static final int OFFSET_Y = 15;
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
package com.ycwl.basic.image.watermark.edge;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.ycwl.basic.constant.StorageConstant;
|
||||
import com.ycwl.basic.image.watermark.entity.WatermarkInfo;
|
||||
import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum;
|
||||
import com.ycwl.basic.puzzle.edge.task.PuzzleEdgeRenderTaskService;
|
||||
import com.ycwl.basic.storage.StorageFactory;
|
||||
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||
import com.ycwl.basic.storage.enums.StorageAcl;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 水印边缘端处理服务
|
||||
* 将原有的 IOperator 本地处理迁移到边缘端渲染
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WatermarkEdgeService {
|
||||
|
||||
private final WatermarkEdgeTaskCreator watermarkEdgeTaskCreator;
|
||||
|
||||
/**
|
||||
* 默认等待超时时间(毫秒)
|
||||
*/
|
||||
private static final long DEFAULT_TIMEOUT_MS = 30_000L;
|
||||
|
||||
/**
|
||||
* 使用边缘端处理水印(适用于 GoodsServiceImpl 场景)
|
||||
* 直接传入 URL,不需要本地文件
|
||||
*
|
||||
* @param type 水印类型
|
||||
* @param originalUrl 原图URL
|
||||
* @param qrcodeUrl 二维码URL
|
||||
* @param faceUrl 头像URL(可选)
|
||||
* @param scenicLine 景区名称
|
||||
* @param datetime 日期时间
|
||||
* @param dtFormat 日期格式
|
||||
* @param sourceId 关联的sourceId(用于记录追踪)
|
||||
* @param faceId 人脸ID(可选)
|
||||
* @return 带水印的图片URL,处理失败返回null
|
||||
*/
|
||||
public String processWatermark(ImageWatermarkOperatorEnum type,
|
||||
String originalUrl,
|
||||
String qrcodeUrl,
|
||||
String faceUrl,
|
||||
String scenicLine,
|
||||
Date datetime,
|
||||
String dtFormat,
|
||||
Long sourceId,
|
||||
Long faceId) {
|
||||
return processWatermark(type, originalUrl, qrcodeUrl, faceUrl, scenicLine, datetime, dtFormat,
|
||||
sourceId, faceId, null, null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用边缘端处理水印(完整参数版本)
|
||||
*
|
||||
* @param type 水印类型
|
||||
* @param originalUrl 原图URL
|
||||
* @param qrcodeUrl 二维码URL
|
||||
* @param faceUrl 头像URL(可选)
|
||||
* @param scenicLine 景区名称
|
||||
* @param datetime 日期时间
|
||||
* @param dtFormat 日期格式
|
||||
* @param sourceId 关联的sourceId(用于记录追踪)
|
||||
* @param faceId 人脸ID(可选)
|
||||
* @param scale 缩放倍数(可选)
|
||||
* @param offsetLeft 左偏移(可选)
|
||||
* @param offsetRight 右偏移(可选)
|
||||
* @param offsetTop 上偏移(可选)
|
||||
* @param offsetBottom 下偏移(可选)
|
||||
* @return 带水印的图片URL,处理失败返回null
|
||||
*/
|
||||
public String processWatermark(ImageWatermarkOperatorEnum type,
|
||||
String originalUrl,
|
||||
String qrcodeUrl,
|
||||
String faceUrl,
|
||||
String scenicLine,
|
||||
Date datetime,
|
||||
String dtFormat,
|
||||
Long sourceId,
|
||||
Long faceId,
|
||||
Double scale,
|
||||
Integer offsetLeft,
|
||||
Integer offsetRight,
|
||||
Integer offsetTop,
|
||||
Integer offsetBottom) {
|
||||
// 将 ImageWatermarkOperatorEnum 映射到边缘端风格
|
||||
String style = mapTypeToStyle(type);
|
||||
|
||||
// 检查边缘端是否支持该风格
|
||||
if (!watermarkEdgeTaskCreator.isStyleSupported(style)) {
|
||||
log.warn("边缘端不支持水印风格: {}", style);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取图片尺寸
|
||||
int[] dimensions = getImageDimensions(originalUrl);
|
||||
if (dimensions == null) {
|
||||
log.error("无法获取图片尺寸: {}", originalUrl);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建日期时间行
|
||||
String datetimeLine = datetime != null && dtFormat != null
|
||||
? DateUtil.format(datetime, dtFormat)
|
||||
: null;
|
||||
|
||||
// 构建水印请求
|
||||
WatermarkRequest request = WatermarkRequest.builder()
|
||||
.originalImageUrl(originalUrl)
|
||||
.imageWidth(dimensions[0])
|
||||
.imageHeight(dimensions[1])
|
||||
.qrcodeUrl(qrcodeUrl)
|
||||
.faceUrl(faceUrl)
|
||||
.scenicLine(scenicLine)
|
||||
.datetimeLine(datetimeLine)
|
||||
.scale(scale)
|
||||
.offsetLeft(offsetLeft)
|
||||
.offsetRight(offsetRight)
|
||||
.offsetTop(offsetTop)
|
||||
.offsetBottom(offsetBottom)
|
||||
.outputFormat(type.getPreferFileType().equalsIgnoreCase("png") ? "PNG" : "JPEG")
|
||||
.outputQuality(90)
|
||||
.build();
|
||||
|
||||
// 创建边缘任务并等待结果
|
||||
PuzzleEdgeRenderTaskService.TaskWaitResult result = watermarkEdgeTaskCreator.createAndWait(
|
||||
style,
|
||||
request,
|
||||
sourceId, // recordId
|
||||
faceId,
|
||||
type.getType(), // watermarkType
|
||||
DEFAULT_TIMEOUT_MS
|
||||
);
|
||||
|
||||
if (result.isSuccess()) {
|
||||
log.info("边缘端水印处理成功: sourceId={}, type={}, url={}", sourceId, type, result.getImageUrl());
|
||||
return result.getImageUrl();
|
||||
} else {
|
||||
log.error("边缘端水印处理失败: sourceId={}, type={}, error={}", sourceId, type, result.getErrorMessage());
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("边缘端水印处理异常: sourceId={}, type={}", sourceId, type, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用边缘端处理水印(适用于 WatermarkStage / Pipeline 场景)
|
||||
* 从本地文件处理,需要先上传原图和二维码
|
||||
*
|
||||
* @param info 水印信息(包含本地文件)
|
||||
* @param type 水印类型
|
||||
* @param adapter 存储适配器
|
||||
* @return 处理后的本地文件,失败返回null
|
||||
*/
|
||||
public File processWatermarkFromFile(WatermarkInfo info,
|
||||
ImageWatermarkOperatorEnum type,
|
||||
IStorageAdapter adapter) {
|
||||
// 将 ImageWatermarkOperatorEnum 映射到边缘端风格
|
||||
String style = mapTypeToStyle(type);
|
||||
|
||||
// 检查边缘端是否支持该风格
|
||||
if (!watermarkEdgeTaskCreator.isStyleSupported(style)) {
|
||||
log.warn("边缘端不支持水印风格: {}", style);
|
||||
return null;
|
||||
}
|
||||
|
||||
String uploadedOriginalUrl = null;
|
||||
String uploadedQrcodeUrl = null;
|
||||
String uploadedFaceUrl = null;
|
||||
|
||||
try {
|
||||
// 1. 获取图片尺寸
|
||||
BufferedImage originalImage = ImageIO.read(info.getOriginalFile());
|
||||
if (originalImage == null) {
|
||||
log.error("无法读取原图文件: {}", info.getOriginalFile());
|
||||
return null;
|
||||
}
|
||||
int imageWidth = originalImage.getWidth();
|
||||
int imageHeight = originalImage.getHeight();
|
||||
originalImage.flush();
|
||||
|
||||
// 2. 上传原图到临时位置
|
||||
String originalFileName = "temp_watermark_" + UUID.randomUUID() + ".jpg";
|
||||
uploadedOriginalUrl = adapter.uploadFile(null, info.getOriginalFile(),
|
||||
StorageConstant.PHOTO_WATERMARKED_PATH + "/temp", originalFileName);
|
||||
adapter.setAcl(StorageAcl.PUBLIC_READ, StorageConstant.PHOTO_WATERMARKED_PATH + "/temp", originalFileName);
|
||||
|
||||
// 3. 上传二维码(如果有)
|
||||
if (info.getQrcodeFile() != null && info.getQrcodeFile().exists()) {
|
||||
String qrcodeFileName = "temp_qrcode_" + UUID.randomUUID() + ".jpg";
|
||||
uploadedQrcodeUrl = adapter.uploadFile(null, info.getQrcodeFile(),
|
||||
StorageConstant.PHOTO_WATERMARKED_PATH + "/temp", qrcodeFileName);
|
||||
adapter.setAcl(StorageAcl.PUBLIC_READ, StorageConstant.PHOTO_WATERMARKED_PATH + "/temp", qrcodeFileName);
|
||||
}
|
||||
|
||||
// 4. 上传头像(如果有)
|
||||
if (info.getFaceFile() != null && info.getFaceFile().exists()) {
|
||||
String faceFileName = "temp_face_" + UUID.randomUUID() + ".jpg";
|
||||
uploadedFaceUrl = adapter.uploadFile(null, info.getFaceFile(),
|
||||
StorageConstant.PHOTO_WATERMARKED_PATH + "/temp", faceFileName);
|
||||
adapter.setAcl(StorageAcl.PUBLIC_READ, StorageConstant.PHOTO_WATERMARKED_PATH + "/temp", faceFileName);
|
||||
}
|
||||
|
||||
// 5. 构建水印请求
|
||||
WatermarkRequest request = WatermarkRequest.builder()
|
||||
.originalImageUrl(uploadedOriginalUrl)
|
||||
.imageWidth(imageWidth)
|
||||
.imageHeight(imageHeight)
|
||||
.qrcodeUrl(uploadedQrcodeUrl)
|
||||
.faceUrl(uploadedFaceUrl)
|
||||
.scenicLine(info.getScenicLine())
|
||||
.datetimeLine(info.getDatetimeLine())
|
||||
.scale(info.getScale())
|
||||
.offsetLeft(info.getOffsetLeft())
|
||||
.offsetRight(info.getOffsetRight())
|
||||
.offsetTop(info.getOffsetTop())
|
||||
.offsetBottom(info.getOffsetBottom())
|
||||
.outputFormat(type.getPreferFileType().equalsIgnoreCase("png") ? "PNG" : "JPEG")
|
||||
.outputQuality(90)
|
||||
.build();
|
||||
|
||||
// 6. 创建边缘任务并等待结果
|
||||
PuzzleEdgeRenderTaskService.TaskWaitResult result = watermarkEdgeTaskCreator.createAndWait(
|
||||
style,
|
||||
request,
|
||||
null, // recordId
|
||||
null, // faceId
|
||||
type.getType(), // watermarkType
|
||||
DEFAULT_TIMEOUT_MS
|
||||
);
|
||||
|
||||
if (!result.isSuccess()) {
|
||||
log.error("边缘端水印处理失败: error={}", result.getErrorMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
// 7. 下载结果到目标文件
|
||||
String resultUrl = result.getImageUrl();
|
||||
File outputFile = info.getWatermarkedFile();
|
||||
downloadFile(resultUrl, outputFile);
|
||||
|
||||
log.info("边缘端水印处理成功: type={}, outputFile={}", type, outputFile);
|
||||
return outputFile;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("边缘端水印处理异常: type={}", type, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 ImageWatermarkOperatorEnum 映射到边缘端风格
|
||||
*/
|
||||
private String mapTypeToStyle(ImageWatermarkOperatorEnum type) {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
return switch (type) {
|
||||
case NORMAL -> NormalWatermarkTemplateBuilder.STYLE;
|
||||
case LEICA -> LeicaWatermarkTemplateBuilder.STYLE;
|
||||
case PRINTER_DEFAULT -> PrinterDefaultWatermarkTemplateBuilder.STYLE;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片尺寸
|
||||
*
|
||||
* @param imageUrl 图片URL
|
||||
* @return [width, height],失败返回null
|
||||
*/
|
||||
private int[] getImageDimensions(String imageUrl) {
|
||||
try {
|
||||
// 替换内网域名
|
||||
String url = imageUrl.replace("oss.zhentuai.com",
|
||||
"frametour-assets.oss-cn-shanghai-internal.aliyuncs.com");
|
||||
BufferedImage image = ImageIO.read(new URL(url));
|
||||
if (image == null) {
|
||||
return null;
|
||||
}
|
||||
int[] dimensions = new int[]{image.getWidth(), image.getHeight()};
|
||||
image.flush();
|
||||
return dimensions;
|
||||
} catch (IOException e) {
|
||||
log.error("获取图片尺寸失败: {}", imageUrl, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*/
|
||||
private void downloadFile(String url, File targetFile) throws IOException {
|
||||
// 替换内网域名
|
||||
String downloadUrl = url.replace("oss.zhentuai.com",
|
||||
"frametour-assets.oss-cn-shanghai-internal.aliyuncs.com");
|
||||
HttpUtil.downloadFile(downloadUrl, targetFile);
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,9 @@ import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public enum ImageWatermarkOperatorEnum {
|
||||
WATERMARK("defW", "jpg"),
|
||||
LEICA("leica", "png"),
|
||||
NORMAL("normal", "png"),
|
||||
PRINTER_DEFAULT("pDefault", "png");
|
||||
LEICA("leica", "jpg"),
|
||||
NORMAL("normal", "jpg"),
|
||||
PRINTER_DEFAULT("pDefault", "jpg");
|
||||
|
||||
private final String type;
|
||||
private final String preferFileType;
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
package com.ycwl.basic.image.watermark.operator;
|
||||
|
||||
import com.ycwl.basic.image.watermark.entity.WatermarkInfo;
|
||||
import com.ycwl.basic.image.watermark.exception.ImageWatermarkException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageWriteParam;
|
||||
import javax.imageio.ImageWriter;
|
||||
import javax.imageio.stream.ImageOutputStream;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
@Slf4j
|
||||
public class DefaultImageWatermarkOperator implements IOperator {
|
||||
@Override
|
||||
public File process(WatermarkInfo info) throws ImageWatermarkException {
|
||||
BufferedImage baseImage;
|
||||
BufferedImage watermarkImage;
|
||||
InputStream logoInputStream = getClass().getResourceAsStream("/watermark.png");
|
||||
if (logoInputStream == null) {
|
||||
throw new ImageWatermarkException("无法找到 watermark.png 资源文件");
|
||||
}
|
||||
try {
|
||||
baseImage = ImageIO.read(info.getOriginalFile());
|
||||
watermarkImage = ImageIO.read(logoInputStream);
|
||||
} catch (IOException e) {
|
||||
throw new ImageWatermarkException("图片打开失败");
|
||||
}
|
||||
// 新图像画布
|
||||
BufferedImage newImage = new BufferedImage(baseImage.getWidth(), baseImage.getHeight(), BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = newImage.createGraphics();
|
||||
g2d.drawImage(baseImage, 0, 0, null);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.5f));
|
||||
g2d.drawImage(watermarkImage, 0, 0, baseImage.getWidth(), baseImage.getHeight(), null);
|
||||
String fileName = info.getWatermarkedFile().getName();
|
||||
String formatName = "jpg"; // 默认格式为 jpg
|
||||
if (fileName.endsWith(".png")) {
|
||||
formatName = "png";
|
||||
} else if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) {
|
||||
formatName = "jpg";
|
||||
}
|
||||
ImageWriter writer = ImageIO.getImageWritersByFormatName(formatName).next();
|
||||
ImageOutputStream ios;
|
||||
try {
|
||||
ios = ImageIO.createImageOutputStream(info.getWatermarkedFile());
|
||||
} catch (IOException e) {
|
||||
throw new ImageWatermarkException("图片保存失败,目标文件无法写入");
|
||||
}
|
||||
writer.setOutput(ios);
|
||||
try {
|
||||
// 使用 ImageWriter 设置写入质量
|
||||
ImageWriteParam writeParam = writer.getDefaultWriteParam();
|
||||
if (writeParam.canWriteCompressed()) {
|
||||
writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
|
||||
writeParam.setCompressionQuality(0.8f); // 设置写入质量为 80%
|
||||
}
|
||||
writer.write(null, new javax.imageio.IIOImage(newImage, null, null), writeParam);
|
||||
} catch (IOException e) {
|
||||
throw new ImageWatermarkException("图片保存失败");
|
||||
}
|
||||
finally {
|
||||
g2d.dispose();
|
||||
try {
|
||||
ios.close();
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
writer.dispose();
|
||||
}
|
||||
return info.getWatermarkedFile();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user