You've already forked FrameTour-BE
feat(image): 启用边缘端水印处理并优化徕卡模板布局算法
- 将边缘端处理默认启用状态从false改为true - 将边缘端处理超时时间从30秒调整为10秒 - 将徕卡水印模板的固定像素配置转换为基于1920x1080的百分比配置 - 新增多种百分比常量包括底部区域、Logo大小、边距、字体大小等 - 实现动态计算实际像素值的方法替代固定数值 - 在PrinterServiceImpl中注入WatermarkEdgeService依赖 - 配置水印处理流程启用边缘服务和存储适配器
This commit is contained in:
@@ -60,11 +60,11 @@ public class WatermarkConfig {
|
||||
* 是否启用边缘端处理
|
||||
*/
|
||||
@Builder.Default
|
||||
private final boolean edgeEnabled = false;
|
||||
private final boolean edgeEnabled = true;
|
||||
|
||||
/**
|
||||
* 边缘端处理超时时间(毫秒)
|
||||
*/
|
||||
@Builder.Default
|
||||
private final long edgeTimeoutMs = 30000L;
|
||||
private final long edgeTimeoutMs = 10_000L;
|
||||
}
|
||||
|
||||
@@ -190,8 +190,8 @@ public class WatermarkStage extends AbstractPipelineStage<PhotoProcessContext> {
|
||||
// 构建水印信息用于边缘端处理
|
||||
WatermarkInfo info = buildWatermarkInfo(context, currentFile, watermarkedFile, type);
|
||||
|
||||
// 调用边缘端服务处理
|
||||
return edgeService.processWatermarkFromFile(info, type, storageAdapter);
|
||||
// 调用边缘端服务处理,传递 processId 作为 recordId
|
||||
return edgeService.processWatermarkFromFile(info, type, storageAdapter, context.getProcessId());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("边缘端水印处理异常: type={}, error={}", type.getType(), e.getMessage(), e);
|
||||
|
||||
@@ -11,7 +11,7 @@ import java.util.Map;
|
||||
* 徕卡风格水印模板构建器
|
||||
* 对应 LeicaWatermarkOperator
|
||||
*
|
||||
* 布局说明:
|
||||
* 布局说明(百分比基于1920x1080量化,精度0.5%):
|
||||
* - 画布大小 = 原图大小(不扩展)
|
||||
* - 原图收缩放在画布上半部分,底部留出空间
|
||||
* - 底部白色区域左侧:帧途 Logo + "帧途" 文字
|
||||
@@ -22,20 +22,32 @@ public class LeicaWatermarkTemplateBuilder extends AbstractWatermarkTemplateBuil
|
||||
|
||||
public static final String STYLE = "leica";
|
||||
|
||||
// 常量配置(与 LeicaWatermarkOperator 保持一致)
|
||||
private static final int EXTRA_BOTTOM_PX = 140;
|
||||
private static final int LOGO_SIZE = 50;
|
||||
private static final int LOGO_EXTRA_BORDER = 20;
|
||||
private static final int LOGO_FONT_SIZE = 38;
|
||||
// 百分比常量配置(基于1920x1080量化,精度0.5%)
|
||||
/** 底部额外区域占高度百分比 */
|
||||
private static final double EXTRA_BOTTOM_PERCENT = 0.13; // 13%
|
||||
/** Logo大小占高度百分比 */
|
||||
private static final double LOGO_SIZE_PERCENT = 0.045; // 4.5%
|
||||
/** Logo额外边距占高度百分比 */
|
||||
private static final double LOGO_EXTRA_BORDER_PERCENT = 0.02; // 2%
|
||||
/** Logo字体大小占高度百分比 */
|
||||
private static final double LOGO_FONT_SIZE_PERCENT = 0.035; // 3.5%
|
||||
/** 二维码大小占高度百分比 */
|
||||
private static final double QRCODE_SIZE_PERCENT = 0.11; // 11%
|
||||
/** 二维码X偏移占宽度百分比 */
|
||||
private static final double QRCODE_OFFSET_X_PERCENT = 0.005; // 0.5%
|
||||
/** 二维码Y偏移占高度百分比 */
|
||||
private static final double QRCODE_OFFSET_Y_PERCENT = 0.02; // 2%
|
||||
/** 左右边距占宽度百分比 */
|
||||
private static final double OFFSET_X_PERCENT = 0.04; // 4%
|
||||
/** 上下边距占高度百分比 */
|
||||
private static final double OFFSET_Y_PERCENT = 0.03; // 3%
|
||||
/** 景区名字体大小占高度百分比 */
|
||||
private static final double SCENIC_FONT_SIZE_PERCENT = 0.03; // 3%
|
||||
/** 日期时间字体大小占高度百分比 */
|
||||
private static final double DATETIME_FONT_SIZE_PERCENT = 0.025; // 2.5%
|
||||
|
||||
private static final String LOGO_TEXT_COLOR = "#333333";
|
||||
private static final int QRCODE_SIZE = 120;
|
||||
private static final int QRCODE_OFFSET_X = 5;
|
||||
private static final int QRCODE_OFFSET_Y = 20;
|
||||
private static final int OFFSET_X = 80;
|
||||
private static final int OFFSET_Y = 30;
|
||||
private static final int SCENIC_FONT_SIZE = 32;
|
||||
private static final String SCENIC_COLOR = "#333333";
|
||||
private static final int DATETIME_FONT_SIZE = 28;
|
||||
private static final String DATETIME_COLOR = "#999999";
|
||||
|
||||
/**
|
||||
@@ -53,12 +65,25 @@ public class LeicaWatermarkTemplateBuilder extends AbstractWatermarkTemplateBuil
|
||||
int imageWidth = request.getImageWidth();
|
||||
int imageHeight = request.getImageHeight();
|
||||
|
||||
// 根据百分比计算实际像素值
|
||||
int extraBottom = (int) (imageHeight * EXTRA_BOTTOM_PERCENT);
|
||||
int logoSize = (int) (imageHeight * LOGO_SIZE_PERCENT);
|
||||
int logoExtraBorder = (int) (imageHeight * LOGO_EXTRA_BORDER_PERCENT);
|
||||
int logoFontSize = (int) (imageHeight * LOGO_FONT_SIZE_PERCENT);
|
||||
int qrcodeSize = (int) (imageHeight * QRCODE_SIZE_PERCENT);
|
||||
int qrcodeOffsetX = (int) (imageWidth * QRCODE_OFFSET_X_PERCENT);
|
||||
int qrcodeOffsetY = (int) (imageHeight * QRCODE_OFFSET_Y_PERCENT);
|
||||
int offsetX = (int) (imageWidth * OFFSET_X_PERCENT);
|
||||
int offsetY = (int) (imageHeight * OFFSET_Y_PERCENT);
|
||||
int scenicFontSize = (int) (imageHeight * SCENIC_FONT_SIZE_PERCENT);
|
||||
int datetimeFontSize = (int) (imageHeight * DATETIME_FONT_SIZE_PERCENT);
|
||||
|
||||
// 画布大小 = 原图大小(不扩展)
|
||||
int canvasWidth = imageWidth;
|
||||
int canvasHeight = imageHeight;
|
||||
|
||||
// 原图收缩后的区域高度
|
||||
int shrunkImageHeight = imageHeight - EXTRA_BOTTOM_PX;
|
||||
int shrunkImageHeight = imageHeight - extraBottom;
|
||||
// 底部区域起始 Y 坐标
|
||||
int bottomAreaY = shrunkImageHeight;
|
||||
|
||||
@@ -84,47 +109,44 @@ public class LeicaWatermarkTemplateBuilder extends AbstractWatermarkTemplateBuil
|
||||
dynamicData.put("originalImage", request.getOriginalImageUrl());
|
||||
|
||||
// 2. Logo 元素(底部左侧)
|
||||
int logoY = bottomAreaY + OFFSET_Y + LOGO_EXTRA_BORDER;
|
||||
int logoY = bottomAreaY + offsetY + logoExtraBorder;
|
||||
PuzzleElementEntity logoElement = createImageElement(
|
||||
"logo", "Logo",
|
||||
OFFSET_X, logoY - 12,
|
||||
LOGO_SIZE, LOGO_SIZE, 10,
|
||||
offsetX, logoY - (int)(logoSize * 0.24),
|
||||
logoSize, logoSize, 10,
|
||||
FIT_MODE_CONTAIN, null, null
|
||||
);
|
||||
elements.add(logoElement);
|
||||
dynamicData.put("logo", LOGO_URL);
|
||||
|
||||
// 3. "帧途" 文字(Logo 右边)
|
||||
int logoTextX = OFFSET_X + LOGO_SIZE + 5;
|
||||
int logoTextY = bottomAreaY + OFFSET_Y + LOGO_EXTRA_BORDER;
|
||||
int logoTextX = offsetX + logoSize + (int)(imageWidth * 0.005);
|
||||
int logoTextY = bottomAreaY + offsetY + logoExtraBorder;
|
||||
PuzzleElementEntity logoTextElement = createTextElement(
|
||||
"logoText", "帧途文字",
|
||||
logoTextX, logoTextY,
|
||||
100, LOGO_SIZE, 10,
|
||||
"PingFang SC", LOGO_FONT_SIZE, LOGO_TEXT_COLOR,
|
||||
(int)(imageWidth * 0.05), logoSize, 10,
|
||||
"PingFang SC", logoFontSize, LOGO_TEXT_COLOR,
|
||||
"NORMAL", TEXT_ALIGN_LEFT
|
||||
);
|
||||
elements.add(logoTextElement);
|
||||
dynamicData.put("logoText", "帧途");
|
||||
|
||||
// 4. 计算右侧区域位置
|
||||
int qrcodeWidth = QRCODE_SIZE;
|
||||
int qrcodeHeight = QRCODE_SIZE;
|
||||
|
||||
// 估算文字宽度(使用景区名和日期的较大者)
|
||||
int estimatedTextWidth = Math.max(
|
||||
(request.getScenicLine() != null ? request.getScenicLine().length() : 0) * SCENIC_FONT_SIZE / 2,
|
||||
(request.getDatetimeLine() != null ? request.getDatetimeLine().length() : 0) * DATETIME_FONT_SIZE / 2
|
||||
(request.getScenicLine() != null ? request.getScenicLine().length() : 0) * scenicFontSize / 2,
|
||||
(request.getDatetimeLine() != null ? request.getDatetimeLine().length() : 0) * datetimeFontSize / 2
|
||||
);
|
||||
|
||||
int qrcodeX = canvasWidth - OFFSET_X - qrcodeWidth - QRCODE_OFFSET_X - estimatedTextWidth;
|
||||
int qrcodeY = bottomAreaY + OFFSET_Y - QRCODE_OFFSET_Y;
|
||||
int qrcodeX = canvasWidth - offsetX - qrcodeSize - qrcodeOffsetX - estimatedTextWidth;
|
||||
int qrcodeY = bottomAreaY + offsetY - qrcodeOffsetY;
|
||||
|
||||
// 5. 二维码元素
|
||||
PuzzleElementEntity qrcodeElement = createImageElement(
|
||||
"qrcode", "二维码",
|
||||
qrcodeX, qrcodeY,
|
||||
qrcodeWidth, qrcodeHeight, 10,
|
||||
qrcodeSize, qrcodeSize, 10,
|
||||
FIT_MODE_CONTAIN, null, null
|
||||
);
|
||||
elements.add(qrcodeElement);
|
||||
@@ -132,9 +154,9 @@ public class LeicaWatermarkTemplateBuilder extends AbstractWatermarkTemplateBuil
|
||||
|
||||
// 6. 头像元素(二维码中央,可选)
|
||||
if (request.getFaceUrl() != null && !request.getFaceUrl().isEmpty()) {
|
||||
int avatarDiameter = (int) (qrcodeHeight * 0.45);
|
||||
int avatarX = qrcodeX + (qrcodeWidth - avatarDiameter) / 2;
|
||||
int avatarY = qrcodeY + (qrcodeHeight - avatarDiameter) / 2;
|
||||
int avatarDiameter = (int) (qrcodeSize * 0.45);
|
||||
int avatarX = qrcodeX + (qrcodeSize - avatarDiameter) / 2;
|
||||
int avatarY = qrcodeY + (qrcodeSize - avatarDiameter) / 2;
|
||||
|
||||
PuzzleElementEntity faceElement = createCircleImageElement(
|
||||
"face", "头像",
|
||||
@@ -146,29 +168,29 @@ public class LeicaWatermarkTemplateBuilder extends AbstractWatermarkTemplateBuil
|
||||
}
|
||||
|
||||
// 7. 计算文字位置(与二维码垂直居中)
|
||||
int qrcodeCenter = qrcodeY + qrcodeHeight / 2;
|
||||
int totalTextHeight = SCENIC_FONT_SIZE + DATETIME_FONT_SIZE + 10;
|
||||
int qrcodeCenter = qrcodeY + qrcodeSize / 2;
|
||||
int totalTextHeight = scenicFontSize + datetimeFontSize + (int)(imageHeight * 0.01);
|
||||
int textY = qrcodeCenter - totalTextHeight / 2;
|
||||
int textX = canvasWidth - OFFSET_X - estimatedTextWidth;
|
||||
int textX = canvasWidth - offsetX - estimatedTextWidth;
|
||||
|
||||
// 8. 景区名文字
|
||||
PuzzleElementEntity scenicTextElement = createTextElement(
|
||||
"scenicLine", "景区名",
|
||||
textX, textY,
|
||||
estimatedTextWidth, SCENIC_FONT_SIZE + 10, 30,
|
||||
"PingFang SC", SCENIC_FONT_SIZE, SCENIC_COLOR,
|
||||
estimatedTextWidth, scenicFontSize + (int)(imageHeight * 0.01), 30,
|
||||
"PingFang SC", scenicFontSize, SCENIC_COLOR,
|
||||
"NORMAL", TEXT_ALIGN_LEFT
|
||||
);
|
||||
elements.add(scenicTextElement);
|
||||
dynamicData.put("scenicLine", request.getScenicLine() != null ? request.getScenicLine() : "");
|
||||
|
||||
// 9. 日期时间文字
|
||||
int datetimeY = textY + SCENIC_FONT_SIZE + 5;
|
||||
int datetimeY = textY + scenicFontSize + (int)(imageHeight * 0.005);
|
||||
PuzzleElementEntity datetimeTextElement = createTextElement(
|
||||
"datetimeLine", "日期时间",
|
||||
textX, datetimeY,
|
||||
estimatedTextWidth, DATETIME_FONT_SIZE + 10, 30,
|
||||
"PingFang SC", DATETIME_FONT_SIZE, DATETIME_COLOR,
|
||||
estimatedTextWidth, datetimeFontSize + (int)(imageHeight * 0.01), 30,
|
||||
"PingFang SC", datetimeFontSize, DATETIME_COLOR,
|
||||
"NORMAL", TEXT_ALIGN_LEFT
|
||||
);
|
||||
elements.add(datetimeTextElement);
|
||||
|
||||
@@ -170,11 +170,13 @@ public class WatermarkEdgeService {
|
||||
* @param info 水印信息(包含本地文件)
|
||||
* @param type 水印类型
|
||||
* @param adapter 存储适配器
|
||||
* @param recordId 记录ID(用于边缘端任务追踪,不能为空)
|
||||
* @return 处理后的本地文件,失败返回null
|
||||
*/
|
||||
public File processWatermarkFromFile(WatermarkInfo info,
|
||||
ImageWatermarkOperatorEnum type,
|
||||
IStorageAdapter adapter) {
|
||||
IStorageAdapter adapter,
|
||||
String recordId) {
|
||||
// 将 ImageWatermarkOperatorEnum 映射到边缘端风格
|
||||
String style = mapTypeToStyle(type);
|
||||
|
||||
@@ -239,18 +241,27 @@ public class WatermarkEdgeService {
|
||||
.outputQuality(90)
|
||||
.build();
|
||||
|
||||
// 6. 创建边缘任务并等待结果
|
||||
// 6. 创建边缘任务并等待结果(使用传入的 recordId)
|
||||
// recordId 转换为 Long,如果无法转换则使用哈希值
|
||||
Long recordIdLong;
|
||||
try {
|
||||
recordIdLong = Long.parseLong(recordId);
|
||||
} catch (NumberFormatException e) {
|
||||
// 如果 recordId 不是数字(如 UUID),使用其哈希值的绝对值
|
||||
recordIdLong = (long) Math.abs(recordId.hashCode());
|
||||
}
|
||||
|
||||
PuzzleEdgeRenderTaskService.TaskWaitResult result = watermarkEdgeTaskCreator.createAndWait(
|
||||
style,
|
||||
request,
|
||||
null, // recordId
|
||||
recordIdLong, // recordId
|
||||
null, // faceId
|
||||
type.getType(), // watermarkType
|
||||
DEFAULT_TIMEOUT_MS
|
||||
);
|
||||
|
||||
if (!result.isSuccess()) {
|
||||
log.error("边缘端水印处理失败: error={}", result.getErrorMessage());
|
||||
log.error("边缘端水印处理失败: recordId={}, error={}", recordId, result.getErrorMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -259,11 +270,11 @@ public class WatermarkEdgeService {
|
||||
File outputFile = info.getWatermarkedFile();
|
||||
downloadFile(resultUrl, outputFile);
|
||||
|
||||
log.info("边缘端水印处理成功: type={}, outputFile={}", type, outputFile);
|
||||
log.info("边缘端水印处理成功: recordId={}, type={}, outputFile={}", recordId, type, outputFile);
|
||||
return outputFile;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("边缘端水印处理异常: type={}", type, e);
|
||||
log.error("边缘端水印处理异常: recordId={}, type={}", recordId, type, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.ycwl.basic.image.pipeline.stages.UpdateMemberPrintStage;
|
||||
import com.ycwl.basic.image.pipeline.stages.UploadStage;
|
||||
import com.ycwl.basic.image.pipeline.stages.WatermarkConfig;
|
||||
import com.ycwl.basic.image.pipeline.stages.WatermarkStage;
|
||||
import com.ycwl.basic.image.watermark.edge.WatermarkEdgeService;
|
||||
import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum;
|
||||
import com.ycwl.basic.integration.common.manager.DeviceConfigManager;
|
||||
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||
@@ -159,6 +160,9 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
);
|
||||
@Autowired
|
||||
private SourceRepository sourceRepository;
|
||||
@Autowired
|
||||
@Lazy
|
||||
private WatermarkEdgeService watermarkEdgeService;
|
||||
|
||||
@Override
|
||||
public List<PrinterResp> listByScenicId(Long scenicId) {
|
||||
@@ -1052,6 +1056,9 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
.watermarkType(watermarkType)
|
||||
.scenicText(scenicText)
|
||||
.dateFormat(dateFormat)
|
||||
.edgeService(watermarkEdgeService)
|
||||
.storageAdapter(StorageFactory.use())
|
||||
.edgeEnabled(true)
|
||||
.qrcodeFile(qrCodeFile)
|
||||
.scale(scale)
|
||||
.build();
|
||||
|
||||
Reference in New Issue
Block a user