feat(image): 添加水印缩放功能支持

- 在 WatermarkConfig 中新增 scale 字段用于控制整体缩放倍数
- 在 WatermarkStage 中读取并传递 scale 参数到 WatermarkInfo
- 在 PrinterDefaultWatermarkOperator 中实现所有位置和尺寸的缩放逻辑
- 对偏移量、边距、字体大小、二维码尺寸等应用缩放因子
- 更新图像绘制相关参数计算方式以支持动态缩放
- 优化二维码圆形背景和头像绘制的缩放处理
- 确保缩放后的水印元素保持相对位置和视觉一致性
This commit is contained in:
2025-12-07 21:42:11 +08:00
parent a5fe00052d
commit fef616c837
4 changed files with 53 additions and 20 deletions

View File

@@ -34,4 +34,11 @@ public class WatermarkConfig {
* 二维码文件
*/
private final File qrcodeFile;
/**
* 缩放倍数,用于将所有定位和大小乘以该倍数
* 默认值为 1.0(不缩放)
*/
@Builder.Default
private final Double scale = 1.0;
}

View File

@@ -170,6 +170,12 @@ public class WatermarkStage extends AbstractPipelineStage<PhotoProcessContext> {
info.setQrcodeFile(qrcodeFile);
}
// 从 config 读取缩放倍数
Double scale = config.getScale();
if (scale != null) {
info.setScale(scale);
}
// 根据旋转状态自己处理 offsetLeft
if (context.isRotationApplied()) {
if (context.getImageRotation() == 90) {

View File

@@ -33,6 +33,13 @@ public class WatermarkInfo {
private Integer offsetLeft;
private Integer offsetRight;
/**
* 缩放倍数,用于将所有定位和大小乘以该倍数
* 例如: scale=2.0 表示所有尺寸和位置都放大2倍
* null 表示使用默认值1.0(不缩放)
*/
private Double scale;
public String getDatetimeLine() {
if (datetimeLine == null) {
datetimeLine = DateUtil.format(datetime, dtFormat);

View File

@@ -64,11 +64,14 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
@Override
public File process(WatermarkInfo info) throws ImageWatermarkException {
// 获取四边偏移值,优先使用传入的值,否则使用默认值
int offsetTop = info.getOffsetTop() != null ? info.getOffsetTop() : DEFAULT_OFFSET_TOP;
int offsetBottom = info.getOffsetBottom() != null ? info.getOffsetBottom() : DEFAULT_OFFSET_BOTTOM;
int offsetLeft = info.getOffsetLeft() != null ? info.getOffsetLeft() : DEFAULT_OFFSET_LEFT;
int offsetRight = info.getOffsetRight() != null ? info.getOffsetRight() : DEFAULT_OFFSET_RIGHT;
// 获取缩放倍数,默认为1.0(不缩放)
double scale = info.getScale() != null ? info.getScale() : 1.0;
// 获取四边偏移值,优先使用传入的值,否则使用默认值,并应用缩放
int offsetTop = (int) ((info.getOffsetTop() != null ? info.getOffsetTop() : DEFAULT_OFFSET_TOP) * scale);
int offsetBottom = (int) ((info.getOffsetBottom() != null ? info.getOffsetBottom() : DEFAULT_OFFSET_BOTTOM) * scale);
int offsetLeft = (int) ((info.getOffsetLeft() != null ? info.getOffsetLeft() : DEFAULT_OFFSET_LEFT) * scale);
int offsetRight = (int) ((info.getOffsetRight() != null ? info.getOffsetRight() : DEFAULT_OFFSET_RIGHT) * scale);
BufferedImage baseImage;
BufferedImage qrcodeImage;
@@ -86,17 +89,26 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
} catch (IOException e) {
throw new ImageWatermarkException("图片打开失败");
}
// 应用缩放到所有常量
int scaledExtraBorder = (int) (EXTRA_BORDER_PX * scale);
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);
// 新图像画布
BufferedImage newImage = new BufferedImage(baseImage.getWidth() + 2 * EXTRA_BORDER_PX, baseImage.getHeight() + 2 * EXTRA_BORDER_PX, BufferedImage.TYPE_INT_RGB);
BufferedImage newImage = new BufferedImage(baseImage.getWidth() + 2 * scaledExtraBorder, baseImage.getHeight() + 2 * scaledExtraBorder, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = newImage.createGraphics();
g2d.setColor(BG_COLOR);
g2d.fillRect(0, 0, newImage.getWidth(), newImage.getHeight());
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.drawImage(baseImage, EXTRA_BORDER_PX, EXTRA_BORDER_PX, null);
int newQrcodeHeight = QRCODE_SIZE;
g2d.drawImage(baseImage, scaledExtraBorder, scaledExtraBorder, null);
int newQrcodeHeight = scaledQrcodeSize;
int newQrcodeWidth = (int) (newQrcodeHeight * 1.0 / qrcodeImage.getHeight() * qrcodeImage.getWidth());
Font scenicFont = new Font(defaultFontName, Font.BOLD, SCENIC_FONT_SIZE);
Font datetimeFont = new Font(defaultFontName, Font.BOLD, DATETIME_FONT_SIZE);
Font scenicFont = new Font(defaultFontName, Font.BOLD, scaledScenicFontSize);
Font datetimeFont = new Font(defaultFontName, Font.BOLD, scaledDatetimeFontSize);
FontMetrics scenicFontMetrics = g2d.getFontMetrics(scenicFont);
FontMetrics datetimeFontMetrics = g2d.getFontMetrics(datetimeFont);
int scenicLineHeight = scenicFontMetrics.getHeight();
@@ -106,13 +118,14 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
// 二维码放置在左下角,距离左边缘图片宽度的5%,再加上左侧偏移
int qrcodeOffsetX = (int) (newImage.getWidth() * QRCODE_LEFT_MARGIN_RATIO) + offsetLeft;
int qrcodeOffsetY = EXTRA_BORDER_PX + baseImage.getHeight() - OFFSET_Y - newQrcodeHeight - offsetBottom;
int qrcodeOffsetY = scaledExtraBorder + baseImage.getHeight() - scaledOffsetY - newQrcodeHeight - offsetBottom;
Shape originalClip = g2d.getClip();
// 创建比二维码大10像素的白色圆形背景
int whiteCircleSize = Math.max(newQrcodeWidth, newQrcodeHeight) + 10;
// 创建比二维码大10像素的白色圆形背景(10像素也要缩放)
int whiteCirclePadding = (int) (10 * scale);
int whiteCircleSize = Math.max(newQrcodeWidth, newQrcodeHeight) + whiteCirclePadding;
int whiteCircleX = qrcodeOffsetX - (whiteCircleSize - newQrcodeWidth) / 2;
int whiteCircleY = qrcodeOffsetY + QRCODE_OFFSET_Y - (whiteCircleSize - newQrcodeHeight) / 2;
int whiteCircleY = qrcodeOffsetY + scaledQrcodeOffsetY - (whiteCircleSize - newQrcodeHeight) / 2;
// 绘制白色圆形背景
g2d.setColor(Color.WHITE);
@@ -122,7 +135,7 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
// 用白色圆形尺寸裁切二维码(保持二维码原始尺寸,但用大圆裁切)
Ellipse2D qrcodeCircle = new Ellipse2D.Double(whiteCircleX, whiteCircleY, whiteCircleSize, whiteCircleSize);
g2d.setClip(qrcodeCircle);
g2d.drawImage(qrcodeImage, qrcodeOffsetX, qrcodeOffsetY + QRCODE_OFFSET_Y, newQrcodeWidth, newQrcodeHeight, null);
g2d.drawImage(qrcodeImage, qrcodeOffsetX, qrcodeOffsetY + scaledQrcodeOffsetY, newQrcodeWidth, newQrcodeHeight, null);
g2d.setClip(originalClip);
// 在圆形二维码中央绘制圆形头像
@@ -130,7 +143,7 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
// 计算圆形头像的尺寸和位置
int avatarDiameter = (int) (newQrcodeHeight * 0.45);
int avatarX = qrcodeOffsetX + (newQrcodeWidth - avatarDiameter) / 2;
int avatarY = qrcodeOffsetY + QRCODE_OFFSET_Y + (newQrcodeHeight - avatarDiameter) / 2;
int avatarY = qrcodeOffsetY + scaledQrcodeOffsetY + (newQrcodeHeight - avatarDiameter) / 2;
// 保存当前的渲染设置
RenderingHints originalHints = g2d.getRenderingHints();
@@ -149,10 +162,10 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
double faceHeight = faceImage.getHeight();
double scaleX = avatarDiameter / faceWidth;
double scaleY = avatarDiameter / faceHeight;
double scale = Math.max(scaleX, scaleY); // 使用较大的缩放比例以填满圆形
double faceScale = Math.max(scaleX, scaleY); // 使用较大的缩放比例以填满圆形
int scaledWidth = (int) (faceWidth * scale);
int scaledHeight = (int) (faceHeight * scale);
int scaledWidth = (int) (faceWidth * faceScale);
int scaledHeight = (int) (faceHeight * faceScale);
// 计算居中位置
int faceDrawX = avatarX + (avatarDiameter - scaledWidth) / 2;
@@ -167,7 +180,7 @@ public class PrinterDefaultWatermarkOperator implements IOperator {
}
// 计算文字与二维码垂直居中对齐的Y坐标
int qrcodeTop = qrcodeOffsetY + QRCODE_OFFSET_Y;
int qrcodeTop = qrcodeOffsetY + scaledQrcodeOffsetY;
int qrcodeBottom = qrcodeTop + newQrcodeHeight;
int qrcodeCenter = (qrcodeTop + qrcodeBottom) / 2;