diff --git a/pom.xml b/pom.xml
index 33b07a2..ca3fcea 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,6 +24,7 @@
1.2.83
2.0.7
5.3.1
+ 9.0.102
true
@@ -198,6 +199,17 @@
+
+
+
+ src/main/resources
+
+ **/*.png
+ **/*.ttf
+ **/*
+
+
+
org.springframework.boot
diff --git a/src/main/java/com/ycwl/basic/image/util/ImageUtil.java b/src/main/java/com/ycwl/basic/image/util/ImageUtil.java
new file mode 100644
index 0000000..deecb2c
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/image/util/ImageUtil.java
@@ -0,0 +1,4 @@
+package com.ycwl.basic.image.util;
+
+public class ImageUtil {
+}
diff --git a/src/main/java/com/ycwl/basic/image/watermark/ImageWatermarkFactory.java b/src/main/java/com/ycwl/basic/image/watermark/ImageWatermarkFactory.java
new file mode 100644
index 0000000..5e456fb
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/image/watermark/ImageWatermarkFactory.java
@@ -0,0 +1,24 @@
+package com.ycwl.basic.image.watermark;
+
+import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum;
+import com.ycwl.basic.image.watermark.exception.ImageWatermarkUnsupportedException;
+import com.ycwl.basic.image.watermark.operator.IOperator;
+import com.ycwl.basic.image.watermark.operator.LeicaWatermarkOperator;
+import com.ycwl.basic.image.watermark.operator.NormalWatermarkOperator;
+
+public class ImageWatermarkFactory {
+ public static IOperator get(String watermarkType) {
+ ImageWatermarkOperatorEnum type = ImageWatermarkOperatorEnum.getByCode(watermarkType);
+ if (type == null) {
+ throw new ImageWatermarkUnsupportedException(watermarkType);
+ }
+ switch (type) {
+ case NORMAL:
+ return new NormalWatermarkOperator();
+ case LEICA:
+ return new LeicaWatermarkOperator();
+ default:
+ throw new ImageWatermarkUnsupportedException(watermarkType);
+ }
+ }
+}
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
new file mode 100644
index 0000000..9782e38
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/image/watermark/entity/WatermarkInfo.java
@@ -0,0 +1,35 @@
+package com.ycwl.basic.image.watermark.entity;
+
+import cn.hutool.core.date.DateUtil;
+import lombok.Data;
+
+import java.io.File;
+import java.util.Date;
+
+@Data
+public class WatermarkInfo {
+ private File originalFile;
+ private File watermarkedFile;
+ private File qrcodeFile;
+ private String scenicLine;
+ private String secondLine;
+ private String thirdLine;
+ private String fourthLine;
+ private Date datetime;
+ private String dtFormat;
+ private String datetimeLine;
+
+ public String getDatetimeLine() {
+ if (datetimeLine == null) {
+ datetimeLine = DateUtil.format(datetime, dtFormat);
+ }
+ return datetimeLine;
+ }
+
+ public String getScenicLine() {
+ if (scenicLine == null) {
+ scenicLine = "";
+ }
+ return scenicLine;
+ }
+}
diff --git a/src/main/java/com/ycwl/basic/image/watermark/enums/ImageWatermarkOperatorEnum.java b/src/main/java/com/ycwl/basic/image/watermark/enums/ImageWatermarkOperatorEnum.java
new file mode 100644
index 0000000..447c0a7
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/image/watermark/enums/ImageWatermarkOperatorEnum.java
@@ -0,0 +1,21 @@
+package com.ycwl.basic.image.watermark.enums;
+
+public enum ImageWatermarkOperatorEnum {
+ LEICA("leica"),
+ NORMAL("normal");
+
+ private final String type;
+
+ ImageWatermarkOperatorEnum(String type) {
+ this.type = type;
+ }
+
+ public static ImageWatermarkOperatorEnum getByCode(String type) {
+ for (ImageWatermarkOperatorEnum imageWatermarkOperatorEnum : ImageWatermarkOperatorEnum.values()) {
+ if (imageWatermarkOperatorEnum.type.equals(type)) {
+ return imageWatermarkOperatorEnum;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/ycwl/basic/image/watermark/exception/ImageWatermarkException.java b/src/main/java/com/ycwl/basic/image/watermark/exception/ImageWatermarkException.java
new file mode 100644
index 0000000..2cbcc97
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/image/watermark/exception/ImageWatermarkException.java
@@ -0,0 +1,7 @@
+package com.ycwl.basic.image.watermark.exception;
+
+public class ImageWatermarkException extends RuntimeException {
+ public ImageWatermarkException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/ycwl/basic/image/watermark/exception/ImageWatermarkUnsupportedException.java b/src/main/java/com/ycwl/basic/image/watermark/exception/ImageWatermarkUnsupportedException.java
new file mode 100644
index 0000000..f4e74cc
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/image/watermark/exception/ImageWatermarkUnsupportedException.java
@@ -0,0 +1,7 @@
+package com.ycwl.basic.image.watermark.exception;
+
+public class ImageWatermarkUnsupportedException extends ImageWatermarkException {
+ public ImageWatermarkUnsupportedException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/ycwl/basic/image/watermark/operator/IOperator.java b/src/main/java/com/ycwl/basic/image/watermark/operator/IOperator.java
new file mode 100644
index 0000000..2e82440
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/image/watermark/operator/IOperator.java
@@ -0,0 +1,10 @@
+package com.ycwl.basic.image.watermark.operator;
+
+import com.ycwl.basic.image.watermark.entity.WatermarkInfo;
+import com.ycwl.basic.image.watermark.exception.ImageWatermarkException;
+
+import java.io.File;
+
+public interface IOperator {
+ File process(WatermarkInfo info) throws ImageWatermarkException;
+}
diff --git a/src/main/java/com/ycwl/basic/image/watermark/operator/LeicaWatermarkOperator.java b/src/main/java/com/ycwl/basic/image/watermark/operator/LeicaWatermarkOperator.java
new file mode 100644
index 0000000..962eafc
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/image/watermark/operator/LeicaWatermarkOperator.java
@@ -0,0 +1,122 @@
+package com.ycwl.basic.image.watermark.operator;
+
+import com.ycwl.basic.image.watermark.entity.WatermarkInfo;
+import com.ycwl.basic.image.watermark.exception.ImageWatermarkException;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * 徕卡水印
+ */
+@Slf4j
+public class LeicaWatermarkOperator implements IOperator {
+ private static final String FONT_PATH = "/PingFang_SC.ttf";
+ public static String defaultFontName;
+ public static float FONT_GLOBAL_OFFSET_PERCENT = 0;
+ static {
+ try {
+ // 加载字体文件流
+ InputStream fontStream = LeicaWatermarkOperator.class.getResourceAsStream(FONT_PATH);
+ if (fontStream == null) {
+ throw new RuntimeException("字体文件未找到!路径:" + FONT_PATH);
+ }
+
+ // 创建字体对象
+ Font customFont = Font.createFont(Font.TRUETYPE_FONT, fontStream);
+
+ // 注册字体到系统
+ GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
+ ge.registerFont(customFont);
+
+ // 更新默认字体名称为新字体的逻辑名称
+ defaultFontName = customFont.getName(); // 如 "PingFang SC"
+ FONT_GLOBAL_OFFSET_PERCENT = -0.3f;
+ } catch (FontFormatException | IOException e) {
+ log.error("加载字体文件失败", e);
+ defaultFontName = "宋体";
+ }
+ }
+ public static int EXTRA_BOTTOM_PX = 140;
+ public static int EXTRA_BORDER_PX = 0;
+ public static Color BG_COLOR = Color.WHITE;
+ public static int LOGO_SIZE = 50;
+ public static int LOGO_EXTRA_BORDER = 10;
+ public static int LOGO_FONT_SIZE = 38;
+ public static Color logoTextColor = new Color(0x33, 0x33, 0x33);
+ public static int QRCODE_SIZE = 80;
+ public static int QRCODE_OFFSET_X = 5;
+ public static int OFFSET_X = 80;
+ public static int OFFSET_Y = 30;
+ public static int SCENIC_FONT_SIZE = 32;
+ public static Color scenicColor = new Color(0x33, 0x33, 0x33);
+ public static int DATETIME_FONT_SIZE = 28;
+ public static Color datetimeColor = new Color(0x99, 0x99, 0x99);
+
+ @Override
+ public File process(WatermarkInfo info) throws ImageWatermarkException {
+ BufferedImage baseImage;
+ BufferedImage qrcodeImage;
+ BufferedImage logoImage;
+ // 从类路径加载 zt-logo.png
+ InputStream logoInputStream = getClass().getResourceAsStream("/zt-logo.png");
+ if (logoInputStream == null) {
+ throw new ImageWatermarkException("无法找到 zt-logo.png 资源文件");
+ }
+ try {
+ baseImage = ImageIO.read(info.getOriginalFile());
+ qrcodeImage = ImageIO.read(info.getQrcodeFile());
+ logoImage = ImageIO.read(logoInputStream);
+ logoInputStream.close();
+ } catch (IOException e) {
+ throw new ImageWatermarkException("图片打开失败");
+ }
+ // 新图像画布
+ BufferedImage newImage = new BufferedImage(baseImage.getWidth() + 2 * EXTRA_BORDER_PX, baseImage.getHeight() + 2 * EXTRA_BORDER_PX + EXTRA_BOTTOM_PX, 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 logoHeight = LOGO_SIZE;
+ int logoWidth = (int) (logoHeight * 1.0 / logoImage.getHeight() * logoImage.getWidth());
+ g2d.drawImage(logoImage, EXTRA_BORDER_PX + OFFSET_X, EXTRA_BORDER_PX + baseImage.getHeight() + OFFSET_Y + LOGO_EXTRA_BORDER, logoWidth, logoHeight, null);
+ Font logoTextFont = new Font(defaultFontName, Font.PLAIN, LOGO_FONT_SIZE);
+ g2d.setFont(logoTextFont);
+ g2d.setColor(logoTextColor);
+ FontMetrics logoFontMetrics = g2d.getFontMetrics(logoTextFont);
+ int logoTextHeight = logoFontMetrics.getHeight();
+ int logoTextOffsetY = (LOGO_SIZE - logoHeight) / 2;
+ g2d.drawString("帧途", EXTRA_BORDER_PX + OFFSET_X + logoWidth + 5, EXTRA_BORDER_PX + baseImage.getHeight() + OFFSET_Y + logoTextHeight + LOGO_EXTRA_BORDER + logoTextOffsetY + logoTextHeight * FONT_GLOBAL_OFFSET_PERCENT);
+ int newQrcodeHeight = QRCODE_SIZE;
+ int newQrcodeWidth = (int) (newQrcodeHeight * 1.0 / qrcodeImage.getHeight() * qrcodeImage.getWidth());
+ Font scenicFont = new Font(defaultFontName, Font.PLAIN, SCENIC_FONT_SIZE);
+ Font datetimeFont = new Font(defaultFontName, Font.PLAIN, DATETIME_FONT_SIZE);
+ FontMetrics scenicFontMetrics = g2d.getFontMetrics(scenicFont);
+ FontMetrics datetimeFontMetrics = g2d.getFontMetrics(datetimeFont);
+ int scenicLineHeight = scenicFontMetrics.getHeight();
+ int dtLineHeight = datetimeFontMetrics.getHeight();
+ int scenicLineWidth = scenicFontMetrics.stringWidth(info.getScenicLine());
+ int datetimeLineWidth = scenicFontMetrics.stringWidth(info.getDatetimeLine());
+ g2d.drawImage(qrcodeImage, newImage.getWidth() + EXTRA_BORDER_PX - OFFSET_X - newQrcodeWidth - QRCODE_OFFSET_X - Math.max(scenicLineWidth, datetimeLineWidth), EXTRA_BORDER_PX + baseImage.getHeight() + + OFFSET_Y, newQrcodeWidth, newQrcodeHeight, null);
+ g2d.setFont(scenicFont);
+ g2d.setColor(scenicColor);
+ g2d.drawString(info.getScenicLine(), newImage.getWidth() + EXTRA_BORDER_PX - OFFSET_X - Math.max(scenicLineWidth, datetimeLineWidth), EXTRA_BORDER_PX + baseImage.getHeight() + + OFFSET_Y + scenicLineHeight + scenicLineHeight * FONT_GLOBAL_OFFSET_PERCENT);
+ g2d.setFont(datetimeFont);
+ g2d.setColor(datetimeColor);
+ g2d.drawString(info.getDatetimeLine(), newImage.getWidth() + EXTRA_BORDER_PX - OFFSET_X - Math.max(scenicLineWidth, datetimeLineWidth), EXTRA_BORDER_PX + baseImage.getHeight() + + OFFSET_Y + scenicLineHeight + dtLineHeight + dtLineHeight * FONT_GLOBAL_OFFSET_PERCENT);
+ try {
+ ImageIO.write(newImage, "png", info.getWatermarkedFile());
+ } catch (IOException e) {
+ throw new ImageWatermarkException("图片保存失败");
+ } finally {
+ g2d.dispose();
+ }
+ return info.getWatermarkedFile();
+ }
+}
diff --git a/src/main/java/com/ycwl/basic/image/watermark/operator/NormalWatermarkOperator.java b/src/main/java/com/ycwl/basic/image/watermark/operator/NormalWatermarkOperator.java
new file mode 100644
index 0000000..36bdf85
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/image/watermark/operator/NormalWatermarkOperator.java
@@ -0,0 +1,98 @@
+package com.ycwl.basic.image.watermark.operator;
+
+import com.ycwl.basic.image.watermark.entity.WatermarkInfo;
+import com.ycwl.basic.image.watermark.exception.ImageWatermarkException;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+@Slf4j
+public class NormalWatermarkOperator implements IOperator {
+ private static final String FONT_PATH = "/PingFang_SC.ttf";
+ public static String defaultFontName;
+ public static float FONT_GLOBAL_OFFSET_PERCENT = 0;
+ static {
+ try {
+ // 加载字体文件流
+ InputStream fontStream = LeicaWatermarkOperator.class.getResourceAsStream(FONT_PATH);
+ if (fontStream == null) {
+ throw new RuntimeException("字体文件未找到!路径:" + FONT_PATH);
+ }
+
+ // 创建字体对象
+ Font customFont = Font.createFont(Font.TRUETYPE_FONT, fontStream);
+
+ // 注册字体到系统
+ GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
+ ge.registerFont(customFont);
+
+ // 更新默认字体名称为新字体的逻辑名称
+ defaultFontName = customFont.getName(); // 如 "PingFang SC"
+ FONT_GLOBAL_OFFSET_PERCENT = -0.3f;
+ } catch (FontFormatException | IOException e) {
+ log.error("加载字体文件失败", e);
+ defaultFontName = "宋体";
+ }
+ }
+ public static int EXTRA_BORDER_PX = 0;
+ public static int OFFSET_Y = 90;
+ public static Color BG_COLOR = Color.WHITE;
+ public static int QRCODE_SIZE = 100;
+ public static int QRCODE_OFFSET_X = 10;
+
+ public static int SCENIC_FONT_SIZE = 42;
+ public static Color scenicColor = Color.white;
+ public static int DATETIME_FONT_SIZE = 42;
+ public static Color datetimeColor = Color.white;
+
+ @Override
+ public File process(WatermarkInfo info) throws ImageWatermarkException {
+ BufferedImage baseImage;
+ BufferedImage qrcodeImage;
+ try {
+ baseImage = ImageIO.read(info.getOriginalFile());
+ qrcodeImage = ImageIO.read(info.getQrcodeFile());
+ } catch (IOException e) {
+ throw new ImageWatermarkException("图片打开失败");
+ }
+ // 新图像画布
+ BufferedImage newImage = new BufferedImage(baseImage.getWidth() + 2 * EXTRA_BORDER_PX, baseImage.getHeight() + 2 * EXTRA_BORDER_PX, 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;
+ int newQrcodeWidth = (int) (newQrcodeHeight * 1.0 / qrcodeImage.getHeight() * qrcodeImage.getWidth());
+ Font scenicFont = new Font(defaultFontName, Font.PLAIN, SCENIC_FONT_SIZE);
+ Font datetimeFont = new Font(defaultFontName, Font.PLAIN, DATETIME_FONT_SIZE);
+ FontMetrics scenicFontMetrics = g2d.getFontMetrics(scenicFont);
+ FontMetrics datetimeFontMetrics = g2d.getFontMetrics(datetimeFont);
+ int scenicLineHeight = scenicFontMetrics.getHeight();
+ int dtLineHeight = datetimeFontMetrics.getHeight();
+ int scenicLineWidth = scenicFontMetrics.stringWidth(info.getScenicLine());
+ int datetimeLineWidth = scenicFontMetrics.stringWidth(info.getDatetimeLine());
+ int offsetX = (newImage.getWidth() - newQrcodeWidth - QRCODE_OFFSET_X - Math.max(scenicLineWidth, datetimeLineWidth)) / 2;
+ int offsetY = EXTRA_BORDER_PX + baseImage.getHeight() - OFFSET_Y - newQrcodeHeight;
+ g2d.drawImage(qrcodeImage, offsetX, offsetY, newQrcodeWidth, newQrcodeHeight, null);
+ g2d.setFont(scenicFont);
+ g2d.setColor(scenicColor);
+ g2d.drawString(info.getScenicLine(), offsetX + newQrcodeWidth + QRCODE_OFFSET_X, offsetY + scenicLineHeight + FONT_GLOBAL_OFFSET_PERCENT * scenicLineHeight);
+ g2d.setFont(datetimeFont);
+ g2d.setColor(datetimeColor);
+ g2d.drawString(info.getDatetimeLine(), offsetX + newQrcodeWidth + QRCODE_OFFSET_X, offsetY + scenicLineHeight + dtLineHeight + FONT_GLOBAL_OFFSET_PERCENT * dtLineHeight);
+ try {
+ ImageIO.write(newImage, "png", info.getWatermarkedFile());
+ } catch (IOException e) {
+ throw new ImageWatermarkException("图片保存失败");
+ } finally {
+ g2d.dispose();
+ }
+ return info.getWatermarkedFile();
+ }
+}
diff --git a/src/main/resources/PingFang_SC.ttf b/src/main/resources/PingFang_SC.ttf
new file mode 100644
index 0000000..c06388b
Binary files /dev/null and b/src/main/resources/PingFang_SC.ttf differ
diff --git a/src/main/resources/zt-logo.png b/src/main/resources/zt-logo.png
new file mode 100644
index 0000000..6f1d10a
Binary files /dev/null and b/src/main/resources/zt-logo.png differ