You've already forked FrameTour-BE
feat(image): 添加打印机默认水印操作器并优化图片处理逻辑- 新增 PrinterDefaultWatermarkOperator 实现自定义水印处理
- 在 ImageWatermarkOperatorEnum 中添加 PRINTER_DEFAULT 类型 - 更新 ImageWatermarkFactory 以支持新的水印操作器 - 调整日期格式为 yyyy.MM.dd 用于打印场景 -优化 ImageUtils 中的图片旋转逻辑,仅支持270度旋转 - 移除对90度旋转的支持以简化处理流程
This commit is contained in:
@@ -6,6 +6,7 @@ import com.ycwl.basic.image.watermark.operator.IOperator;
|
||||
import com.ycwl.basic.image.watermark.operator.DefaultImageWatermarkOperator;
|
||||
import com.ycwl.basic.image.watermark.operator.LeicaWatermarkOperator;
|
||||
import com.ycwl.basic.image.watermark.operator.NormalWatermarkOperator;
|
||||
import com.ycwl.basic.image.watermark.operator.PrinterDefaultWatermarkOperator;
|
||||
|
||||
public class ImageWatermarkFactory {
|
||||
public static IOperator get(String watermarkType) {
|
||||
@@ -20,6 +21,7 @@ public class ImageWatermarkFactory {
|
||||
case WATERMARK -> new DefaultImageWatermarkOperator();
|
||||
case NORMAL -> new NormalWatermarkOperator();
|
||||
case LEICA -> new LeicaWatermarkOperator();
|
||||
case PRINTER_DEFAULT -> new PrinterDefaultWatermarkOperator();
|
||||
default -> throw new ImageWatermarkUnsupportedException("不支持的类型" + type.name());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import lombok.Getter;
|
||||
public enum ImageWatermarkOperatorEnum {
|
||||
WATERMARK("defW", "jpg"),
|
||||
LEICA("leica", "png"),
|
||||
NORMAL("normal", "png");
|
||||
NORMAL("normal", "png"),
|
||||
PRINTER_DEFAULT("pDefault", "png");
|
||||
|
||||
private final String type;
|
||||
private final String preferFileType;
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
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 javax.imageio.ImageWriteParam;
|
||||
import javax.imageio.ImageWriter;
|
||||
import javax.imageio.stream.ImageOutputStream;
|
||||
import java.awt.*;
|
||||
import java.awt.geom.Ellipse2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
@Slf4j
|
||||
public class PrinterDefaultWatermarkOperator 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 = 20;
|
||||
public static Color BG_COLOR = Color.WHITE;
|
||||
public static int QRCODE_SIZE = 150;
|
||||
public static double QRCODE_LEFT_MARGIN_RATIO = 0.07; // 二维码距离左边缘的图片宽度比例
|
||||
public static int QRCODE_OFFSET_Y = -20;
|
||||
|
||||
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;
|
||||
public static double TEXT_RIGHT_MARGIN_RATIO = 0.05; // 文字距离右边缘的图片宽度比例
|
||||
|
||||
@Override
|
||||
public File process(WatermarkInfo info) throws ImageWatermarkException {
|
||||
BufferedImage baseImage;
|
||||
BufferedImage qrcodeImage;
|
||||
BufferedImage faceImage = null;
|
||||
try {
|
||||
baseImage = ImageIO.read(info.getOriginalFile());
|
||||
qrcodeImage = ImageIO.read(info.getQrcodeFile());
|
||||
if (info.getFaceFile() != null && info.getFaceFile().isFile()) {
|
||||
try {
|
||||
faceImage = ImageIO.read(info.getFaceFile());
|
||||
} catch (IOException e) {
|
||||
log.warn("头像文件读取失败", e);
|
||||
}
|
||||
}
|
||||
} 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 = datetimeFontMetrics.stringWidth(info.getDatetimeLine());
|
||||
|
||||
// 二维码放置在左下角,距离左边缘图片宽度的5%
|
||||
int qrcodeOffsetX = (int) (newImage.getWidth() * QRCODE_LEFT_MARGIN_RATIO);
|
||||
int qrcodeOffsetY = EXTRA_BORDER_PX + baseImage.getHeight() - OFFSET_Y - newQrcodeHeight;
|
||||
Shape originalClip = g2d.getClip();
|
||||
|
||||
// 创建比二维码大10像素的白色圆形背景
|
||||
int whiteCircleSize = Math.max(newQrcodeWidth, newQrcodeHeight) + 10;
|
||||
int whiteCircleX = qrcodeOffsetX - (whiteCircleSize - newQrcodeWidth) / 2;
|
||||
int whiteCircleY = qrcodeOffsetY + QRCODE_OFFSET_Y - (whiteCircleSize - newQrcodeHeight) / 2;
|
||||
|
||||
// 绘制白色圆形背景
|
||||
g2d.setColor(Color.WHITE);
|
||||
Ellipse2D whiteCircle = new Ellipse2D.Double(whiteCircleX, whiteCircleY, whiteCircleSize, whiteCircleSize);
|
||||
g2d.fill(whiteCircle);
|
||||
|
||||
// 用白色圆形尺寸裁切二维码(保持二维码原始尺寸,但用大圆裁切)
|
||||
Ellipse2D qrcodeCircle = new Ellipse2D.Double(whiteCircleX, whiteCircleY, whiteCircleSize, whiteCircleSize);
|
||||
g2d.setClip(qrcodeCircle);
|
||||
g2d.drawImage(qrcodeImage, qrcodeOffsetX, qrcodeOffsetY + QRCODE_OFFSET_Y, newQrcodeWidth, newQrcodeHeight, null);
|
||||
g2d.setClip(originalClip);
|
||||
|
||||
// 在圆形二维码中央绘制圆形头像
|
||||
if (faceImage != null) {
|
||||
// 计算圆形头像的尺寸和位置
|
||||
int avatarDiameter = (int) (newQrcodeHeight * 0.45);
|
||||
int avatarX = qrcodeOffsetX + (newQrcodeWidth - avatarDiameter) / 2;
|
||||
int avatarY = qrcodeOffsetY + QRCODE_OFFSET_Y + (newQrcodeHeight - avatarDiameter) / 2;
|
||||
|
||||
// 保存当前的渲染设置
|
||||
RenderingHints originalHints = g2d.getRenderingHints();
|
||||
|
||||
// 设置高质量渲染
|
||||
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
// 创建圆形剪切区域
|
||||
Ellipse2D avatarCircle = new Ellipse2D.Double(avatarX, avatarY, avatarDiameter, avatarDiameter);
|
||||
g2d.setClip(avatarCircle);
|
||||
|
||||
// 实现CSS cover效果的缩放逻辑
|
||||
double faceWidth = faceImage.getWidth();
|
||||
double faceHeight = faceImage.getHeight();
|
||||
double scaleX = avatarDiameter / faceWidth;
|
||||
double scaleY = avatarDiameter / faceHeight;
|
||||
double scale = Math.max(scaleX, scaleY); // 使用较大的缩放比例以填满圆形
|
||||
|
||||
int scaledWidth = (int) (faceWidth * scale);
|
||||
int scaledHeight = (int) (faceHeight * scale);
|
||||
|
||||
// 计算居中位置
|
||||
int faceDrawX = avatarX + (avatarDiameter - scaledWidth) / 2;
|
||||
int faceDrawY = avatarY + (avatarDiameter - scaledHeight) / 2;
|
||||
|
||||
// 绘制缩放后的头像
|
||||
g2d.drawImage(faceImage, faceDrawX, faceDrawY, scaledWidth, scaledHeight, null);
|
||||
|
||||
// 恢复原始设置
|
||||
g2d.setClip(originalClip);
|
||||
g2d.setRenderingHints(originalHints);
|
||||
}
|
||||
|
||||
// 计算文字与二维码垂直居中对齐的Y坐标
|
||||
int qrcodeTop = qrcodeOffsetY + QRCODE_OFFSET_Y;
|
||||
int qrcodeBottom = qrcodeTop + newQrcodeHeight;
|
||||
int qrcodeCenter = (qrcodeTop + qrcodeBottom) / 2;
|
||||
|
||||
// 两行文字的总高度
|
||||
int totalTextHeight = scenicLineHeight + dtLineHeight;
|
||||
|
||||
// 计算第一行文字的Y坐标(基线位置),使两行文字整体垂直居中于二维码
|
||||
int textStartY = qrcodeCenter - totalTextHeight / 2 + scenicFontMetrics.getAscent();
|
||||
|
||||
// 文字右对齐,放置在右下角,距离右边缘图片宽度的5%
|
||||
int textRightX = newImage.getWidth() - (int) (newImage.getWidth() * TEXT_RIGHT_MARGIN_RATIO);
|
||||
|
||||
g2d.setFont(scenicFont);
|
||||
g2d.setColor(scenicColor);
|
||||
g2d.drawString(info.getScenicLine(), textRightX - scenicLineWidth, textStartY);
|
||||
|
||||
g2d.setFont(datetimeFont);
|
||||
g2d.setColor(datetimeColor);
|
||||
g2d.drawString(info.getDatetimeLine(), textRightX - datetimeLineWidth, textStartY + scenicLineHeight);
|
||||
|
||||
String fileName = info.getWatermarkedFile().getName();
|
||||
String formatName = "jpg"; // 默认格式为 jpg
|
||||
if (fileName.endsWith(".png")) {
|
||||
formatName = "png";
|
||||
} else if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) {
|
||||
formatName = "jpg";
|
||||
}
|
||||
ImageWriter writer = ImageIO.getImageWritersByFormatName(formatName).next();
|
||||
ImageOutputStream ios;
|
||||
try {
|
||||
ios = ImageIO.createImageOutputStream(info.getWatermarkedFile());
|
||||
} catch (IOException e) {
|
||||
throw new ImageWatermarkException("图片保存失败,目标文件无法写入");
|
||||
}
|
||||
writer.setOutput(ios);
|
||||
try {
|
||||
// 使用 ImageWriter 设置写入质量
|
||||
ImageWriteParam writeParam = writer.getDefaultWriteParam();
|
||||
if (writeParam.canWriteCompressed()) {
|
||||
writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
|
||||
writeParam.setCompressionQuality(0.75f); // 设置写入质量为 75%
|
||||
}
|
||||
writer.write(null, new javax.imageio.IIOImage(newImage, null, null), writeParam);
|
||||
} catch (IOException e) {
|
||||
throw new ImageWatermarkException("图片保存失败");
|
||||
}
|
||||
finally {
|
||||
g2d.dispose();
|
||||
try {
|
||||
ios.close();
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
writer.dispose();
|
||||
}
|
||||
return info.getWatermarkedFile();
|
||||
}
|
||||
}
|
||||
@@ -646,7 +646,7 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
watermarkInfo.setWatermarkedFile(watermarkedFile);
|
||||
watermarkInfo.setQrcodeFile(qrCodeFile);
|
||||
watermarkInfo.setDatetime(new Date());
|
||||
watermarkInfo.setDtFormat("yyyy-MM-dd");
|
||||
watermarkInfo.setDtFormat("yyyy.MM.dd");
|
||||
|
||||
operator.process(watermarkInfo);
|
||||
|
||||
|
||||
@@ -287,10 +287,9 @@ public class ImageUtils {
|
||||
best.rotationDegrees = 0;
|
||||
best.pixelsLost = Integer.MAX_VALUE;
|
||||
|
||||
// 测试三种情况: 不旋转、旋转90度、旋转270度
|
||||
// 测试两种情况: 不旋转、旋转270度
|
||||
int[][] scenarios = {
|
||||
{0, srcWidth, srcHeight}, // 不旋转
|
||||
{90, srcHeight, srcWidth}, // 旋转90度
|
||||
{270, srcHeight, srcWidth} // 旋转270度
|
||||
};
|
||||
|
||||
@@ -331,35 +330,28 @@ public class ImageUtils {
|
||||
return source;
|
||||
}
|
||||
|
||||
if (degrees != 270) {
|
||||
throw new IllegalArgumentException("仅支持270度旋转");
|
||||
}
|
||||
|
||||
int width = source.getWidth();
|
||||
int height = source.getHeight();
|
||||
|
||||
// 90度和270度会交换宽高
|
||||
BufferedImage rotated;
|
||||
|
||||
// 270度会交换宽高
|
||||
BufferedImage rotated = new BufferedImage(height, width, source.getType());
|
||||
Graphics2D g2d = null;
|
||||
|
||||
|
||||
try {
|
||||
if (degrees == 90 || degrees == 270) {
|
||||
rotated = new BufferedImage(height, width, source.getType());
|
||||
g2d = rotated.createGraphics();
|
||||
|
||||
AffineTransform transform = new AffineTransform();
|
||||
if (degrees == 90) {
|
||||
transform.translate(height / 2.0, width / 2.0);
|
||||
transform.rotate(Math.PI / 2);
|
||||
transform.translate(-width / 2.0, -height / 2.0);
|
||||
} else { // 270度
|
||||
transform.translate(height / 2.0, width / 2.0);
|
||||
transform.rotate(-Math.PI / 2);
|
||||
transform.translate(-width / 2.0, -height / 2.0);
|
||||
}
|
||||
|
||||
g2d.setTransform(transform);
|
||||
g2d.drawImage(source, 0, 0, null);
|
||||
} else {
|
||||
throw new IllegalArgumentException("仅支持90度和270度旋转");
|
||||
}
|
||||
|
||||
g2d = rotated.createGraphics();
|
||||
|
||||
AffineTransform transform = new AffineTransform();
|
||||
transform.translate(height / 2.0, width / 2.0);
|
||||
transform.rotate(-Math.PI / 2);
|
||||
transform.translate(-width / 2.0, -height / 2.0);
|
||||
|
||||
g2d.setTransform(transform);
|
||||
g2d.drawImage(source, 0, 0, null);
|
||||
|
||||
return rotated;
|
||||
} finally {
|
||||
if (g2d != null) {
|
||||
|
||||
Reference in New Issue
Block a user