图片压缩策略处理

图片压缩

目标:可实现将上传图片压缩到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();
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三横同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值