feat(watermark): 在二维码中央添加圆形头像

- 在 WatermarkInfo 实体中添加 faceFile 字段,用于存储头像文件
- 在 LeicaWatermarkOperator 和 NormalWatermarkOperator 中实现头像添加功能
- 在 GoodsServiceImpl 中下载用户头像并传递给 watermarkInfo
This commit is contained in:
2025-08-20 23:13:38 +08:00
parent 29f142f53f
commit f981791039
4 changed files with 113 additions and 2 deletions

View File

@@ -14,6 +14,7 @@ public class WatermarkInfo {
*/ */
private File watermarkedFile; private File watermarkedFile;
private File qrcodeFile; private File qrcodeFile;
private File faceFile;
private String scenicLine; private String scenicLine;
private String secondLine; private String secondLine;
private String thirdLine; private String thirdLine;
@@ -35,4 +36,5 @@ public class WatermarkInfo {
} }
return scenicLine; return scenicLine;
} }
} }

View File

@@ -4,11 +4,13 @@ import com.ycwl.basic.image.watermark.entity.WatermarkInfo;
import com.ycwl.basic.image.watermark.exception.ImageWatermarkException; import com.ycwl.basic.image.watermark.exception.ImageWatermarkException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter; import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream; import javax.imageio.stream.ImageOutputStream;
import java.awt.*; import java.awt.*;
import java.awt.geom.Ellipse2D;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -67,6 +69,7 @@ public class LeicaWatermarkOperator implements IOperator {
BufferedImage baseImage; BufferedImage baseImage;
BufferedImage qrcodeImage; BufferedImage qrcodeImage;
BufferedImage logoImage; BufferedImage logoImage;
BufferedImage faceImage = null;
// 从类路径加载 zt-logo.png // 从类路径加载 zt-logo.png
InputStream logoInputStream = getClass().getResourceAsStream("/zt-logo.png"); InputStream logoInputStream = getClass().getResourceAsStream("/zt-logo.png");
if (logoInputStream == null) { if (logoInputStream == null) {
@@ -77,6 +80,13 @@ public class LeicaWatermarkOperator implements IOperator {
qrcodeImage = ImageIO.read(info.getQrcodeFile()); qrcodeImage = ImageIO.read(info.getQrcodeFile());
logoImage = ImageIO.read(logoInputStream); logoImage = ImageIO.read(logoInputStream);
logoInputStream.close(); logoInputStream.close();
if (info.getFaceFile() != null && info.getFaceFile().isFile()) {
try {
faceImage = ImageIO.read(info.getFaceFile());
} catch (IOException e) {
log.warn("头像文件读取失败", e);
}
}
} catch (IOException e) { } catch (IOException e) {
throw new ImageWatermarkException("图片打开失败"); throw new ImageWatermarkException("图片打开失败");
} }
@@ -110,9 +120,51 @@ public class LeicaWatermarkOperator implements IOperator {
// 计算二维码的位置 // 计算二维码的位置
int qrcodeX = newImage.getWidth() + EXTRA_BORDER_PX - OFFSET_X - newQrcodeWidth - QRCODE_OFFSET_X - Math.max(scenicLineWidth, datetimeLineWidth); int qrcodeX = newImage.getWidth() + EXTRA_BORDER_PX - OFFSET_X - newQrcodeWidth - QRCODE_OFFSET_X - Math.max(scenicLineWidth, datetimeLineWidth);
int qrcodeY = EXTRA_BORDER_PX + baseImage.getHeight() + OFFSET_Y - QRCODE_OFFSET_Y; int qrcodeY = EXTRA_BORDER_PX + baseImage.getHeight() + OFFSET_Y - QRCODE_OFFSET_Y;
g2d.drawImage(qrcodeImage, qrcodeX, qrcodeY, newQrcodeWidth, newQrcodeHeight, null); g2d.drawImage(qrcodeImage, qrcodeX, qrcodeY, newQrcodeWidth, newQrcodeHeight, null);
// 在二维码中央绘制圆形头像
if (faceImage != null) {
// 计算圆形头像的尺寸和位置
int avatarDiameter = (int) (newQrcodeHeight * 0.45);
int avatarX = qrcodeX + (newQrcodeWidth - avatarDiameter) / 2;
int avatarY = qrcodeY + (newQrcodeHeight - avatarDiameter) / 2;
// 保存当前的渲染设置和剪切区域
RenderingHints originalHints = g2d.getRenderingHints();
Shape originalClip = g2d.getClip();
// 设置高质量渲染
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坐标 // 计算文字与二维码垂直居中对齐的Y坐标
int qrcodeTop = qrcodeY; int qrcodeTop = qrcodeY;
int qrcodeBottom = qrcodeTop + newQrcodeHeight; int qrcodeBottom = qrcodeTop + newQrcodeHeight;
@@ -153,7 +205,7 @@ public class LeicaWatermarkOperator implements IOperator {
writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
writeParam.setCompressionQuality(0.75f); // 设置写入质量为 75% writeParam.setCompressionQuality(0.75f); // 设置写入质量为 75%
} }
writer.write(null, new javax.imageio.IIOImage(newImage, null, null), writeParam); writer.write(null, new IIOImage(newImage, null, null), writeParam);
} catch (IOException e) { } catch (IOException e) {
throw new ImageWatermarkException("图片保存失败"); throw new ImageWatermarkException("图片保存失败");
} }

View File

@@ -59,9 +59,17 @@ public class NormalWatermarkOperator implements IOperator {
public File process(WatermarkInfo info) throws ImageWatermarkException { public File process(WatermarkInfo info) throws ImageWatermarkException {
BufferedImage baseImage; BufferedImage baseImage;
BufferedImage qrcodeImage; BufferedImage qrcodeImage;
BufferedImage faceImage = null;
try { try {
baseImage = ImageIO.read(info.getOriginalFile()); baseImage = ImageIO.read(info.getOriginalFile());
qrcodeImage = ImageIO.read(info.getQrcodeFile()); 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) { } catch (IOException e) {
throw new ImageWatermarkException("图片打开失败"); throw new ImageWatermarkException("图片打开失败");
} }
@@ -89,6 +97,47 @@ public class NormalWatermarkOperator implements IOperator {
g2d.setClip(circle); g2d.setClip(circle);
g2d.drawImage(qrcodeImage, offsetX, offsetY + QRCODE_OFFSET_Y, newQrcodeWidth, newQrcodeHeight, null); g2d.drawImage(qrcodeImage, offsetX, offsetY + QRCODE_OFFSET_Y, newQrcodeWidth, newQrcodeHeight, null);
g2d.setClip(originalClip); g2d.setClip(originalClip);
// 在圆形二维码中央绘制圆形头像
if (faceImage != null) {
// 计算圆形头像的尺寸和位置
int avatarDiameter = (int) (newQrcodeHeight * 0.45);
int avatarX = offsetX + (newQrcodeWidth - avatarDiameter) / 2;
int avatarY = offsetY + 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坐标 // 计算文字与二维码垂直居中对齐的Y坐标
int qrcodeTop = offsetY + QRCODE_OFFSET_Y; int qrcodeTop = offsetY + QRCODE_OFFSET_Y;
int qrcodeBottom = qrcodeTop + newQrcodeHeight; int qrcodeBottom = qrcodeTop + newQrcodeHeight;

View File

@@ -691,6 +691,13 @@ public class GoodsServiceImpl implements GoodsService {
return defaultUrlList; return defaultUrlList;
} }
tmpFile.add(qrcode); tmpFile.add(qrcode);
File faceFile = new File("face_"+face.getId()+".jpg");
try {
HttpUtil.downloadFile(face.getFaceUrl().replace("oss.zhentuai.com", "frametour-assets.oss-cn-shanghai-internal.aliyuncs.com"), faceFile);
tmpFile.add(faceFile);
} catch (Exception e) {
log.error("download face error", e);
}
List<GoodsUrlVO> collect = defaultUrlList.stream().peek(item -> { List<GoodsUrlVO> collect = defaultUrlList.stream().peek(item -> {
Optional<SourceWatermarkEntity> any = watermarkEntityList.stream() Optional<SourceWatermarkEntity> any = watermarkEntityList.stream()
.filter(watermark -> watermark.getSourceId().equals(item.getGoodsId())) .filter(watermark -> watermark.getSourceId().equals(item.getGoodsId()))
@@ -723,6 +730,7 @@ public class GoodsServiceImpl implements GoodsService {
info.setQrcodeFile(qrcode); info.setQrcodeFile(qrcode);
info.setScenicLine(text); info.setScenicLine(text);
info.setDatetime(item.getCreateTime()); info.setDatetime(item.getCreateTime());
info.setFaceFile(faceFile);
info.setDtFormat(scenicConfig.getWatermarkDtFormat()); info.setDtFormat(scenicConfig.getWatermarkDtFormat());
info.setWatermarkedFile(watermarkedFile); info.setWatermarkedFile(watermarkedFile);
try { try {