You've already forked FrameTour-BE
feat(watermark): 添加拼图水印模板构建器
- 实现拼图默认水印模板构建器,支持原图区域和底部信息区域布局 - 实现拼图打印水印模板构建器,增加四周白边设计 - 配置二维码、头像、景区名和日期时间的文字布局 - 支持动态数据绑定和图片元素的COVER模式显示 - 提供可选的头像圆形裁剪功能和右对齐文字显示
This commit is contained in:
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拼图默认水印模板构建器
|
||||||
|
*
|
||||||
|
* 布局说明:
|
||||||
|
* - 白色背景
|
||||||
|
* - 顶部90%为原图区域(COVER模式)
|
||||||
|
* - 底部10%为信息区域:
|
||||||
|
* - 左侧(距左5%):二维码(宽高为图片的8%)+ 头像(可选)
|
||||||
|
* - 右侧(距右5%):景区名 + 日期时间(右对齐)
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class PuzzleDefaultWatermarkTemplateBuilder extends AbstractWatermarkTemplateBuilder {
|
||||||
|
|
||||||
|
public static final String STYLE = "puzzle_default";
|
||||||
|
|
||||||
|
// 布局比例配置
|
||||||
|
private static final double IMAGE_HEIGHT_RATIO = 0.90; // 原图占90%高度
|
||||||
|
private static final double MARGIN_X_RATIO = 0.05; // 左右边距为宽度的5%
|
||||||
|
private static final double QRCODE_SIZE_RATIO = 0.08; // 二维码为图片的8%
|
||||||
|
|
||||||
|
// 文字配置
|
||||||
|
private static final int SCENIC_FONT_SIZE = 52;
|
||||||
|
private static final int DATETIME_FONT_SIZE = 42;
|
||||||
|
private static final String SCENIC_COLOR = "#333333";
|
||||||
|
private static final String DATETIME_COLOR = "#999999";
|
||||||
|
|
||||||
|
@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;
|
||||||
|
|
||||||
|
// 原图区域占90%高度,底部信息区占10%高度
|
||||||
|
int originalImageHeight = (int) (imageHeight * IMAGE_HEIGHT_RATIO);
|
||||||
|
int bottomAreaHeight = imageHeight - originalImageHeight;
|
||||||
|
|
||||||
|
// 创建模板(白色背景)
|
||||||
|
PuzzleTemplateEntity template = createTemplateWithColor(
|
||||||
|
"watermark_puzzle_default_" + System.currentTimeMillis(),
|
||||||
|
canvasWidth,
|
||||||
|
canvasHeight,
|
||||||
|
"#FFFFFF"
|
||||||
|
);
|
||||||
|
|
||||||
|
List<PuzzleElementEntity> elements = newElementList();
|
||||||
|
Map<String, String> dynamicData = newDynamicData();
|
||||||
|
|
||||||
|
// 1. 原图元素(顶部90%区域,COVER模式)
|
||||||
|
PuzzleElementEntity originalImageElement = createImageElement(
|
||||||
|
"originalImage", "原图",
|
||||||
|
0, 0,
|
||||||
|
canvasWidth, originalImageHeight, 1,
|
||||||
|
FIT_MODE_COVER, null, null
|
||||||
|
);
|
||||||
|
elements.add(originalImageElement);
|
||||||
|
dynamicData.put("originalImage", request.getOriginalImageUrl());
|
||||||
|
|
||||||
|
// 2. 计算底部区域元素位置
|
||||||
|
int marginX = (int) (canvasWidth * MARGIN_X_RATIO);
|
||||||
|
int qrcodeSize = (int) (canvasHeight * QRCODE_SIZE_RATIO); // 二维码为高度的8%
|
||||||
|
|
||||||
|
// 二维码垂直居中于底部区域
|
||||||
|
int qrcodeX = marginX;
|
||||||
|
int qrcodeY = originalImageHeight + (bottomAreaHeight - qrcodeSize) / 2;
|
||||||
|
|
||||||
|
// 3. 二维码元素
|
||||||
|
PuzzleElementEntity qrcodeElement = createImageElement(
|
||||||
|
"qrcode", "二维码",
|
||||||
|
qrcodeX, qrcodeY,
|
||||||
|
qrcodeSize, qrcodeSize, 10,
|
||||||
|
FIT_MODE_CONTAIN, null, null
|
||||||
|
);
|
||||||
|
elements.add(qrcodeElement);
|
||||||
|
dynamicData.put("qrcode", request.getQrcodeUrl());
|
||||||
|
|
||||||
|
// 4. 头像元素(二维码中央,可选)
|
||||||
|
if (request.getFaceUrl() != null && !request.getFaceUrl().isEmpty()) {
|
||||||
|
int avatarDiameter = (int) (qrcodeSize * 0.45);
|
||||||
|
int avatarX = qrcodeX + (qrcodeSize - avatarDiameter) / 2;
|
||||||
|
int avatarY = qrcodeY + (qrcodeSize - avatarDiameter) / 2;
|
||||||
|
|
||||||
|
PuzzleElementEntity faceElement = createCircleImageElement(
|
||||||
|
"face", "头像",
|
||||||
|
avatarX, avatarY,
|
||||||
|
avatarDiameter, 20
|
||||||
|
);
|
||||||
|
elements.add(faceElement);
|
||||||
|
dynamicData.put("face", request.getFaceUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 计算右侧文字区域
|
||||||
|
int textRightX = canvasWidth - marginX;
|
||||||
|
int textWidth = textRightX - qrcodeX - qrcodeSize - marginX;
|
||||||
|
|
||||||
|
// 文字与二维码垂直居中
|
||||||
|
int totalTextHeight = SCENIC_FONT_SIZE + DATETIME_FONT_SIZE + 5;
|
||||||
|
int textY = originalImageHeight + (bottomAreaHeight - totalTextHeight) / 2;
|
||||||
|
|
||||||
|
// 6. 景区名文字(右对齐)
|
||||||
|
PuzzleElementEntity scenicTextElement = createTextElement(
|
||||||
|
"scenicLine", "景区名",
|
||||||
|
qrcodeX + qrcodeSize + marginX, textY,
|
||||||
|
textWidth, SCENIC_FONT_SIZE + 10, 30,
|
||||||
|
"PingFang SC", SCENIC_FONT_SIZE, SCENIC_COLOR,
|
||||||
|
"NORMAL", TEXT_ALIGN_RIGHT
|
||||||
|
);
|
||||||
|
elements.add(scenicTextElement);
|
||||||
|
dynamicData.put("scenicLine", request.getScenicLine() != null ? request.getScenicLine() : "");
|
||||||
|
|
||||||
|
// 7. 日期时间文字(右对齐)
|
||||||
|
int datetimeY = textY + SCENIC_FONT_SIZE + 5;
|
||||||
|
PuzzleElementEntity datetimeTextElement = createTextElement(
|
||||||
|
"datetimeLine", "日期时间",
|
||||||
|
qrcodeX + qrcodeSize + marginX, datetimeY,
|
||||||
|
textWidth, DATETIME_FONT_SIZE + 10, 30,
|
||||||
|
"PingFang SC", DATETIME_FONT_SIZE, DATETIME_COLOR,
|
||||||
|
"NORMAL", TEXT_ALIGN_RIGHT
|
||||||
|
);
|
||||||
|
elements.add(datetimeTextElement);
|
||||||
|
dynamicData.put("datetimeLine", request.getDatetimeLine() != null ? request.getDatetimeLine() : "");
|
||||||
|
|
||||||
|
return createResult(template, elements, dynamicData);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拼图打印水印模板构建器
|
||||||
|
*
|
||||||
|
* 布局说明:
|
||||||
|
* - 白色背景
|
||||||
|
* - 四周留1%白边
|
||||||
|
* - 内部区域:顶部90%为原图区域(COVER模式)
|
||||||
|
* - 底部10%为信息区域:
|
||||||
|
* - 左侧(距左5%):二维码(宽高为图片的8%)+ 头像(可选)
|
||||||
|
* - 右侧(距右5%):景区名 + 日期时间(右对齐)
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class PuzzlePrintWatermarkTemplateBuilder extends AbstractWatermarkTemplateBuilder {
|
||||||
|
|
||||||
|
public static final String STYLE = "puzzle_print";
|
||||||
|
|
||||||
|
// 布局比例配置
|
||||||
|
private static final double BORDER_RATIO = 0.01; // 四周白边为1%
|
||||||
|
private static final double IMAGE_HEIGHT_RATIO = 0.90; // 原图占内容区90%高度
|
||||||
|
private static final double MARGIN_X_RATIO = 0.05; // 左右边距为宽度的5%
|
||||||
|
private static final double QRCODE_SIZE_RATIO = 0.08; // 二维码为图片的8%
|
||||||
|
|
||||||
|
// 文字配置
|
||||||
|
private static final int SCENIC_FONT_SIZE = 52;
|
||||||
|
private static final int DATETIME_FONT_SIZE = 42;
|
||||||
|
private static final String SCENIC_COLOR = "#333333";
|
||||||
|
private static final String DATETIME_COLOR = "#999999";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getStyle() {
|
||||||
|
return STYLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WatermarkTemplateResult build(WatermarkRequest request) {
|
||||||
|
int imageWidth = request.getImageWidth();
|
||||||
|
int imageHeight = request.getImageHeight();
|
||||||
|
|
||||||
|
// 计算白边尺寸(基于原图尺寸的1%)
|
||||||
|
int borderX = (int) (imageWidth * BORDER_RATIO);
|
||||||
|
int borderY = (int) (imageHeight * BORDER_RATIO);
|
||||||
|
|
||||||
|
// 画布尺寸 = 原图尺寸 + 四周白边
|
||||||
|
int canvasWidth = imageWidth + borderX * 2;
|
||||||
|
int canvasHeight = imageHeight + borderY * 2;
|
||||||
|
|
||||||
|
// 内容区起始位置(白边内)
|
||||||
|
int contentStartX = borderX;
|
||||||
|
int contentStartY = borderY;
|
||||||
|
|
||||||
|
// 内容区尺寸 = 原图尺寸
|
||||||
|
int contentWidth = imageWidth;
|
||||||
|
int contentHeight = imageHeight;
|
||||||
|
|
||||||
|
// 原图区域占90%高度,底部信息区占10%高度
|
||||||
|
int originalImageHeight = (int) (contentHeight * IMAGE_HEIGHT_RATIO);
|
||||||
|
int bottomAreaHeight = contentHeight - originalImageHeight;
|
||||||
|
|
||||||
|
// 创建模板(白色背景)
|
||||||
|
PuzzleTemplateEntity template = createTemplateWithColor(
|
||||||
|
"watermark_puzzle_print_" + System.currentTimeMillis(),
|
||||||
|
canvasWidth,
|
||||||
|
canvasHeight,
|
||||||
|
"#FFFFFF"
|
||||||
|
);
|
||||||
|
|
||||||
|
List<PuzzleElementEntity> elements = newElementList();
|
||||||
|
Map<String, String> dynamicData = newDynamicData();
|
||||||
|
|
||||||
|
// 1. 原图元素(内容区顶部90%,COVER模式)
|
||||||
|
PuzzleElementEntity originalImageElement = createImageElement(
|
||||||
|
"originalImage", "原图",
|
||||||
|
contentStartX, contentStartY,
|
||||||
|
contentWidth, originalImageHeight, 1,
|
||||||
|
FIT_MODE_COVER, null, null
|
||||||
|
);
|
||||||
|
elements.add(originalImageElement);
|
||||||
|
dynamicData.put("originalImage", request.getOriginalImageUrl());
|
||||||
|
|
||||||
|
// 2. 计算底部区域元素位置(相对于内容区)
|
||||||
|
int marginX = (int) (contentWidth * MARGIN_X_RATIO);
|
||||||
|
int qrcodeSize = (int) (contentHeight * QRCODE_SIZE_RATIO); // 二维码为高度的8%
|
||||||
|
|
||||||
|
// 二维码垂直居中于底部区域
|
||||||
|
int qrcodeX = contentStartX + marginX;
|
||||||
|
int qrcodeY = contentStartY + originalImageHeight + (bottomAreaHeight - qrcodeSize) / 2;
|
||||||
|
|
||||||
|
// 3. 二维码元素
|
||||||
|
PuzzleElementEntity qrcodeElement = createImageElement(
|
||||||
|
"qrcode", "二维码",
|
||||||
|
qrcodeX, qrcodeY,
|
||||||
|
qrcodeSize, qrcodeSize, 10,
|
||||||
|
FIT_MODE_CONTAIN, null, null
|
||||||
|
);
|
||||||
|
elements.add(qrcodeElement);
|
||||||
|
dynamicData.put("qrcode", request.getQrcodeUrl());
|
||||||
|
|
||||||
|
// 4. 头像元素(二维码中央,可选)
|
||||||
|
if (request.getFaceUrl() != null && !request.getFaceUrl().isEmpty()) {
|
||||||
|
int avatarDiameter = (int) (qrcodeSize * 0.45);
|
||||||
|
int avatarX = qrcodeX + (qrcodeSize - avatarDiameter) / 2;
|
||||||
|
int avatarY = qrcodeY + (qrcodeSize - avatarDiameter) / 2;
|
||||||
|
|
||||||
|
PuzzleElementEntity faceElement = createCircleImageElement(
|
||||||
|
"face", "头像",
|
||||||
|
avatarX, avatarY,
|
||||||
|
avatarDiameter, 20
|
||||||
|
);
|
||||||
|
elements.add(faceElement);
|
||||||
|
dynamicData.put("face", request.getFaceUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 计算右侧文字区域
|
||||||
|
int textRightX = contentStartX + contentWidth - marginX;
|
||||||
|
int textWidth = textRightX - qrcodeX - qrcodeSize - marginX;
|
||||||
|
|
||||||
|
// 文字与二维码垂直居中
|
||||||
|
int totalTextHeight = SCENIC_FONT_SIZE + DATETIME_FONT_SIZE + 5;
|
||||||
|
int textY = contentStartY + originalImageHeight + (bottomAreaHeight - totalTextHeight) / 2;
|
||||||
|
|
||||||
|
// 6. 景区名文字(右对齐)
|
||||||
|
PuzzleElementEntity scenicTextElement = createTextElement(
|
||||||
|
"scenicLine", "景区名",
|
||||||
|
qrcodeX + qrcodeSize + marginX, textY,
|
||||||
|
textWidth, SCENIC_FONT_SIZE + 10, 30,
|
||||||
|
"PingFang SC", SCENIC_FONT_SIZE, SCENIC_COLOR,
|
||||||
|
"NORMAL", TEXT_ALIGN_RIGHT
|
||||||
|
);
|
||||||
|
elements.add(scenicTextElement);
|
||||||
|
dynamicData.put("scenicLine", request.getScenicLine() != null ? request.getScenicLine() : "");
|
||||||
|
|
||||||
|
// 7. 日期时间文字(右对齐)
|
||||||
|
int datetimeY = textY + SCENIC_FONT_SIZE + 5;
|
||||||
|
PuzzleElementEntity datetimeTextElement = createTextElement(
|
||||||
|
"datetimeLine", "日期时间",
|
||||||
|
qrcodeX + qrcodeSize + marginX, datetimeY,
|
||||||
|
textWidth, DATETIME_FONT_SIZE + 10, 30,
|
||||||
|
"PingFang SC", DATETIME_FONT_SIZE, DATETIME_COLOR,
|
||||||
|
"NORMAL", TEXT_ALIGN_RIGHT
|
||||||
|
);
|
||||||
|
elements.add(datetimeTextElement);
|
||||||
|
dynamicData.put("datetimeLine", request.getDatetimeLine() != null ? request.getDatetimeLine() : "");
|
||||||
|
|
||||||
|
return createResult(template, elements, dynamicData);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -727,7 +727,7 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
|
|
||||||
String dt = DateUtil.format(face.getCreateAt(), "yyyyMMdd");
|
String dt = DateUtil.format(face.getCreateAt(), "yyyyMMdd");
|
||||||
String path = "pages/videoSynthesis/bind_face";
|
String path = "pages/videoSynthesis/bind_face";
|
||||||
String filePath = "wxa_face/"+dt+"/f" + faceId + ".jpg";
|
String filePath = "f" + faceId + ".jpg";
|
||||||
IStorageAdapter adapter = StorageFactory.use();
|
IStorageAdapter adapter = StorageFactory.use();
|
||||||
if (adapter.isExists(filePath)) {
|
if (adapter.isExists(filePath)) {
|
||||||
String url = adapter.getUrl(filePath);
|
String url = adapter.getUrl(filePath);
|
||||||
@@ -743,13 +743,13 @@ public class FaceServiceImpl implements FaceService {
|
|||||||
try {
|
try {
|
||||||
File file = new File(filePath);
|
File file = new File(filePath);
|
||||||
WxMpUtil.generateUnlimitedWXAQRCode(appId, appSecret, path, faceId.toString(), file);
|
WxMpUtil.generateUnlimitedWXAQRCode(appId, appSecret, path, faceId.toString(), file);
|
||||||
String url = adapter.uploadFile(null, file, filePath);
|
String url = adapter.uploadFile(null, file, "wxa_face", dt, filePath);
|
||||||
file.delete();
|
file.delete();
|
||||||
adapter.setAcl(StorageAcl.PUBLIC_READ, filePath);
|
adapter.setAcl(StorageAcl.PUBLIC_READ, filePath);
|
||||||
url = url.replace("-internal.aliyuncs.com", ".aliyuncs.com");
|
url = url.replace("-internal.aliyuncs.com", ".aliyuncs.com");
|
||||||
return url;
|
return url;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new BaseException("生成二维码失败");
|
throw new BaseException("生成二维码失败:"+e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user