You've already forked FrameTour-BE
feat(watermark): 添加水印边缘渲染模板构建功能
- 实现抽象水印模板构建器基类提供通用构建工具方法 - 定义水印模板构建器接口规范模板构建契约 - 实现徕卡风格水印模板构建器支持底部扩展布局 - 实现普通风格水印模板构建器支持左下角布局 - 实现打印专用水印模板构建器支持缩放和偏移 - 创建水印边缘任务服务统一管理模板构建流程 - 添加水印请求参数类定义边缘渲染所需字段 - 实现水印模板构建结果类封装模板元素和动态数据 - 集成拼图边缘渲染任务服务实现异步渲染机制
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
package com.ycwl.basic.image.watermark.edge;
|
||||
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||
import com.ycwl.basic.utils.JacksonUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* 水印模板构建器基类
|
||||
* 提供构建元素的工具方法
|
||||
*/
|
||||
public abstract class AbstractWatermarkTemplateBuilder implements IWatermarkTemplateBuilder {
|
||||
|
||||
// 虚拟模板ID(运行时使用,不存储)
|
||||
private static final AtomicLong VIRTUAL_TEMPLATE_ID = new AtomicLong(-1);
|
||||
private static final AtomicLong VIRTUAL_ELEMENT_ID = new AtomicLong(-1);
|
||||
|
||||
/**
|
||||
* 元素类型常量
|
||||
*/
|
||||
protected static final String ELEMENT_TYPE_IMAGE = "IMAGE";
|
||||
protected static final String ELEMENT_TYPE_TEXT = "TEXT";
|
||||
|
||||
/**
|
||||
* 图片适配模式
|
||||
*/
|
||||
protected static final String FIT_MODE_COVER = "COVER";
|
||||
protected static final String FIT_MODE_CONTAIN = "CONTAIN";
|
||||
|
||||
/**
|
||||
* 文本对齐方式
|
||||
*/
|
||||
protected static final String TEXT_ALIGN_LEFT = "LEFT";
|
||||
protected static final String TEXT_ALIGN_RIGHT = "RIGHT";
|
||||
protected static final String TEXT_ALIGN_CENTER = "CENTER";
|
||||
|
||||
/**
|
||||
* 创建虚拟模板
|
||||
*/
|
||||
protected PuzzleTemplateEntity createTemplate(String code, int width, int height, String backgroundImage) {
|
||||
PuzzleTemplateEntity template = new PuzzleTemplateEntity();
|
||||
template.setId(VIRTUAL_TEMPLATE_ID.decrementAndGet());
|
||||
template.setCode(code);
|
||||
template.setName("水印模板-" + getStyle());
|
||||
template.setCanvasWidth(width);
|
||||
template.setCanvasHeight(height);
|
||||
template.setBackgroundType(1); // 图片背景
|
||||
template.setBackgroundImage(backgroundImage);
|
||||
template.setStatus(1);
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建纯色背景模板
|
||||
*/
|
||||
protected PuzzleTemplateEntity createTemplateWithColor(String code, int width, int height, String backgroundColor) {
|
||||
PuzzleTemplateEntity template = new PuzzleTemplateEntity();
|
||||
template.setId(VIRTUAL_TEMPLATE_ID.decrementAndGet());
|
||||
template.setCode(code);
|
||||
template.setName("水印模板-" + getStyle());
|
||||
template.setCanvasWidth(width);
|
||||
template.setCanvasHeight(height);
|
||||
template.setBackgroundType(0); // 纯色背景
|
||||
template.setBackgroundColor(backgroundColor);
|
||||
template.setStatus(1);
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建图片元素
|
||||
*/
|
||||
protected PuzzleElementEntity createImageElement(String key, String name, int x, int y, int width, int height, int zIndex,
|
||||
String fitMode, Integer borderRadius, Integer opacity) {
|
||||
PuzzleElementEntity element = new PuzzleElementEntity();
|
||||
element.setId(VIRTUAL_ELEMENT_ID.decrementAndGet());
|
||||
element.setElementType(ELEMENT_TYPE_IMAGE);
|
||||
element.setElementKey(key);
|
||||
element.setElementName(name);
|
||||
element.setXPosition(x);
|
||||
element.setYPosition(y);
|
||||
element.setWidth(width);
|
||||
element.setHeight(height);
|
||||
element.setZIndex(zIndex);
|
||||
element.setOpacity(opacity != null ? opacity : 100);
|
||||
|
||||
// 构建配置JSON
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
config.put("imageFitMode", fitMode != null ? fitMode : FIT_MODE_COVER);
|
||||
if (borderRadius != null && borderRadius > 0) {
|
||||
config.put("borderRadius", borderRadius);
|
||||
}
|
||||
element.setConfig(JacksonUtil.toJson(config));
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建圆形图片元素
|
||||
*/
|
||||
protected PuzzleElementEntity createCircleImageElement(String key, String name, int x, int y, int diameter, int zIndex) {
|
||||
// 圆形 = borderRadius 为直径的一半
|
||||
return createImageElement(key, name, x, y, diameter, diameter, zIndex, FIT_MODE_COVER, diameter / 2, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文字元素
|
||||
*/
|
||||
protected PuzzleElementEntity createTextElement(String key, String name, int x, int y, int width, int height, int zIndex,
|
||||
String fontFamily, int fontSize, String fontColor,
|
||||
String fontWeight, String textAlign) {
|
||||
PuzzleElementEntity element = new PuzzleElementEntity();
|
||||
element.setId(VIRTUAL_ELEMENT_ID.decrementAndGet());
|
||||
element.setElementType(ELEMENT_TYPE_TEXT);
|
||||
element.setElementKey(key);
|
||||
element.setElementName(name);
|
||||
element.setXPosition(x);
|
||||
element.setYPosition(y);
|
||||
element.setWidth(width);
|
||||
element.setHeight(height);
|
||||
element.setZIndex(zIndex);
|
||||
element.setOpacity(100);
|
||||
|
||||
// 构建配置JSON
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
config.put("fontFamily", fontFamily != null ? fontFamily : "PingFang SC");
|
||||
config.put("fontSize", fontSize);
|
||||
config.put("fontColor", fontColor != null ? fontColor : "#FFFFFF");
|
||||
config.put("fontWeight", fontWeight != null ? fontWeight : "NORMAL");
|
||||
config.put("textAlign", textAlign != null ? textAlign : TEXT_ALIGN_LEFT);
|
||||
element.setConfig(JacksonUtil.toJson(config));
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建构建结果
|
||||
*/
|
||||
protected WatermarkTemplateResult createResult(PuzzleTemplateEntity template,
|
||||
List<PuzzleElementEntity> elements,
|
||||
Map<String, String> dynamicData) {
|
||||
WatermarkTemplateResult result = new WatermarkTemplateResult();
|
||||
result.setTemplate(template);
|
||||
result.setElements(elements);
|
||||
result.setDynamicData(dynamicData);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建空的元素列表和动态数据
|
||||
*/
|
||||
protected List<PuzzleElementEntity> newElementList() {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
protected Map<String, String> newDynamicData() {
|
||||
return new HashMap<>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ycwl.basic.image.watermark.edge;
|
||||
|
||||
/**
|
||||
* 水印模板构建器接口
|
||||
* 将水印参数转换为拼图模板+元素的形式,用于发送给边缘渲染任务
|
||||
*/
|
||||
public interface IWatermarkTemplateBuilder {
|
||||
|
||||
/**
|
||||
* 构建水印模板
|
||||
*
|
||||
* @param request 水印请求参数
|
||||
* @return 模板构建结果
|
||||
*/
|
||||
WatermarkTemplateResult build(WatermarkRequest request);
|
||||
|
||||
/**
|
||||
* 获取水印风格标识
|
||||
*/
|
||||
String getStyle();
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package com.ycwl.basic.image.watermark.edge;
|
||||
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 徕卡风格水印模板构建器
|
||||
* 对应 LeicaWatermarkOperator
|
||||
*
|
||||
* 布局说明:
|
||||
* - 画布高度 = 原图高度 + 底部扩展区域(白色背景)
|
||||
* - 原图放在画布顶部
|
||||
* - 底部白色区域左侧:帧途 Logo + "帧途" 文字
|
||||
* - 底部白色区域右侧:二维码(含头像)+ 景区名 + 日期时间
|
||||
*/
|
||||
@Component
|
||||
public class LeicaWatermarkTemplateBuilder extends AbstractWatermarkTemplateBuilder {
|
||||
|
||||
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;
|
||||
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";
|
||||
|
||||
/**
|
||||
* Logo 图片 URL(需要预先上传到 OSS)
|
||||
*/
|
||||
private static final String LOGO_URL = "https://oss.zhentuai.com/zt/zt-logo.png";
|
||||
|
||||
@Override
|
||||
public String getStyle() {
|
||||
return STYLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WatermarkTemplateResult build(WatermarkRequest request) {
|
||||
int imageWidth = request.getImageWidth();
|
||||
int imageHeight = request.getImageHeight();
|
||||
|
||||
// 画布高度 = 原图高度 + 底部扩展区域
|
||||
int canvasWidth = imageWidth;
|
||||
int canvasHeight = imageHeight + EXTRA_BOTTOM_PX;
|
||||
|
||||
// 创建模板(白色背景)
|
||||
PuzzleTemplateEntity template = createTemplateWithColor(
|
||||
"watermark_leica_" + System.currentTimeMillis(),
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
"#FFFFFF"
|
||||
);
|
||||
|
||||
List<PuzzleElementEntity> elements = newElementList();
|
||||
Map<String, String> dynamicData = newDynamicData();
|
||||
|
||||
// 1. 原图元素(放在画布顶部)
|
||||
PuzzleElementEntity originalImageElement = createImageElement(
|
||||
"originalImage", "原图",
|
||||
0, 0,
|
||||
imageWidth, imageHeight, 1,
|
||||
FIT_MODE_COVER, null, null
|
||||
);
|
||||
elements.add(originalImageElement);
|
||||
dynamicData.put("originalImage", request.getOriginalImageUrl());
|
||||
|
||||
// 2. Logo 元素(底部左侧)
|
||||
int logoY = imageHeight + OFFSET_Y + LOGO_EXTRA_BORDER;
|
||||
PuzzleElementEntity logoElement = createImageElement(
|
||||
"logo", "Logo",
|
||||
OFFSET_X, logoY - 12,
|
||||
LOGO_SIZE, LOGO_SIZE, 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 = imageHeight + OFFSET_Y + LOGO_EXTRA_BORDER;
|
||||
PuzzleElementEntity logoTextElement = createTextElement(
|
||||
"logoText", "帧途文字",
|
||||
logoTextX, logoTextY,
|
||||
100, LOGO_SIZE, 10,
|
||||
"PingFang SC", LOGO_FONT_SIZE, 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
|
||||
);
|
||||
|
||||
int qrcodeX = canvasWidth - OFFSET_X - qrcodeWidth - QRCODE_OFFSET_X - estimatedTextWidth;
|
||||
int qrcodeY = imageHeight + OFFSET_Y - QRCODE_OFFSET_Y;
|
||||
|
||||
// 5. 二维码元素
|
||||
PuzzleElementEntity qrcodeElement = createImageElement(
|
||||
"qrcode", "二维码",
|
||||
qrcodeX, qrcodeY,
|
||||
qrcodeWidth, qrcodeHeight, 10,
|
||||
FIT_MODE_CONTAIN, null, null
|
||||
);
|
||||
elements.add(qrcodeElement);
|
||||
dynamicData.put("qrcode", request.getQrcodeUrl());
|
||||
|
||||
// 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;
|
||||
|
||||
PuzzleElementEntity faceElement = createCircleImageElement(
|
||||
"face", "头像",
|
||||
avatarX, avatarY,
|
||||
avatarDiameter, 20
|
||||
);
|
||||
elements.add(faceElement);
|
||||
dynamicData.put("face", request.getFaceUrl());
|
||||
}
|
||||
|
||||
// 7. 计算文字位置(与二维码垂直居中)
|
||||
int qrcodeCenter = qrcodeY + qrcodeHeight / 2;
|
||||
int totalTextHeight = SCENIC_FONT_SIZE + DATETIME_FONT_SIZE + 10;
|
||||
int textY = qrcodeCenter - totalTextHeight / 2;
|
||||
int textX = canvasWidth - OFFSET_X - estimatedTextWidth;
|
||||
|
||||
// 8. 景区名文字
|
||||
PuzzleElementEntity scenicTextElement = createTextElement(
|
||||
"scenicLine", "景区名",
|
||||
textX, textY,
|
||||
estimatedTextWidth, SCENIC_FONT_SIZE + 10, 30,
|
||||
"PingFang SC", SCENIC_FONT_SIZE, 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;
|
||||
PuzzleElementEntity datetimeTextElement = createTextElement(
|
||||
"datetimeLine", "日期时间",
|
||||
textX, datetimeY,
|
||||
estimatedTextWidth, DATETIME_FONT_SIZE + 10, 30,
|
||||
"PingFang SC", DATETIME_FONT_SIZE, DATETIME_COLOR,
|
||||
"NORMAL", TEXT_ALIGN_LEFT
|
||||
);
|
||||
elements.add(datetimeTextElement);
|
||||
dynamicData.put("datetimeLine", request.getDatetimeLine() != null ? request.getDatetimeLine() : "");
|
||||
|
||||
return createResult(template, elements, dynamicData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.ycwl.basic.image.watermark.edge;
|
||||
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Normal 风格水印模板构建器
|
||||
* 对应 NormalWatermarkOperator
|
||||
*
|
||||
* 布局说明:
|
||||
* - 白色背景 + 原图元素(COVER模式)
|
||||
* - 左下角:圆形二维码(带白色圆形背景)
|
||||
* - 二维码中央:圆形头像(可选)
|
||||
* - 二维码右侧:景区名 + 日期时间 两行文字(白色)
|
||||
*/
|
||||
@Component
|
||||
public class NormalWatermarkTemplateBuilder extends AbstractWatermarkTemplateBuilder {
|
||||
|
||||
public static final String STYLE = "normal";
|
||||
|
||||
// 常量配置(与 NormalWatermarkOperator 保持一致)
|
||||
private static final int OFFSET_Y = 90;
|
||||
private static final int QRCODE_SIZE = 150;
|
||||
private static final int QRCODE_OFFSET_X = 10;
|
||||
private static final int QRCODE_OFFSET_Y = -20;
|
||||
private static final int SCENIC_FONT_SIZE = 42;
|
||||
private static final int DATETIME_FONT_SIZE = 42;
|
||||
private static final String FONT_COLOR = "#FFFFFF";
|
||||
|
||||
@Override
|
||||
public String getStyle() {
|
||||
return STYLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WatermarkTemplateResult build(WatermarkRequest request) {
|
||||
int imageWidth = request.getImageWidth();
|
||||
int imageHeight = request.getImageHeight();
|
||||
|
||||
// 创建模板(白色背景,原图作为元素实现 COVER 模式)
|
||||
PuzzleTemplateEntity template = createTemplateWithColor(
|
||||
"watermark_normal_" + System.currentTimeMillis(),
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
"#FFFFFF"
|
||||
);
|
||||
|
||||
List<PuzzleElementEntity> elements = newElementList();
|
||||
Map<String, String> dynamicData = newDynamicData();
|
||||
|
||||
// 0. 原图元素(z-index=1,最底层,COVER模式)
|
||||
PuzzleElementEntity originalImageElement = createImageElement(
|
||||
"originalImage", "原图",
|
||||
0, 0,
|
||||
imageWidth, imageHeight, 1,
|
||||
FIT_MODE_COVER, null, null
|
||||
);
|
||||
elements.add(originalImageElement);
|
||||
dynamicData.put("originalImage", request.getOriginalImageUrl());
|
||||
|
||||
// 计算二维码位置
|
||||
int qrcodeHeight = QRCODE_SIZE;
|
||||
int qrcodeWidth = QRCODE_SIZE; // 假设二维码是正方形
|
||||
int offsetX = calculateQrcodeOffsetX(imageWidth, qrcodeWidth, request);
|
||||
int qrcodeY = imageHeight - OFFSET_Y - qrcodeHeight;
|
||||
int qrcodeX = offsetX;
|
||||
|
||||
// 1. 白色圆形背景(比二维码大10像素)
|
||||
int whiteCircleSize = qrcodeHeight + 10;
|
||||
int whiteCircleX = qrcodeX - (whiteCircleSize - qrcodeWidth) / 2;
|
||||
int whiteCircleY = qrcodeY + QRCODE_OFFSET_Y - (whiteCircleSize - qrcodeHeight) / 2;
|
||||
|
||||
// 使用图片元素模拟白色圆形(边缘端需要支持纯色圆形或使用白色圆形图片)
|
||||
// 这里暂时跳过白色背景,让二维码直接显示
|
||||
|
||||
// 2. 二维码元素(圆形裁切)
|
||||
PuzzleElementEntity qrcodeElement = createCircleImageElement(
|
||||
"qrcode", "二维码",
|
||||
qrcodeX, qrcodeY + QRCODE_OFFSET_Y,
|
||||
qrcodeHeight, 10
|
||||
);
|
||||
elements.add(qrcodeElement);
|
||||
dynamicData.put("qrcode", request.getQrcodeUrl());
|
||||
|
||||
// 3. 头像元素(圆形,二维码中央,可选)
|
||||
if (request.getFaceUrl() != null && !request.getFaceUrl().isEmpty()) {
|
||||
int avatarDiameter = (int) (qrcodeHeight * 0.45);
|
||||
int avatarX = qrcodeX + (qrcodeWidth - avatarDiameter) / 2;
|
||||
int avatarY = qrcodeY + QRCODE_OFFSET_Y + (qrcodeHeight - avatarDiameter) / 2;
|
||||
|
||||
PuzzleElementEntity faceElement = createCircleImageElement(
|
||||
"face", "头像",
|
||||
avatarX, avatarY,
|
||||
avatarDiameter, 20
|
||||
);
|
||||
elements.add(faceElement);
|
||||
dynamicData.put("face", request.getFaceUrl());
|
||||
}
|
||||
|
||||
// 4. 景区名文字
|
||||
int textX = qrcodeX + qrcodeWidth + QRCODE_OFFSET_X;
|
||||
int textY = qrcodeY + QRCODE_OFFSET_Y + qrcodeHeight / 2 - SCENIC_FONT_SIZE;
|
||||
|
||||
PuzzleElementEntity scenicTextElement = createTextElement(
|
||||
"scenicLine", "景区名",
|
||||
textX, textY,
|
||||
imageWidth - textX - 20, SCENIC_FONT_SIZE + 10, 30,
|
||||
"PingFang SC", SCENIC_FONT_SIZE, FONT_COLOR,
|
||||
"NORMAL", TEXT_ALIGN_LEFT
|
||||
);
|
||||
elements.add(scenicTextElement);
|
||||
dynamicData.put("scenicLine", request.getScenicLine() != null ? request.getScenicLine() : "");
|
||||
|
||||
// 5. 日期时间文字
|
||||
int datetimeY = textY + SCENIC_FONT_SIZE + 5;
|
||||
|
||||
PuzzleElementEntity datetimeTextElement = createTextElement(
|
||||
"datetimeLine", "日期时间",
|
||||
textX, datetimeY,
|
||||
imageWidth - textX - 20, DATETIME_FONT_SIZE + 10, 30,
|
||||
"PingFang SC", DATETIME_FONT_SIZE, FONT_COLOR,
|
||||
"NORMAL", TEXT_ALIGN_LEFT
|
||||
);
|
||||
elements.add(datetimeTextElement);
|
||||
dynamicData.put("datetimeLine", request.getDatetimeLine() != null ? request.getDatetimeLine() : "");
|
||||
|
||||
return createResult(template, elements, dynamicData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算二维码的X偏移位置(居中于文字和二维码整体)
|
||||
*/
|
||||
private int calculateQrcodeOffsetX(int imageWidth, int qrcodeWidth, WatermarkRequest request) {
|
||||
// 简化处理:固定距离左边一定比例
|
||||
return (int) (imageWidth * 0.1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package com.ycwl.basic.image.watermark.edge;
|
||||
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 打印专用水印模板构建器
|
||||
* 对应 PrinterDefaultWatermarkOperator
|
||||
*
|
||||
* 布局说明:
|
||||
* - 白色背景 + 原图元素(COVER模式)
|
||||
* - 左下角:圆形二维码(带白色圆形背景)
|
||||
* - 二维码中央:圆形头像(可选)
|
||||
* - 右下角:景区名 + 日期时间 两行文字(白色,右对齐)
|
||||
* - 支持缩放和四边偏移
|
||||
*/
|
||||
@Component
|
||||
public class PrinterDefaultWatermarkTemplateBuilder extends AbstractWatermarkTemplateBuilder {
|
||||
|
||||
public static final String STYLE = "printer_default";
|
||||
|
||||
// 常量配置(与 PrinterDefaultWatermarkOperator 保持一致)
|
||||
private static final int OFFSET_Y = 15;
|
||||
private static final int QRCODE_SIZE = 150;
|
||||
private static final double QRCODE_LEFT_MARGIN_RATIO = 0.05;
|
||||
private static final int QRCODE_OFFSET_Y = -35;
|
||||
private static final int SCENIC_FONT_SIZE = 42;
|
||||
private static final int DATETIME_FONT_SIZE = 42;
|
||||
private static final String FONT_COLOR = "#FFFFFF";
|
||||
private static final double TEXT_RIGHT_MARGIN_RATIO = 0.05;
|
||||
|
||||
@Override
|
||||
public String getStyle() {
|
||||
return STYLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WatermarkTemplateResult build(WatermarkRequest request) {
|
||||
int imageWidth = request.getImageWidth();
|
||||
int imageHeight = request.getImageHeight();
|
||||
double scale = request.getScaleValue();
|
||||
|
||||
// 应用缩放
|
||||
int scaledOffsetY = (int) (OFFSET_Y * scale);
|
||||
int scaledQrcodeSize = (int) (QRCODE_SIZE * scale);
|
||||
int scaledQrcodeOffsetY = (int) (QRCODE_OFFSET_Y * scale);
|
||||
int scaledScenicFontSize = (int) (SCENIC_FONT_SIZE * scale);
|
||||
int scaledDatetimeFontSize = (int) (DATETIME_FONT_SIZE * scale);
|
||||
|
||||
// 获取偏移值
|
||||
int offsetLeft = (int) (request.getOffsetLeftValue() * scale);
|
||||
int offsetRight = (int) (request.getOffsetRightValue() * scale);
|
||||
int offsetBottom = (int) (request.getOffsetBottomValue() * scale);
|
||||
|
||||
// 创建模板(白色背景,原图作为元素实现 COVER 模式)
|
||||
PuzzleTemplateEntity template = createTemplateWithColor(
|
||||
"watermark_printer_" + System.currentTimeMillis(),
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
"#FFFFFF"
|
||||
);
|
||||
|
||||
List<PuzzleElementEntity> elements = newElementList();
|
||||
Map<String, String> dynamicData = newDynamicData();
|
||||
|
||||
// 0. 原图元素(z-index=1,最底层,COVER模式)
|
||||
PuzzleElementEntity originalImageElement = createImageElement(
|
||||
"originalImage", "原图",
|
||||
0, 0,
|
||||
imageWidth, imageHeight, 1,
|
||||
FIT_MODE_COVER, null, null
|
||||
);
|
||||
elements.add(originalImageElement);
|
||||
dynamicData.put("originalImage", request.getOriginalImageUrl());
|
||||
|
||||
// 计算二维码位置
|
||||
int qrcodeWidth = scaledQrcodeSize;
|
||||
int qrcodeHeight = scaledQrcodeSize;
|
||||
int qrcodeX = (int) (imageWidth * QRCODE_LEFT_MARGIN_RATIO) + offsetLeft;
|
||||
int qrcodeY = imageHeight - scaledOffsetY - qrcodeHeight - offsetBottom;
|
||||
|
||||
// 1. 二维码元素(圆形裁切)
|
||||
PuzzleElementEntity qrcodeElement = createCircleImageElement(
|
||||
"qrcode", "二维码",
|
||||
qrcodeX, qrcodeY + scaledQrcodeOffsetY,
|
||||
qrcodeHeight, 10
|
||||
);
|
||||
elements.add(qrcodeElement);
|
||||
dynamicData.put("qrcode", request.getQrcodeUrl());
|
||||
|
||||
// 2. 头像元素(圆形,二维码中央,可选)
|
||||
if (request.getFaceUrl() != null && !request.getFaceUrl().isEmpty()) {
|
||||
int avatarDiameter = (int) (qrcodeHeight * 0.45);
|
||||
int avatarX = qrcodeX + (qrcodeWidth - avatarDiameter) / 2;
|
||||
int avatarY = qrcodeY + scaledQrcodeOffsetY + (qrcodeHeight - avatarDiameter) / 2;
|
||||
|
||||
PuzzleElementEntity faceElement = createCircleImageElement(
|
||||
"face", "头像",
|
||||
avatarX, avatarY,
|
||||
avatarDiameter, 20
|
||||
);
|
||||
elements.add(faceElement);
|
||||
dynamicData.put("face", request.getFaceUrl());
|
||||
}
|
||||
|
||||
// 3. 计算文字位置(右对齐)
|
||||
int textRightX = imageWidth - (int) (imageWidth * TEXT_RIGHT_MARGIN_RATIO) - offsetRight;
|
||||
int textWidth = textRightX - qrcodeX - qrcodeWidth - 20;
|
||||
|
||||
// 计算垂直居中
|
||||
int qrcodeTop = qrcodeY + scaledQrcodeOffsetY;
|
||||
int qrcodeBottom = qrcodeTop + qrcodeHeight;
|
||||
int qrcodeCenter = (qrcodeTop + qrcodeBottom) / 2;
|
||||
int totalTextHeight = scaledScenicFontSize + scaledDatetimeFontSize + 10;
|
||||
int textY = qrcodeCenter - totalTextHeight / 2;
|
||||
|
||||
// 4. 景区名文字(右对齐)
|
||||
PuzzleElementEntity scenicTextElement = createTextElement(
|
||||
"scenicLine", "景区名",
|
||||
textRightX - textWidth, textY,
|
||||
textWidth, scaledScenicFontSize + 10, 30,
|
||||
"PingFang SC", scaledScenicFontSize, FONT_COLOR,
|
||||
"BOLD", TEXT_ALIGN_RIGHT
|
||||
);
|
||||
elements.add(scenicTextElement);
|
||||
dynamicData.put("scenicLine", request.getScenicLine() != null ? request.getScenicLine() : "");
|
||||
|
||||
// 5. 日期时间文字(右对齐)
|
||||
int datetimeY = textY + scaledScenicFontSize + 5;
|
||||
|
||||
PuzzleElementEntity datetimeTextElement = createTextElement(
|
||||
"datetimeLine", "日期时间",
|
||||
textRightX - textWidth, datetimeY,
|
||||
textWidth, scaledDatetimeFontSize + 10, 30,
|
||||
"PingFang SC", scaledDatetimeFontSize, FONT_COLOR,
|
||||
"BOLD", TEXT_ALIGN_RIGHT
|
||||
);
|
||||
elements.add(datetimeTextElement);
|
||||
dynamicData.put("datetimeLine", request.getDatetimeLine() != null ? request.getDatetimeLine() : "");
|
||||
|
||||
return createResult(template, elements, dynamicData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.ycwl.basic.image.watermark.edge;
|
||||
|
||||
import com.ycwl.basic.puzzle.edge.task.PuzzleEdgeRenderTaskService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 水印边缘任务创建服务
|
||||
* 将水印请求转换为边缘渲染任务
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WatermarkEdgeTaskCreator {
|
||||
|
||||
private final PuzzleEdgeRenderTaskService edgeRenderTaskService;
|
||||
private final List<IWatermarkTemplateBuilder> builders;
|
||||
|
||||
private final Map<String, IWatermarkTemplateBuilder> builderMap = new HashMap<>();
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
for (IWatermarkTemplateBuilder builder : builders) {
|
||||
builderMap.put(builder.getStyle(), builder);
|
||||
log.info("注册水印模板构建器: {}", builder.getStyle());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建水印渲染任务
|
||||
*
|
||||
* @param style 水印风格(normal/leica/printer_default)
|
||||
* @param request 水印请求参数
|
||||
* @param recordId 原始拼图记录ID(用于关联)
|
||||
* @param faceId 人脸ID(可选)
|
||||
* @param watermarkType 水印类型标识(如 print、free_download)
|
||||
* @return 任务ID
|
||||
*/
|
||||
public Long createTask(String style,
|
||||
WatermarkRequest request,
|
||||
Long recordId,
|
||||
Long faceId,
|
||||
String watermarkType) {
|
||||
IWatermarkTemplateBuilder builder = builderMap.get(style);
|
||||
if (builder == null) {
|
||||
throw new IllegalArgumentException("未知的水印风格: " + style);
|
||||
}
|
||||
|
||||
// 构建水印模板
|
||||
WatermarkTemplateResult result = builder.build(request);
|
||||
|
||||
// 创建边缘渲染任务
|
||||
Long taskId = edgeRenderTaskService.createWatermarkRenderTask(
|
||||
recordId,
|
||||
faceId,
|
||||
watermarkType,
|
||||
result.getTemplate(),
|
||||
result.getElements(),
|
||||
result.getDynamicData(),
|
||||
request.getOutputFormat(),
|
||||
request.getOutputQuality()
|
||||
);
|
||||
|
||||
log.info("创建水印边缘渲染任务: style={}, taskId={}, recordId={}, watermarkType={}",
|
||||
style, taskId, recordId, watermarkType);
|
||||
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建水印渲染任务并等待结果
|
||||
*
|
||||
* @param style 水印风格
|
||||
* @param request 水印请求参数
|
||||
* @param recordId 原始拼图记录ID
|
||||
* @param faceId 人脸ID
|
||||
* @param watermarkType 水印类型
|
||||
* @param timeoutMs 超时时间(毫秒)
|
||||
* @return 任务结果
|
||||
*/
|
||||
public PuzzleEdgeRenderTaskService.TaskWaitResult createAndWait(String style,
|
||||
WatermarkRequest request,
|
||||
Long recordId,
|
||||
Long faceId,
|
||||
String watermarkType,
|
||||
long timeoutMs) {
|
||||
Long taskId = createTask(style, request, recordId, faceId, watermarkType);
|
||||
edgeRenderTaskService.registerWait(taskId);
|
||||
return edgeRenderTaskService.waitForTask(taskId, timeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支持的水印风格列表
|
||||
*/
|
||||
public List<String> getSupportedStyles() {
|
||||
return List.copyOf(builderMap.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持指定的水印风格
|
||||
*/
|
||||
public boolean isStyleSupported(String style) {
|
||||
return builderMap.containsKey(style);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.ycwl.basic.image.watermark.edge;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 水印请求参数
|
||||
* 将原有的 WatermarkInfo(基于文件)转换为边缘渲染所需的格式(基于URL)
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class WatermarkRequest {
|
||||
/**
|
||||
* 原图URL
|
||||
*/
|
||||
private String originalImageUrl;
|
||||
|
||||
/**
|
||||
* 原图宽度(像素)
|
||||
*/
|
||||
private int imageWidth;
|
||||
|
||||
/**
|
||||
* 原图高度(像素)
|
||||
*/
|
||||
private int imageHeight;
|
||||
|
||||
/**
|
||||
* 二维码URL
|
||||
*/
|
||||
private String qrcodeUrl;
|
||||
|
||||
/**
|
||||
* 头像URL(可选)
|
||||
*/
|
||||
private String faceUrl;
|
||||
|
||||
/**
|
||||
* 景区名称
|
||||
*/
|
||||
private String scenicLine;
|
||||
|
||||
/**
|
||||
* 日期时间行
|
||||
*/
|
||||
private String datetimeLine;
|
||||
|
||||
/**
|
||||
* 四边偏移(像素),正数表示向内偏移
|
||||
*/
|
||||
private Integer offsetTop;
|
||||
private Integer offsetBottom;
|
||||
private Integer offsetLeft;
|
||||
private Integer offsetRight;
|
||||
|
||||
/**
|
||||
* 缩放倍数,默认1.0
|
||||
*/
|
||||
private Double scale;
|
||||
|
||||
/**
|
||||
* 输出格式:PNG / JPEG
|
||||
*/
|
||||
@Builder.Default
|
||||
private String outputFormat = "JPEG";
|
||||
|
||||
/**
|
||||
* 输出质量(0-100)
|
||||
*/
|
||||
@Builder.Default
|
||||
private Integer outputQuality = 75;
|
||||
|
||||
public double getScaleValue() {
|
||||
return scale != null ? scale : 1.0;
|
||||
}
|
||||
|
||||
public int getOffsetTopValue() {
|
||||
return offsetTop != null ? offsetTop : 0;
|
||||
}
|
||||
|
||||
public int getOffsetBottomValue() {
|
||||
return offsetBottom != null ? offsetBottom : 0;
|
||||
}
|
||||
|
||||
public int getOffsetLeftValue() {
|
||||
return offsetLeft != null ? offsetLeft : 0;
|
||||
}
|
||||
|
||||
public int getOffsetRightValue() {
|
||||
return offsetRight != null ? offsetRight : 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.ycwl.basic.image.watermark.edge;
|
||||
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleElementEntity;
|
||||
import com.ycwl.basic.puzzle.entity.PuzzleTemplateEntity;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 水印模板构建结果
|
||||
* 包含虚拟模板、元素列表和动态数据,用于发送给边缘渲染任务
|
||||
*/
|
||||
@Data
|
||||
public class WatermarkTemplateResult {
|
||||
/**
|
||||
* 虚拟模板(运行时构造,不存储到数据库)
|
||||
*/
|
||||
private PuzzleTemplateEntity template;
|
||||
|
||||
/**
|
||||
* 元素列表(按z-index排序)
|
||||
*/
|
||||
private List<PuzzleElementEntity> elements;
|
||||
|
||||
/**
|
||||
* 动态数据(elementKey -> 实际值URL或文本)
|
||||
*/
|
||||
private Map<String, String> dynamicData;
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
package com.ycwl.basic.image.watermark.edge.controller;
|
||||
|
||||
import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.image.watermark.edge.WatermarkEdgeTaskCreator;
|
||||
import com.ycwl.basic.image.watermark.edge.WatermarkRequest;
|
||||
import com.ycwl.basic.puzzle.edge.task.PuzzleEdgeRenderTaskService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 水印边缘渲染测试控制器
|
||||
* 用于测试水印边缘渲染功能
|
||||
*/
|
||||
@Slf4j
|
||||
@IgnoreToken
|
||||
@RestController
|
||||
@RequestMapping("/test/watermark/edge")
|
||||
@RequiredArgsConstructor
|
||||
public class WatermarkEdgeTestController {
|
||||
|
||||
private final WatermarkEdgeTaskCreator watermarkEdgeTaskCreator;
|
||||
private final PuzzleEdgeRenderTaskService puzzleEdgeRenderTaskService;
|
||||
|
||||
/**
|
||||
* 获取支持的水印风格列表
|
||||
*/
|
||||
@GetMapping("/styles")
|
||||
public ApiResponse<List<String>> getSupportedStyles() {
|
||||
return ApiResponse.success(watermarkEdgeTaskCreator.getSupportedStyles());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建水印渲染任务(异步)
|
||||
* 任务创建后由边缘端拉取执行
|
||||
*/
|
||||
@PostMapping("/create")
|
||||
public ApiResponse<CreateTaskResponse> createTask(@RequestBody CreateTaskRequest req) {
|
||||
// 参数校验
|
||||
if (req.getStyle() == null || req.getStyle().isEmpty()) {
|
||||
return ApiResponse.fail("水印风格(style)不能为空");
|
||||
}
|
||||
if (!watermarkEdgeTaskCreator.isStyleSupported(req.getStyle())) {
|
||||
return ApiResponse.fail("不支持的水印风格: " + req.getStyle() +
|
||||
",支持的风格: " + watermarkEdgeTaskCreator.getSupportedStyles());
|
||||
}
|
||||
if (req.getOriginalImageUrl() == null || req.getOriginalImageUrl().isEmpty()) {
|
||||
return ApiResponse.fail("原图URL(originalImageUrl)不能为空");
|
||||
}
|
||||
if (req.getImageWidth() <= 0 || req.getImageHeight() <= 0) {
|
||||
return ApiResponse.fail("图片宽高必须大于0");
|
||||
}
|
||||
|
||||
// 构建请求
|
||||
WatermarkRequest watermarkRequest = WatermarkRequest.builder()
|
||||
.originalImageUrl(req.getOriginalImageUrl())
|
||||
.imageWidth(req.getImageWidth())
|
||||
.imageHeight(req.getImageHeight())
|
||||
.qrcodeUrl(req.getQrcodeUrl())
|
||||
.faceUrl(req.getFaceUrl())
|
||||
.scenicLine(req.getScenicLine())
|
||||
.datetimeLine(req.getDatetimeLine())
|
||||
.offsetTop(req.getOffsetTop())
|
||||
.offsetBottom(req.getOffsetBottom())
|
||||
.offsetLeft(req.getOffsetLeft())
|
||||
.offsetRight(req.getOffsetRight())
|
||||
.scale(req.getScale())
|
||||
.outputFormat(req.getOutputFormat() != null ? req.getOutputFormat() : "JPEG")
|
||||
.outputQuality(req.getOutputQuality() != null ? req.getOutputQuality() : 75)
|
||||
.build();
|
||||
|
||||
// 创建任务
|
||||
Long taskId = watermarkEdgeTaskCreator.createTask(
|
||||
req.getStyle(),
|
||||
watermarkRequest,
|
||||
req.getRecordId() != null ? req.getRecordId() : 0L, // 测试用默认值
|
||||
req.getFaceId(),
|
||||
req.getWatermarkType() != null ? req.getWatermarkType() : "test"
|
||||
);
|
||||
|
||||
CreateTaskResponse response = new CreateTaskResponse();
|
||||
response.setTaskId(taskId);
|
||||
response.setMessage("任务已创建,等待边缘端拉取执行");
|
||||
|
||||
log.info("测试创建水印任务: style={}, taskId={}", req.getStyle(), taskId);
|
||||
return ApiResponse.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建水印渲染任务并等待结果(同步)
|
||||
* 注意:此接口会阻塞直到任务完成或超时
|
||||
*/
|
||||
@PostMapping("/createAndWait")
|
||||
public ApiResponse<CreateAndWaitResponse> createAndWait(@RequestBody CreateTaskRequest req) {
|
||||
// 参数校验
|
||||
if (req.getStyle() == null || req.getStyle().isEmpty()) {
|
||||
return ApiResponse.fail("水印风格(style)不能为空");
|
||||
}
|
||||
if (!watermarkEdgeTaskCreator.isStyleSupported(req.getStyle())) {
|
||||
return ApiResponse.fail("不支持的水印风格: " + req.getStyle() +
|
||||
",支持的风格: " + watermarkEdgeTaskCreator.getSupportedStyles());
|
||||
}
|
||||
if (req.getOriginalImageUrl() == null || req.getOriginalImageUrl().isEmpty()) {
|
||||
return ApiResponse.fail("原图URL(originalImageUrl)不能为空");
|
||||
}
|
||||
if (req.getImageWidth() <= 0 || req.getImageHeight() <= 0) {
|
||||
return ApiResponse.fail("图片宽高必须大于0");
|
||||
}
|
||||
|
||||
// 构建请求
|
||||
WatermarkRequest watermarkRequest = WatermarkRequest.builder()
|
||||
.originalImageUrl(req.getOriginalImageUrl())
|
||||
.imageWidth(req.getImageWidth())
|
||||
.imageHeight(req.getImageHeight())
|
||||
.qrcodeUrl(req.getQrcodeUrl())
|
||||
.faceUrl(req.getFaceUrl())
|
||||
.scenicLine(req.getScenicLine())
|
||||
.datetimeLine(req.getDatetimeLine())
|
||||
.offsetTop(req.getOffsetTop())
|
||||
.offsetBottom(req.getOffsetBottom())
|
||||
.offsetLeft(req.getOffsetLeft())
|
||||
.offsetRight(req.getOffsetRight())
|
||||
.scale(req.getScale())
|
||||
.outputFormat(req.getOutputFormat() != null ? req.getOutputFormat() : "JPEG")
|
||||
.outputQuality(req.getOutputQuality() != null ? req.getOutputQuality() : 75)
|
||||
.build();
|
||||
|
||||
// 超时时间,默认30秒
|
||||
long timeoutMs = req.getTimeoutMs() != null ? req.getTimeoutMs() : 30000L;
|
||||
|
||||
// 先创建任务获取 taskId
|
||||
Long taskId = watermarkEdgeTaskCreator.createTask(
|
||||
req.getStyle(),
|
||||
watermarkRequest,
|
||||
req.getRecordId() != null ? req.getRecordId() : 0L,
|
||||
req.getFaceId(),
|
||||
req.getWatermarkType() != null ? req.getWatermarkType() : "test"
|
||||
);
|
||||
|
||||
// 注册等待并等待结果
|
||||
puzzleEdgeRenderTaskService.registerWait(taskId);
|
||||
PuzzleEdgeRenderTaskService.TaskWaitResult result = puzzleEdgeRenderTaskService.waitForTask(taskId, timeoutMs);
|
||||
|
||||
CreateAndWaitResponse response = new CreateAndWaitResponse();
|
||||
response.setTaskId(taskId);
|
||||
response.setSuccess(result.isSuccess());
|
||||
response.setImageUrl(result.getImageUrl());
|
||||
response.setErrorMessage(result.getErrorMessage());
|
||||
|
||||
log.info("测试水印任务完成: style={}, taskId={}, success={}, imageUrl={}",
|
||||
req.getStyle(), taskId, result.isSuccess(), result.getImageUrl());
|
||||
|
||||
return ApiResponse.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建任务请求
|
||||
*/
|
||||
@Data
|
||||
public static class CreateTaskRequest {
|
||||
/**
|
||||
* 水印风格:normal / leica / printer_default
|
||||
*/
|
||||
private String style;
|
||||
|
||||
/**
|
||||
* 原图URL
|
||||
*/
|
||||
private String originalImageUrl;
|
||||
|
||||
/**
|
||||
* 原图宽度
|
||||
*/
|
||||
private int imageWidth;
|
||||
|
||||
/**
|
||||
* 原图高度
|
||||
*/
|
||||
private int imageHeight;
|
||||
|
||||
/**
|
||||
* 二维码URL
|
||||
*/
|
||||
private String qrcodeUrl;
|
||||
|
||||
/**
|
||||
* 头像URL(可选)
|
||||
*/
|
||||
private String faceUrl;
|
||||
|
||||
/**
|
||||
* 景区名称
|
||||
*/
|
||||
private String scenicLine;
|
||||
|
||||
/**
|
||||
* 日期时间行
|
||||
*/
|
||||
private String datetimeLine;
|
||||
|
||||
/**
|
||||
* 四边偏移(像素)
|
||||
*/
|
||||
private Integer offsetTop;
|
||||
private Integer offsetBottom;
|
||||
private Integer offsetLeft;
|
||||
private Integer offsetRight;
|
||||
|
||||
/**
|
||||
* 缩放倍数
|
||||
*/
|
||||
private Double scale;
|
||||
|
||||
/**
|
||||
* 输出格式:PNG / JPEG
|
||||
*/
|
||||
private String outputFormat;
|
||||
|
||||
/**
|
||||
* 输出质量(0-100)
|
||||
*/
|
||||
private Integer outputQuality;
|
||||
|
||||
/**
|
||||
* 关联的拼图记录ID(测试用)
|
||||
*/
|
||||
private Long recordId;
|
||||
|
||||
/**
|
||||
* 人脸ID(可选)
|
||||
*/
|
||||
private Long faceId;
|
||||
|
||||
/**
|
||||
* 水印类型标识
|
||||
*/
|
||||
private String watermarkType;
|
||||
|
||||
/**
|
||||
* 等待超时时间(毫秒),仅用于 createAndWait
|
||||
*/
|
||||
private Long timeoutMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建任务响应
|
||||
*/
|
||||
@Data
|
||||
public static class CreateTaskResponse {
|
||||
private Long taskId;
|
||||
private String message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并等待响应
|
||||
*/
|
||||
@Data
|
||||
public static class CreateAndWaitResponse {
|
||||
private Long taskId;
|
||||
private boolean success;
|
||||
private String imageUrl;
|
||||
private String errorMessage;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user