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 index f4b36c0..f28a9a6 100644 --- a/src/main/java/com/ycwl/basic/image/watermark/entity/WatermarkInfo.java +++ b/src/main/java/com/ycwl/basic/image/watermark/entity/WatermarkInfo.java @@ -14,6 +14,7 @@ public class WatermarkInfo { */ private File watermarkedFile; private File qrcodeFile; + private File faceFile; private String scenicLine; private String secondLine; private String thirdLine; @@ -35,4 +36,5 @@ public class WatermarkInfo { } return scenicLine; } + } 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 index a1368df..7e67287 100644 --- a/src/main/java/com/ycwl/basic/image/watermark/operator/LeicaWatermarkOperator.java +++ b/src/main/java/com/ycwl/basic/image/watermark/operator/LeicaWatermarkOperator.java @@ -4,11 +4,13 @@ import com.ycwl.basic.image.watermark.entity.WatermarkInfo; import com.ycwl.basic.image.watermark.exception.ImageWatermarkException; import lombok.extern.slf4j.Slf4j; +import javax.imageio.IIOImage; 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; @@ -67,6 +69,7 @@ public class LeicaWatermarkOperator implements IOperator { BufferedImage baseImage; BufferedImage qrcodeImage; BufferedImage logoImage; + BufferedImage faceImage = null; // 从类路径加载 zt-logo.png InputStream logoInputStream = getClass().getResourceAsStream("/zt-logo.png"); if (logoInputStream == null) { @@ -77,6 +80,13 @@ public class LeicaWatermarkOperator implements IOperator { qrcodeImage = ImageIO.read(info.getQrcodeFile()); logoImage = ImageIO.read(logoInputStream); logoInputStream.close(); + 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("图片打开失败"); } @@ -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 qrcodeY = EXTRA_BORDER_PX + baseImage.getHeight() + OFFSET_Y - QRCODE_OFFSET_Y; - + 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坐标 int qrcodeTop = qrcodeY; int qrcodeBottom = qrcodeTop + newQrcodeHeight; @@ -153,7 +205,7 @@ public class LeicaWatermarkOperator implements IOperator { writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); 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) { throw new ImageWatermarkException("图片保存失败"); } 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 index 6b53d75..9dea395 100644 --- a/src/main/java/com/ycwl/basic/image/watermark/operator/NormalWatermarkOperator.java +++ b/src/main/java/com/ycwl/basic/image/watermark/operator/NormalWatermarkOperator.java @@ -59,9 +59,17 @@ public class NormalWatermarkOperator implements IOperator { 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("图片打开失败"); } @@ -89,6 +97,47 @@ public class NormalWatermarkOperator implements IOperator { g2d.setClip(circle); g2d.drawImage(qrcodeImage, offsetX, offsetY + QRCODE_OFFSET_Y, newQrcodeWidth, newQrcodeHeight, null); 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坐标 int qrcodeTop = offsetY + QRCODE_OFFSET_Y; int qrcodeBottom = qrcodeTop + newQrcodeHeight; diff --git a/src/main/java/com/ycwl/basic/service/mobile/impl/GoodsServiceImpl.java b/src/main/java/com/ycwl/basic/service/mobile/impl/GoodsServiceImpl.java index b618376..0ae63a9 100644 --- a/src/main/java/com/ycwl/basic/service/mobile/impl/GoodsServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/mobile/impl/GoodsServiceImpl.java @@ -691,6 +691,13 @@ public class GoodsServiceImpl implements GoodsService { return defaultUrlList; } 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 collect = defaultUrlList.stream().peek(item -> { Optional any = watermarkEntityList.stream() .filter(watermark -> watermark.getSourceId().equals(item.getGoodsId())) @@ -723,6 +730,7 @@ public class GoodsServiceImpl implements GoodsService { info.setQrcodeFile(qrcode); info.setScenicLine(text); info.setDatetime(item.getCreateTime()); + info.setFaceFile(faceFile); info.setDtFormat(scenicConfig.getWatermarkDtFormat()); info.setWatermarkedFile(watermarkedFile); try {