feat(image): 启用边缘端水印处理并优化徕卡模板布局算法

- 将边缘端处理默认启用状态从false改为true
- 将边缘端处理超时时间从30秒调整为10秒
- 将徕卡水印模板的固定像素配置转换为基于1920x1080的百分比配置
- 新增多种百分比常量包括底部区域、Logo大小、边距、字体大小等
- 实现动态计算实际像素值的方法替代固定数值
- 在PrinterServiceImpl中注入WatermarkEdgeService依赖
- 配置水印处理流程启用边缘服务和存储适配器
This commit is contained in:
2026-01-16 18:53:31 +08:00
parent 830dd17071
commit 4fac129c3a
5 changed files with 90 additions and 50 deletions

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;
}
}