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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user