图片左下角水印添加
满足需求:可以对不同类型尺寸的照片、图片进行水印的添加,实现尺寸自适应添加水印。
水印效果
代码实现
Controller
package com.wlh.zetc.restore.controller;
import cn.hutool.core.date.DateUtil;
import com.alibaba.nacos.client.utils.TenantUtil;
import com.wlh.zetc.admin.api.feign.RemoteTenantService;
import com.wlh.zetc.common.core.util.R;
import com.wlh.zetc.common.data.tenant.TenantBroker;
import com.wlh.zetc.common.data.tenant.TenantContextHolder;
import com.wlh.zetc.restore.service.QiniuService;
import com.wlh.zetc.restore.utils.ComplexWatermarkUtils;
import com.wlh.zetc.restore.utils.TextUtils;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
/**
* 七牛云存储
*
* @author wanghailin
* @date 2024-03-12 10:19:56
*/
@Controller
@RequestMapping("/qiniu" )
@Tag(description = "file" , name = "七牛云文件存储" )
@AllArgsConstructor
//@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
public class QiniuController
{
private final QiniuService qiniuService;
private final ComplexWatermarkUtils complexWatermarkUtils;
/**
*
* @param file
* @param location
* @param latitudeLongitude
* @param remarks
* @param weather
* @param watermark 0:不添加水印 1:添加水印 2:添加水印但是不添加天气、位置、经纬度
* @return
* @throws IOException
*/
@PostMapping("/image/upload")
@ResponseBody
public R uploadImage(@RequestParam(value = "file", required = false) MultipartFile file,
@RequestParam(value = "location", required = false) String location,
@RequestParam(value = "latitudeLongitude", required = false) String latitudeLongitude,
@RequestParam(value = "remarks", required = false) String remarks,
@RequestParam(value = "weather", required = false) String weather,
@RequestParam(value = "watermark", required = false,defaultValue = "1") Integer watermark) throws IOException {
MultipartFile multipartFile = null;
if (watermark != null && watermark.equals(0)){
multipartFile = file;
}else {
multipartFile = complexWatermarkUtils.addComplexWatermark(file, DateUtil.now(), location, weather,latitudeLongitude,remarks,watermark);
}
System.out.println(multipartFile.getSize());
if(!multipartFile.isEmpty())
{
InputStream in = multipartFile.getInputStream();
String filename = multipartFile.getOriginalFilename();
String project = "project"+ TenantContextHolder.getTenantId()+"/";
try
{
String key = project+ TextUtils.generateFileName(filename);
String imageUrl = qiniuService.uploadImage2qiniu(in,key);
if(imageUrl!=null)
{
HashMap<String, String> map = new HashMap<>();
String privateDownloadUrl = qiniuService.getPrivateDownloadUrl(imageUrl);
map.put("url",imageUrl);
map.put("accessibleUrl",privateDownloadUrl);
return R.ok(map).setMsg("上传成功");
}
}
catch(IllegalArgumentException e)
{
e.printStackTrace();
}
}
return R.failed("上传失败");
}
@DeleteMapping("/image/delete")
@ResponseBody
public R deleteImage(@RequestParam("image")String imageUrl)
{
boolean deleted = qiniuService.deleteImageFromQiniu(imageUrl);
if(deleted)
{
return R.ok(null,"删除成功");
}
return R.failed("删除失败");
}
@GetMapping("/image/download")
@ResponseBody
public R downloadImage(@RequestParam("image")String imageUrl)
{
try {
String privateDownloadUrl = qiniuService.getPrivateDownloadUrl(imageUrl);
Map<String, String> map = new HashMap<>();
map.put("url",privateDownloadUrl);
return R.ok().setData(map);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return R.ok();
}
}
Utils
package com.wlh.zetc.restore.utils;
import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.io.IoUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.drew.imaging.ImageMetadataReader;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;
import com.wlh.zetc.admin.api.entity.SysTenant;
import com.wlh.zetc.admin.api.feign.RemoteTenantService;
import com.wlh.zetc.admin.api.feign.RemoteUserService;
import com.wlh.zetc.common.core.util.R;
import com.wlh.zetc.common.data.tenant.TenantContextHolder;
import com.wlh.zetc.common.security.util.SecurityUtils;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
/**
* 至农水印处理工具类
*
* @author wanghailin
* @date 2024-03-18 10:49:33
*/
@Service
@AllArgsConstructor
public class ComplexWatermarkUtils {
private final RemoteTenantService tenantService;
//水印图片地址
private static String watermarkImageURL = "https://qiniu.zjshjkj.com/logo/zhinong12.png";
//水印添加
public MultipartFile addComplexWatermark(MultipartFile file, String time, String location, String weather, String latitudeLongitude, String remarks,Integer watermark) {
try {
//MultipartFile multipartFile = correctImg(file);
BufferedImage srcImg = null;
srcImg = ImgUtil.read(file.getInputStream());
Graphics2D g2d = srcImg.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int watermarkAreaHeight = (int) (srcImg.getHeight() * 0.10);
int watermarkAreaWide = (int) (srcImg.getWidth() * 0.01);
BufferedImage watermarkImage = ImgUtil.read(getWatermarkImage(watermarkImageURL).getInputStream());
int scaledWatermarkWidth = watermarkImage.getWidth() * watermarkAreaHeight / watermarkImage.getHeight();
Image scale = ImgUtil.scale(watermarkImage, scaledWatermarkWidth, watermarkAreaHeight);
int x = watermarkAreaWide;
int y = watermarkAreaHeight * 7 - watermarkAreaHeight / 2;
g2d.drawImage(scale, x, y, null);
//获取当前项目名称
R tenant = tenantService.getByTenantId(TenantContextHolder.getTenantId());
SysTenant project = (SysTenant) tenant.getData();
// 设置较大字体属性,仅用于 projectName
int projectNameFontSize = watermarkAreaHeight / 3;
Font projectNameFont = new Font("宋体", Font.BOLD, projectNameFontSize);
g2d.setFont(projectNameFont);
g2d.setColor(Color.WHITE);
// 将 projectName 分割为多行文本
if(StringUtils.isNotEmpty(project.getName())){
List<String> projectNameLines =splitTextToLines(project.getName(), 20);
int projectNameY = y + watermarkAreaHeight +projectNameFontSize;
for (String line : projectNameLines) {
g2d.drawString(line, x, projectNameY);
projectNameY += g2d.getFontMetrics().getHeight();
}
// 设置文本属性,根据水印区域大小调整字体大小
int fontSize = watermarkAreaHeight / 5;
Font font = new Font("宋体", Font.BOLD, fontSize);
g2d.setFont(font);
FontMetrics metrics = g2d.getFontMetrics(font);
int textHeight = metrics.getHeight();
// 绘制黄色竖杠
g2d.setColor(Color.YELLOW);
int barX = x;
int barY = projectNameY - watermarkAreaHeight / 3; // 根据 projectName 的最终位置调整
if(watermark.equals(2)){
g2d.fillRect(barX, barY, textHeight /4, textHeight * 2 + 5 * 3);
}else {
g2d.fillRect(barX, barY, textHeight /4, textHeight * 5 + 5 * 3);
}
g2d.setColor(Color.WHITE);
int textX = barX + watermarkAreaWide ;
int textY = barY + textHeight;
if(!StringUtils.isNotEmpty(remarks)){
remarks = "";
}
if(watermark.equals(2)){
String[] texts = new String[]{"时 间:"+time, "备 注:"+remarks};
for (String text : texts) {
g2d.drawString(text, textX, textY);
textY += textHeight + 5;
}
}else {
String[] texts = new String[]{"时 间:"+time, "地 点:"+location, "天 气:"+weather, "经纬度:"+latitudeLongitude, "备 注:"+remarks};
for (String text : texts) {
g2d.drawString(text, textX, textY);
textY += textHeight + 5;
}
}
}
g2d.dispose();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
ImageIO.write(srcImg, "png", baos);
} catch (Exception e) {
e.printStackTrace();
return null;
}
MultipartFile resultFile = new MockMultipartFile(file.getName(), file.getOriginalFilename(), file.getContentType(), baos.toByteArray());
IoUtil.close(baos);
return resultFile;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
//文件下载
public static MultipartFile getWatermarkImage(String fileUrl) {
try {
URL url = new URL(fileUrl);
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setRequestMethod("GET");
httpURLConnection.connect();
String fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1);
String contentType = httpURLConnection.getContentType();
InputStream inputStream = httpURLConnection.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) > -1) {
baos.write(buffer, 0, len);
}
baos.flush();
MultipartFile multipartFile = new MockMultipartFile(fileName, fileName, contentType, baos.toByteArray());
inputStream.close();
baos.close();
return multipartFile;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
//图片矫正
public static MultipartFile correctImg(MultipartFile srcFile) {
File outFile = new File(srcFile.getName());
try (FileOutputStream fos = new FileOutputStream(outFile)) {
int angle = getAngle(srcFile);
if (angle != 90 && angle != 270) {
// todo 不需要旋转,直接返回
return srcFile;
} else {
BufferedImage srcImg = ImageIO.read(srcFile.getInputStream());
int imgWidth = srcImg.getHeight();
int imgHeight = srcImg.getWidth();
double centerWidth = ((double) imgWidth) / 2;
double centerHeight = ((double) imgHeight) / 2;
BufferedImage targetImg = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g = targetImg.createGraphics();
g.rotate(Math.toRadians(angle), centerWidth, centerHeight);
g.drawImage(srcImg, (imgWidth - srcImg.getWidth()) / 2, (imgHeight - srcImg.getHeight()) / 2, null);
g.rotate(Math.toRadians(-angle), centerWidth, centerHeight);
g.dispose();
// 输出图片
ImageIO.write(targetImg, "jpg", fos);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
ImgUtil.write(srcImg, srcFile.getContentType(), baos);
} catch (Exception e) {
e.printStackTrace();
return null;
}
MultipartFile resultFile = new MockMultipartFile(srcFile.getName(), srcFile.getOriginalFilename(), srcFile.getContentType(), baos.toByteArray());
IoUtil.close(baos);
return resultFile;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
//角度判断
private static int getAngle(MultipartFile file) throws Exception {
Metadata metadata = ImageMetadataReader.readMetadata(file.getInputStream());
for (Directory directory : metadata.getDirectories()) {
for (Tag tag : directory.getTags()) {
if ("Orientation".equals(tag.getTagName())) {
String orientation = tag.getDescription();
if (orientation.contains("90")) {
return 90;
} else if (orientation.contains("180")) {
return 180;
} else if (orientation.contains("270")) {
return 270;
}
}
}
}
return 0;
}
// 辅助方法:将文本分割为多行
private static List<String> splitTextToLines(String text, int maxCharsPerLine) {
List<String> lines = new ArrayList<>();
for (int start = 0; start < text.length(); start += maxCharsPerLine) {
int end = Math.min(start + maxCharsPerLine, text.length());
lines.add(text.substring(start, end));
}
return lines;
}
}
七牛云service
package com.wlh.zetc.restore.service;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
/**
* 七牛文件存储
*
* @author wanghailin
* @date 2024-03-12 10:21:35
*/
public interface QiniuService {
String uploadImage2qiniu(InputStream in, String key);
boolean deleteImageFromQiniu(String key);
String getPrivateDownloadUrl(String fileName) throws UnsupportedEncodingException;
}
七牛云impl
package com.wlh.zetc.restore.service.impl;
import com.google.gson.Gson;
import com.qiniu.common.QiniuException;
import com.qiniu.common.Zone;
import com.qiniu.http.Response;
import com.qiniu.storage.*;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.util.Auth;
import com.wlh.zetc.restore.properties.QiniuProperties;
import com.wlh.zetc.restore.service.QiniuService;
import com.wlh.zetc.restore.utils.TextUtils;
import io.netty.channel.unix.Unix;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
/**
* 七牛文件存储
*
* @author wanghailin
* @date 2024-03-12 10:21:35
*/
@Service
public class QiniuServiceImpl implements QiniuService
{
private final String domain;
private final String bucketName;
private final String ak;
private final String sk;
// 七牛文件上传管理器
private final Configuration cfg;
private final Auth auth;
@Autowired
public QiniuServiceImpl(QiniuProperties oss)
{
this.ak = oss.getAccessKey();
this.sk = oss.getSecretKey();
this.domain = oss.getDomain(); // CDN域名
this.bucketName = oss.getBucketName();
// //构造一个带指定 Region 对象的配置类
cfg = new Configuration(Zone.zone0());
auth = Auth.create(ak,sk);
}
/**
* 上传图片到七牛云
* @return 图片url
* */
@Override
public String uploadImage2qiniu(InputStream in, String key)
{
try {
UploadManager uploadManager = new UploadManager(cfg);
// 根据命名空间生成的上传token
String upToken = auth.uploadToken(bucketName);
Response response = uploadManager.put(in,key,upToken,null, null);
//解析上传成功的结果
DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
//System.out.println(putRet.key);
//System.out.println(putRet.hash);
//return String.format("http://%s/%s",this.domain,putRet.key);
return putRet.key;
} catch (QiniuException ex) {
Response r = ex.response;
System.err.println(r.toString());
try {
System.err.println(r.bodyString());
} catch (QiniuException ex2) {
//ignore
}
}
return null;
}
/**
* 删除图片
* */
@Override
public boolean deleteImageFromQiniu(String imageUrl)
{
BucketManager bucketManager = new BucketManager(auth, cfg);
try {
String key= TextUtils.getKey(imageUrl);
Response response = bucketManager.delete(bucketName,key);
return response.isOK();
} catch (QiniuException ex) {
//如果遇到异常,说明删除失败
System.err.println(ex.code());
System.err.println(ex.response.toString());
}
return false;
}
/**
* 获取文件下载路径
*
* @param fileName
* @return
* @throws UnsupportedEncodingException
*/
public String getPrivateDownloadUrl(String fileName) throws UnsupportedEncodingException {
//文件https访问配置
DownloadUrl url = new DownloadUrl(domain, true, fileName);
//DownloadUrl url = new DownloadUrl(domain, false, fileName);
long expireInSeconds = 3600;//1小时,可以自定义链接过期时间
long deadline = System.currentTimeMillis()/1000 + expireInSeconds;
Auth auth = Auth.create(ak, sk);
String urlString = null;
try {
urlString = url.buildURL(auth, deadline);
} catch (QiniuException e) {
throw new RuntimeException(e);
}
return urlString;
}
}
七牛云配置类
package com.wlh.zetc.restore.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 七牛文件存储
*
* @author wanghailin
* @date 2024-03-12 10:21:35
*/
@Component
@ConfigurationProperties(prefix = "oss.qiniu")
@Data
public class QiniuProperties
{
private String domain; // CDN域名
private String accessKey; // ACCESS_KEY
private String secretKey; // SECRET_KEY
private String bucketName; // 空间名称
}
七牛云配置yml
oss:
qiniu:
domain: qiniu.znkj0215.com # 访问域名(正式访问域名地址) 暂未配置https
# domain: qiniu.iswhl.com # 访问域名(测试访问域名地址) 已配置https
accessKey: APlM_0fW1A_PRS5bQ92rdGf9oSW-5q9mZK3Tv6yk # 公钥
secretKey: Ri2eN9h4htBjZa8J8n_7QBfsAAvM_Arz5_CLqWth # 私钥
bucketName: zhinonggengdi #存储空间名称