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