图片压缩
目标:可实现将上传图片压缩到1MB内
package com.wlh.zetc.restore.controller;
import cn.hutool.core.date.DateUtil;
import com.wlh.zetc.common.core.util.R;
import com.wlh.zetc.common.data.tenant.TenantContextHolder;
import com.wlh.zetc.restore.params.CompressionParams;
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.tags.Tag;
import lombok.AllArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.imageio.*;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.HashMap;
import java.util.Iterator;
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 static final Logger log = LoggerFactory.getLogger(QiniuController.class);
private static final long TARGET_SIZE = 1024 * 1024; // 目标大小:1MB
private static final int MAX_ATTEMPTS = 5; // 增加最大尝试次数
@Autowired
private ComplexWatermarkUtils complexWatermarkUtils;
@Autowired
private QiniuService qiniuService;
/**
* 图片上传接口
* @param file 图片文件
* @param location 位置信息
* @param latitudeLongitude 经纬度
* @param remarks 备注
* @param weather 天气
* @param watermark 水印类型 0:不添加水印 1:添加水印 2:添加水印但是不添加天气、位置、经纬度
* @return 上传结果
*/
@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) {
try {
// 验证文件
if (file == null || file.isEmpty()) {
return R.failed("文件不能为空");
}
// 验证文件格式
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
return R.failed("只支持图片文件上传");
}
// 压缩图片
MultipartFile compressedFile = compressImage(file);
MultipartFile multipartFile = null;
if (watermark != null && watermark.equals(0)) {
multipartFile = compressedFile;
} else {
multipartFile = complexWatermarkUtils.addComplexWatermark(compressedFile, DateUtil.now(),
location, weather, latitudeLongitude, remarks, watermark);
}
// 验证压缩后的文件大小
if (multipartFile.getSize() > TARGET_SIZE) {
log.warn("压缩后的文件仍然超过目标大小: {}MB", multipartFile.getSize() / (1024.0 * 1024.0));
}
// 上传文件处理
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("上传成功");
}
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
log.error("关闭输入流失败: ", e);
}
}
}
}
return R.failed("上传失败");
} catch (OutOfMemoryError e) {
log.error("图片处理发生内存溢出: ", e);
return R.failed("图片太大,处理失败");
} catch (Exception e) {
log.error("图片上传处理失败: ", e);
return R.failed("图片处理失败:" + e.getMessage());
}
}
/**
* 压缩图片
* @param file 原始图片
* @return 压缩后的图片
*/
private MultipartFile compressImage(MultipartFile file) throws IOException {
if (file == null || file.isEmpty()) {
return file;
}
// 如果文件已经小于1MB,直接返回
if (file.getSize() <= TARGET_SIZE) {
return file;
}
ImageInputStream iis = null;
ImageReader reader = null;
ByteArrayOutputStream baos = null;
try {
iis = ImageIO.createImageInputStream(file.getInputStream());
if (iis == null) {
throw new IOException("Cannot create ImageInputStream for the image");
}
Iterator<ImageReader> readers = ImageIO.getImageReaders(iis);
if (!readers.hasNext()) {
return file;
}
reader = readers.next();
reader.setInput(iis);
int originalWidth = reader.getWidth(0);
int originalHeight = reader.getHeight(0);
String formatName = reader.getFormatName();
// 初始压缩参数 - 默认读取上传图片尺寸
CompressionParams params = calculateInitialParams(file.getSize(), originalWidth, originalHeight);
byte[] compressedBytes = null;
int attempts = 0;
while (attempts < MAX_ATTEMPTS) {
attempts++;
log.debug("压缩尝试 #{}: scale={}, quality={}", attempts, params.scale, params.quality);
int targetWidth = (int) (originalWidth * params.scale);
int targetHeight = (int) (originalHeight * params.scale);
// 设置最小尺寸
targetWidth = Math.max(targetWidth, 100);
targetHeight = Math.max(targetHeight, 100);
BufferedImage compressedImage = new BufferedImage(targetWidth, targetHeight,
formatName.equalsIgnoreCase("png") ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = compressedImage.createGraphics();
try {
applyRenderingHints(g2d);
if (formatName.equalsIgnoreCase("png")) {
g2d.setComposite(AlphaComposite.Src);
}
BufferedImage originalImage = reader.read(0);
g2d.drawImage(originalImage, 0, 0, targetWidth, targetHeight, null);
} finally {
g2d.dispose();
}
baos = new ByteArrayOutputStream();
// 根据格式选择压缩方法
if (formatName.equalsIgnoreCase("jpeg") || formatName.equalsIgnoreCase("jpg")) {
compressedBytes = compressJpeg(compressedImage, params.quality, baos);
} else {
// 其他格式都转换为JPEG以获得更好的压缩率
compressedBytes = compressJpeg(compressedImage, params.quality, baos);
formatName = "jpeg";
}
// 检查压缩结果
if (compressedBytes.length <= TARGET_SIZE) {
break;
}
// 更新压缩参数
params = updateCompressionParams(params, compressedBytes.length);
}
//经过5次循环压缩后,图片大小依然大于1MB,采取以下压缩策略
// 如果还是超过目标大小,进行最终压缩(比较狠的方法)
if (compressedBytes.length > TARGET_SIZE) {
params.scale *= 0.8;
params.quality = 0.3f;
int finalWidth = (int) (originalWidth * params.scale);
int finalHeight = (int) (originalHeight * params.scale);
BufferedImage finalImage = new BufferedImage(finalWidth, finalHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = finalImage.createGraphics();
try {
applyRenderingHints(g2d);
BufferedImage tempImage = ImageIO.read(new ByteArrayInputStream(compressedBytes));
g2d.drawImage(tempImage, 0, 0, finalWidth, finalHeight, null);
} finally {
g2d.dispose();
}
baos = new ByteArrayOutputStream();
compressedBytes = compressJpeg(finalImage, params.quality, baos);
}
return new MockMultipartFile(
file.getName(),
file.getOriginalFilename(),
"image/" + formatName.toLowerCase(),
compressedBytes
);
} catch (Exception e) {
log.error("图片压缩失败: ", e);
return file;
} finally {
closeResources(reader, iis, baos);
}
}
/**
* 压缩参数类
*/
private static class CompressionParams {
double scale;
float quality;
CompressionParams(double scale, float quality) {
this.scale = scale;
this.quality = quality;
}
}
/**
* 计算初始压缩参数(还需要优化部分,对于大型图片的压缩比处理)
*/
private CompressionParams calculateInitialParams(long originalSize, int width, int height) {
double scale = 1.0;
float quality = 0.8f;
// 基于文件大小计算初始压缩参数
double sizeRatio = (double) TARGET_SIZE / originalSize;
//计算压缩参数大于目标大小
if (originalSize > TARGET_SIZE) {
// 对图片质量进行参数设置压缩,更激进的初始压缩率
scale = Math.sqrt(sizeRatio);
if (originalSize > 5 * TARGET_SIZE) {
quality = 0.6f;
} else if (originalSize > 2 * TARGET_SIZE) {
quality = 0.7f;
}
}
// 考虑图片分辨率,图片越大,压缩比越高
long pixels = (long) width * height;
if (pixels > 4096 * 2160) { // 4K
scale *= 0.5;
} else if (pixels > 1920 * 1080) { // 1080P
scale *= 0.7;
}
// 确保合理的压缩比例
scale = Math.min(1.0, Math.max(0.1, scale));
quality = Math.min(0.8f, Math.max(0.3f, quality));
return new CompressionParams(scale, quality);
}
/**
* 更新压缩参数(还需要优化部分,对于大型图片的压缩比处理)
*/
private CompressionParams updateCompressionParams(CompressionParams params, long currentSize) {
double sizeRatio = (double) TARGET_SIZE / currentSize;
// 如果距离图片目标大小很远,调整参数狠一点
if (currentSize > 2 * TARGET_SIZE) {
params.quality = Math.max(0.3f, params.quality - 0.2f);
params.scale *= Math.max(0.7, Math.sqrt(sizeRatio));
} else {
// 接近目标时,小幅度地调整
params.quality = Math.max(0.3f, params.quality - 0.1f);
params.scale *= Math.max(0.8, Math.sqrt(sizeRatio));
}
return params;
}
/**
* 应用渲染提示
*/
private void applyRenderingHints(Graphics2D g2d) {
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
/**
* 压缩JPEG图片
*/
private byte[] compressJpeg(BufferedImage image, float quality, ByteArrayOutputStream baos) throws IOException {
ImageWriter writer = ImageIO.getImageWritersByFormatName("jpeg").next();
ImageWriteParam param = writer.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(quality);
ImageOutputStream ios = ImageIO.createImageOutputStream(baos);
try {
writer.setOutput(ios);
writer.write(null, new IIOImage(image, null, null), param);
} finally {
ios.close();
writer.dispose();
}
return baos.toByteArray();
}
/**
* 关闭资源
*/
private void closeResources(ImageReader reader, ImageInputStream iis, ByteArrayOutputStream baos) {
if (reader != null) reader.dispose();
if (iis != null) try { iis.close(); } catch (IOException e) { log.error("关闭流失败", e); }
if (baos != null) try { baos.close(); } catch (IOException e) { log.error("关闭流失败", e); }
}
@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();
}
}