画画的

This commit is contained in:
Jerry Yan 2025-03-17 12:22:06 +08:00
parent 072a1a6131
commit 0ae47d3f71
12 changed files with 340 additions and 0 deletions

12
pom.xml

@ -24,6 +24,7 @@
<fastjson.version>1.2.83</fastjson.version>
<knife4j-spring-boot-starter.version>2.0.7</knife4j-spring-boot-starter.version>
<pagehelper.version>5.3.1</pagehelper.version>
<tomcat.version>9.0.102</tomcat.version>
<!--跳过单元测试-->
<skipTests>true</skipTests>
</properties>
@ -198,6 +199,17 @@
</dependencies>
<build>
<resources>
<!-- 确保 src/main/resources 下的资源文件被包含 -->
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.png</include> <!-- 明确包含 PNG 文件 -->
<include>**/*.ttf</include> <!-- 明确包含 TTF 文件 -->
<include>**/*</include> <!-- 包含其他所有资源文件 -->
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>

@ -0,0 +1,4 @@
package com.ycwl.basic.image.util;
public class ImageUtil {
}

@ -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);
}
}
}

@ -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;
}
}

@ -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;
}
}

@ -0,0 +1,7 @@
package com.ycwl.basic.image.watermark.exception;
public class ImageWatermarkException extends RuntimeException {
public ImageWatermarkException(String message) {
super(message);
}
}

@ -0,0 +1,7 @@
package com.ycwl.basic.image.watermark.exception;
public class ImageWatermarkUnsupportedException extends ImageWatermarkException {
public ImageWatermarkUnsupportedException(String message) {
super(message);
}
}

@ -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;
}

@ -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();
}
}

@ -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();
}
}

Binary file not shown.

Binary file not shown.

After

(image error) Size: 39 KiB