You've already forked FrameTour-BE
feat(watermark): 在二维码中央添加圆形头像
- 在 WatermarkInfo 实体中添加 faceFile 字段,用于存储头像文件 - 在 LeicaWatermarkOperator 和 NormalWatermarkOperator 中实现头像添加功能 - 在 GoodsServiceImpl 中下载用户头像并传递给 watermarkInfo
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -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("图片保存失败");
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
@@ -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 {
|
||||||
|
Reference in New Issue
Block a user