From fef616c837ba5c8e2e0be09ad0d9ca981eea63b0 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sun, 7 Dec 2025 21:42:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(image):=20=E6=B7=BB=E5=8A=A0=E6=B0=B4?= =?UTF-8?q?=E5=8D=B0=E7=BC=A9=E6=94=BE=E5=8A=9F=E8=83=BD=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 WatermarkConfig 中新增 scale 字段用于控制整体缩放倍数 - 在 WatermarkStage 中读取并传递 scale 参数到 WatermarkInfo - 在 PrinterDefaultWatermarkOperator 中实现所有位置和尺寸的缩放逻辑 - 对偏移量、边距、字体大小、二维码尺寸等应用缩放因子 - 更新图像绘制相关参数计算方式以支持动态缩放 - 优化二维码圆形背景和头像绘制的缩放处理 - 确保缩放后的水印元素保持相对位置和视觉一致性 --- .../pipeline/stages/WatermarkConfig.java | 7 +++ .../image/pipeline/stages/WatermarkStage.java | 6 +++ .../image/watermark/entity/WatermarkInfo.java | 7 +++ .../PrinterDefaultWatermarkOperator.java | 53 ++++++++++++------- 4 files changed, 53 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/ycwl/basic/image/pipeline/stages/WatermarkConfig.java b/src/main/java/com/ycwl/basic/image/pipeline/stages/WatermarkConfig.java index 5ea279d5..fb987912 100644 --- a/src/main/java/com/ycwl/basic/image/pipeline/stages/WatermarkConfig.java +++ b/src/main/java/com/ycwl/basic/image/pipeline/stages/WatermarkConfig.java @@ -34,4 +34,11 @@ public class WatermarkConfig { * 二维码文件 */ private final File qrcodeFile; + + /** + * 缩放倍数,用于将所有定位和大小乘以该倍数 + * 默认值为 1.0(不缩放) + */ + @Builder.Default + private final Double scale = 1.0; } diff --git a/src/main/java/com/ycwl/basic/image/pipeline/stages/WatermarkStage.java b/src/main/java/com/ycwl/basic/image/pipeline/stages/WatermarkStage.java index 3ef6ce8e..104f17d5 100644 --- a/src/main/java/com/ycwl/basic/image/pipeline/stages/WatermarkStage.java +++ b/src/main/java/com/ycwl/basic/image/pipeline/stages/WatermarkStage.java @@ -170,6 +170,12 @@ public class WatermarkStage extends AbstractPipelineStage { info.setQrcodeFile(qrcodeFile); } + // 从 config 读取缩放倍数 + Double scale = config.getScale(); + if (scale != null) { + info.setScale(scale); + } + // 根据旋转状态自己处理 offsetLeft if (context.isRotationApplied()) { if (context.getImageRotation() == 90) { diff --git a/src/main/java/com/ycwl/basic/image/watermark/entity/WatermarkInfo.java b/src/main/java/com/ycwl/basic/image/watermark/entity/WatermarkInfo.java index 6addf89b..4cabb67a 100644 --- a/src/main/java/com/ycwl/basic/image/watermark/entity/WatermarkInfo.java +++ b/src/main/java/com/ycwl/basic/image/watermark/entity/WatermarkInfo.java @@ -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); diff --git a/src/main/java/com/ycwl/basic/image/watermark/operator/PrinterDefaultWatermarkOperator.java b/src/main/java/com/ycwl/basic/image/watermark/operator/PrinterDefaultWatermarkOperator.java index 6df51f55..c6e0d13a 100644 --- a/src/main/java/com/ycwl/basic/image/watermark/operator/PrinterDefaultWatermarkOperator.java +++ b/src/main/java/com/ycwl/basic/image/watermark/operator/PrinterDefaultWatermarkOperator.java @@ -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;